ECharts学习笔记

如何调试 Echarts?

0、先通读文档

1、下载源码

2、npm install

3、npm run dev

4、在 test 目录下创建一个 html 文件,配置图形(参考 test 下已有的 html 文件)

5、访问该 html 即可

6、善用 debugger 和 console.trace()

基本概念

如何用好 ECharts

ECharts 是一个很成熟易用的工具,要让其发挥出价值,关键还是看你的想象力。

设计思想

ECharts is designed as a data driven streaming pipeline with stages of data processing, visual encoding and rendering, which produces graphic elements finally.

https://www.sciencedirect.com/science/article/pii/S2468502X18300068

  • 数据驱动

  • 流式管线进行数据处理

  • 任务系统

  • 多线程模式:任务计算(Web Worker)与 Canvas 渲染(JS Main Thread)分离开,但是似乎没实现?

  • 大数据的分片处理

  • 扩展机制

关键词

合并策略

lazyUpdate

silent

OptionManager(优秀的设计)

GlobalModel

ecModel(就是 GlobalModel)

pipeline

progressive

seriesModel

task

图表库规范

详见百度的文档:https://github.com/ecomfe/spec/blob/master/chart.md

程序设计

继承关系

(TODO)程序执行流程

Echarts5 图表渲染过程分析

执行流程

schedule 调度流程

renderTask

pipe

ChartView 和 ComponentView 的区别

2 者很类似,提供的方法也类似。

区别在于 ChartView 多了一个renderTask,构造函数里初始化了 renderTask。

(TODO)继承关系

(TODO)Model 和 View 的设计

类的继承关系如下:

animate

Model

数据结构

整个 BarSeriesModel:

animate

单根柱子的 Model:

animate

特性

BarModel 不是单个数据,是整个数据的管理器,一个 BarChart 只有一个 BarSeriesModel 和一个 BarView 对象。

BarModel 中的 itemModel 才是单个数据,当然,itemModel 也是一个 Model 实例:

1
const itemModel = data.getItemModel<BarDataItemOption>(dataIndex);

model 可以嵌套:

1
seriesModel.getModel(modelName);

SeriesModel 中实现了数据驱动的概念,可以从 BarView.ts 中看到:

1
2
3
4
5
6
7
const data = seriesModel.getData();
data
.diff(oldData)
.add(function (dataIndex) {})
.update(function (newIndex, oldIndex) {})
.remove(function (dataIndex) {})
.execute();

Model 的 get 方法,都是返回时动态构建 Model 实例的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 获取一个数据的Model
getItemModel<ItemOpts extends unknown = unknown>(idx: number): Model<ItemOpts
// Extract item option with value key. FIXME will cause incompatitable issue
// Extract<HostModel['option']['data'][number], { value?: any }>
> {
const hostModel = this.hostModel;
const dataItem = this.getRawDataItem(idx) as ModelOption;
return new Model(dataItem, hostModel, hostModel && hostModel.ecModel);
}

// 获取制定的Model
getModel(path: string | readonly string[], parentModel?: Model): Model<any> {
const hasPath = path != null;
const pathFinal = hasPath ? this.parsePath(path) : null;
const obj = hasPath
? this._doGet(pathFinal)
: this.option;

parentModel = parentModel || (
this.parentModel
&& this.parentModel.getModel(this.resolveParentPath(pathFinal) as [string])
);

return new Model(obj, parentModel, this.ecModel);
}

为什么要这样设计呢?

样式属性的获取

Model.ts 上,有一个 option 属性,是专门用来存放用户设置的配置的,获取也是从这上面获取。model.get([‘itemStyle’, ‘borderRadius’])实际上调用的是下面的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private _doGet(pathArr: readonly string[], parentModel?: Model<Dictionary<any>>) {
let obj = this.option;
if (!pathArr) {
return obj;
}

for (let i = 0; i < pathArr.length; i++) {
// Ignore empty
if (!pathArr[i]) {
continue;
}
// obj could be number/string/... (like 0)
obj = (obj && typeof obj === 'object')
? (obj as ModelOption)[pathArr[i] as keyof ModelOption] : null;
if (obj == null) {
break;
}
}
if (obj == null && parentModel) {
obj = parentModel._doGet(
this.resolveParentPath(pathArr) as [string],
parentModel.parentModel
) as any;
}

return obj;
}

这个设计我们可以借鉴下。

View

View 中的核心是 render()函数

1
2
3
render(seriesModel: BarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload){
this._model = seriesModel;
}

this._model 是在 render 中赋值的

这里不会改变 model 数据,都是 get 数据,比如:

1
seriesModel.get(attrName);

Model 和 View 的注册

在 src/chart/bar/install.ts 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
export function install(registers: EChartsExtensionInstallRegisters) {
registers.registerChartView(BarView);
registers.registerSeriesModel(BarSeries);

registers.registerLayout(
registers.PRIORITY.VISUAL.LAYOUT,
zrUtil.curry(layout, 'bar')
);
// Use higher prority to avoid to be blocked by other overall layout, which do not
// only exist in this module, but probably also exist in other modules, like `barPolar`.
registers.registerLayout(
registers.PRIORITY.VISUAL.PROGRESSIVE_LAYOUT,
largeLayout
);

// Down sample after filter
registers.registerProcessor(
registers.PRIORITY.PROCESSOR.STATISTIC,
dataSample('bar')
);

/**
* @payload
* @property {string} [componentType=series]
* @property {number} [dx]
* @property {number} [dy]
* @property {number} [zoom]
* @property {number} [originX]
* @property {number} [originY]
*/
registers.registerAction(
{
type: 'changeAxisOrder',
event: 'changeAxisOrder',
update: 'update',
},
function (payload, ecModel) {
const componentType = payload.componentType || 'series';

ecModel.eachComponent(
{ mainType: componentType, query: payload },
function (componentModel) {
if (payload.sortInfo) {
(componentModel as CartesianAxisModel).axis.setCategorySortInfo(
payload.sortInfo
);
}
}
);
}
);
}

概念

坐标系

源码位置:src/coord,共有这 7 种坐标系:

直角/笛卡尔坐标系(cartesian)

/kɑrˈtiʒən/

https://echarts.apache.org/zh/option.html#grid

https://en.wikipedia.org/wiki/Cartesian_coordinate_system

常用于折线图、柱状图、散点图

极坐标系(polar)

https://echarts.apache.org/zh/option.html#polar

常用于折线图、散点图

平行坐标系(parallel)

https://echarts.apache.org/zh/option.html#parallel

地理坐标系(geo)

https://echarts.apache.org/zh/option.html#geo

日历坐标系(calendar)

很适合在日历中画热力图、散点图,特别是散点图,样式调得好,再加点动画,会很惊艳

雷达图坐标系(radar)

https://echarts.apache.org/zh/option.html#radar

只适用于雷达图。该组件等同 ECharts 2 中的 polar 组件。因为 3 中的 polar 被重构为标准的极坐标组件,为避免混淆,雷达图使用 radar 组件作为其坐标系。

单轴坐标系(single)

https://echarts.apache.org/zh/option.html#singleAxis

可以被应用到散点图中展现一维数据。

程序流程

我们以饼图的 SVG 绘制流程为例,通过调试代码(手动 throw 一个异常或者打印 console.trace()),可以看到调用栈大致是这样的:

1
2
3
4
5
6
7
8
9
at bindStyle (graphic.ts:110)
at Object.brush (graphic.ts:326)
at SVGPainter._paintList (Painter.ts:240)
at SVGPainter.refresh (Painter.ts:180)
at ZRender.refreshImmediately (zrender.ts:220)
at ZRender._flush (zrender.ts:253)
at ZRender.flush (zrender.ts:244)
at ECharts.setOption (echarts.ts:637)
at pie-action.html:57

关键代码

绘制图形

这里有个很关键的函数,就是 zrender/src/svg/graphic.ts 中的svgPath

因为 SVG 的图形最终都是通过 Path 绘制的,所以这里的 brush()方法,就是绘制的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const svgPath: SVGProxy<Path> = {
brush(el: Path) {
const style = el.style;

let svgEl = el.__svgEl;
if (!svgEl) {
svgEl = createElement('path');
el.__svgEl = svgEl;
}

if (!el.path) {
el.createPathProxy();
}
const path = el.path;

if (el.shapeChanged()) {
path.beginPath();
// 关键代码:buildPath();
el.buildPath(path, el.shape);
el.pathUpdated();
}

const pathVersion = path.getVersion();
const elExt = el as PathWithSVGBuildPath;
let svgPathBuilder = elExt.__svgPathBuilder;
if (
elExt.__svgPathVersion !== pathVersion ||
!svgPathBuilder ||
el.style.strokePercent < 1
) {
if (!svgPathBuilder) {
svgPathBuilder = elExt.__svgPathBuilder = new SVGPathRebuilder();
}
svgPathBuilder.reset();
path.rebuildPath(svgPathBuilder, el.style.strokePercent);
svgPathBuilder.generateStr();
elExt.__svgPathVersion = pathVersion;
}

attr(svgEl, 'd', svgPathBuilder.getStr());

bindStyle(svgEl, style, el);
setTransform(svgEl, el.transform);
},
};

设置样式

还有一个关键的方法,是 bindStyle(),这是设置样式的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
function bindStyle(svgEl: SVGElement, style: PathStyleProps, el?: Path): void;
function bindStyle(svgEl: SVGElement, style: TSpanStyleProps, el?: TSpan): void;
function bindStyle(
svgEl: SVGElement,
style: ImageStyleProps,
el?: ZRImage
): void;
function bindStyle(
svgEl: SVGElement,
style: AllStyleOption,
el?: Path | TSpan | ZRImage
) {
const opacity = style.opacity == null ? 1 : style.opacity;

// only set opacity. stroke and fill cannot be applied to svg image
if (el instanceof ZRImage) {
svgEl.style.opacity = opacity + '';
return;
}

if (pathHasFill(style)) {
let fill = style.fill;
fill = fill === 'transparent' ? NONE : fill;
attr(svgEl, 'fill', fill as string);
attr(
svgEl,
'fill-opacity',
(style.fillOpacity != null ? style.fillOpacity * opacity : opacity) + ''
);
} else {
attr(svgEl, 'fill', NONE);
}

if (pathHasStroke(style)) {
let stroke = style.stroke;
stroke = stroke === 'transparent' ? NONE : stroke;
attr(svgEl, 'stroke', stroke as string);
const strokeWidth = style.lineWidth;
const strokeScale = style.strokeNoScale ? (el as Path).getLineScale() : 1;
attr(
svgEl,
'stroke-width',
(strokeScale ? strokeWidth / strokeScale : 0) + ''
);
// stroke then fill for text; fill then stroke for others
attr(svgEl, 'paint-order', style.strokeFirst ? 'stroke' : 'fill');
attr(
svgEl,
'stroke-opacity',
(style.strokeOpacity != null ? style.strokeOpacity * opacity : opacity) +
''
);
let lineDash =
style.lineDash &&
strokeWidth > 0 &&
normalizeLineDash(style.lineDash, strokeWidth);
if (lineDash) {
let lineDashOffset = style.lineDashOffset;
if (strokeScale && strokeScale !== 1) {
lineDash = map(lineDash, function (rawVal) {
return rawVal / strokeScale;
});
if (lineDashOffset) {
lineDashOffset /= strokeScale;
lineDashOffset = mathRound(lineDashOffset);
}
}
attr(svgEl, 'stroke-dasharray', lineDash.join(','));
attr(svgEl, 'stroke-dashoffset', (lineDashOffset || 0) + '');
} else {
attr(svgEl, 'stroke-dasharray', '');
}

// PENDING
style.lineCap && attr(svgEl, 'stroke-linecap', style.lineCap);
style.lineJoin && attr(svgEl, 'stroke-linejoin', style.lineJoin);
style.miterLimit && attr(svgEl, 'stroke-miterlimit', style.miterLimit + '');
} else {
attr(svgEl, 'stroke', NONE);
}
}

一些需要注意的设计

属性名映射

在 ECharts 的 src/model/mixin/itemStyle.ts 中,对一些样式进行了别名映射的设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export const ITEM_STYLE_KEY_MAP = [
['fill', 'color'],
['stroke', 'borderColor'],
['lineWidth', 'borderWidth'],
['opacity'],
['shadowBlur'],
['shadowOffsetX'],
['shadowOffsetY'],
['shadowColor'],
['lineDash', 'borderType'],
['lineDashOffset', 'borderDashOffset'],
['lineCap', 'borderCap'],
['lineJoin', 'borderJoin'],
['miterLimit', 'borderMiterLimit'],
// Option decal is in `DecalObject` but style.decal is in `PatternObject`.
// So do not transfer decal directly.
];

所以你可以看到,有些开放的配置项,和默认的 CSS 属性名是不一样的。比如我要设置饼图的图形之间的间隙,那么就应该在 series 中这样设置:

1
2
3
4
5
6
7
itemStyle: {
borderRadius: 20,
// 最终会转换为stroke属性
borderColor: '#f00',
// 最终会转换为lineWidth属性
borderWidth: 15
},

个性化图表的样式

牢记 ECharts 的配置,是分为全局、系列、数据三个层次的。

直接样式设置

直接的样式设置 itemStyle, lineStyle, areaStyle, label, …
这些的实现原理我要深究下

间接样式设置

颜色主题、调色盘、视觉映射

高亮样式(emphasis)

交互组件

数据区域缩放(dataZoom)

dataZoom 的运行原理是通过数据过滤来达到数据窗口缩放的效果。

上下左右都能缩放。

timeline

实现原理,是在多个 option 之间切换。

核心配置是baseOptionoptions

公有的配置项,推荐配置在 baseOption 中。timeline 播放切换时,会把 options 数组中的对应的 option,与 baseOption 进行 merge 形成最终的 option。

移动端自适应

为了解决这个问题,ECharts 完善了组件的定位设置,并且实现了类似 CSS Media Query 的自适应能力。

数据的视觉映射(visulaMap)

数据可视化是 数据视觉元素 的映射过程(这个过程也可称为视觉编码,视觉元素也可称为视觉通道)。

visualMap 组件定义了把数据的『哪个维度』映射到『什么视觉元素上』。

visualMap 我之前理解错了,这是视觉映射,不是地图
可以将其看做是颜色比例尺,适合数据很多,要通过颜色区分的场景,比如热力地图

还可以通过分段视觉映射,做出个性化的分段差异显示效果。比如地图不同区域显示不同颜色、折线图不同区域显示不同个颜色等等。

视觉元素分类:

图形类别(symbol)、图形大小(symbolSize)
颜色(color)、透明度(opacity)、颜色透明度(colorAlpha)、
颜色明暗度(colorLightness)、颜色饱和度(colorSaturation)、色调(colorHue)

事件和行为

在 ECharts 中事件分为两种类型,一种是用户鼠标操作点击,或者 hover 图表的图形时触发的事件,还有一种是用户在使用可以交互的组件后触发的行为事件,例如在切换图例开关时触发的 ‘legendselectchanged’ 事件(这里需要注意切换图例开关是不会触发’legendselected’事件的),数据区域缩放时触发的 ‘datazoom’ 事件等等。

这里直接放一下官方文档,感觉我会频繁用到:[事件和行为的文档](https://www.echartsjs.com/zh/tutorial.html#ECharts 中的事件和行为)、事件的 API 文档

点击事件通过on进行注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 基于准备好的dom,初始化ECharts实例
var myChart = echarts.init(document.getElementById('main'));

// 指定图表的配置项和数据
var option = {
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'],
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20],
},
],
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
// 处理点击事件并且跳转到相应的百度搜索页面
myChart.on('click', function (params) {
window.open('https://www.baidu.com/s?wd=' + encodeURIComponent(params.name));
});

要特别注意事件传入的 params 参数,这个你会经常用到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
// 当前点击的图形元素所属的组件名称,
// 其值如 'series'、'markLine'、'markPoint'、'timeLine' 等。
componentType: string,
// 系列类型。值可能为:'line'、'bar'、'pie' 等。当 componentType 为 'series' 时有意义。
seriesType: string,
// 系列在传入的 option.series 中的 index。当 componentType 为 'series' 时有意义。
seriesIndex: number,
// 系列名称。当 componentType 为 'series' 时有意义。
seriesName: string,
// 数据名,类目名
name: string,
// 数据在传入的 data 数组中的 index
dataIndex: number,
// 传入的原始数据项
data: Object,
// sankey、graph 等图表同时含有 nodeData 和 edgeData 两种 data,
// dataType 的值会是 'node' 或者 'edge',表示当前点击在 node 还是 edge 上。
// 其他大部分图表中只有一种 data,dataType 无意义。
dataType: string,
// 传入的数据值
value: number|Array
// 数据图形的颜色。当 componentType 为 'series' 时有意义。
color: string
}

绑定事件

类似这样:

1
2
3
el.on('click', function () {
api.dispatchAction('xxxx');
});

可以在业务方使用组件的代码中使用。

注意一个可能会用到的内容:componentElementFinder???

尺寸

getBoundingRect 有误差,原因待深入研究
可以用 getPaintRect

动画

初始动画

如何禁用动画?

basicTrasition.ts中的initProps

比如我要禁用 axis 的动画,可以这样:

交互动画

以 Legend 的分页按钮动画为例,流程如下图:

animate

注意这个 Legend 的移动,是按Group来移动的,而不是去控制每一个小图例进行移动。即看到上图中传入 graphic.updateProps()的第一个参数。这个参数可以这样获取:

1
var contentGroup = this.getContentGroup();

在定义 contentGroup 的地方,可以看到,这就是一个 Group 实例:

1
this.group.add((this._contentGroup = new Group()));

series.universalTransition(全局过渡动画)

官方文档

从  v5.2.0  开始支持

全局过渡动画(Universal Transition)提供了任意系列之间进行变形动画的功能。开启该功能后,每次setOption,相同id的系列之间会自动关联进行动画的过渡,更细粒度的关联配置见universalTransition.seriesKey配置。

通过配置encode.itemGroupId或者dataGroupId等指定数据的分组,还可以实现诸如下钻,聚合等一对多或者多对一的动画。

可以直接在系列中配置  universalTransition: true  开启该功能。也可以提供一个对象进行更多属性的配置。

(精)扩展机制

(MIT)必看的文档

ECharts 的插件机制

如何扩展一个新的配置项

官方示例

百度 FE 的代码库有很多 ECharts 的插件:
https://github.com/ecomfe?q=echart&type=all&language=&sort=

比如这个水球图:
https://github.com/ecomfe/echarts-liquidfill

use() API

必看的源码

extension.ts

这是扩展的入口文件,里面引入了扩展所需的各个模块、use()这个核心 API、各种注册等,必看。

Global.ts(ecModel)

写扩展一定会用到 ecModel,如果没用到,说明扩展写得不规范;比如遍历 eachComponent 等

写扩展一定要熟读 ecModel 的属性和方法

lifecircle.ts

src/core/lifecircle.ts 里面定义了生命周期:

1
2
3
4
5
6
7
8
9
10
interface LifecycleEvents {
afterinit: [EChartsType];
'series:beforeupdate': [GlobalModel, ExtensionAPI, UpdateLifecycleParams];
'series:layoutlabels': [GlobalModel, ExtensionAPI, UpdateLifecycleParams];
'series:transition': [GlobalModel, ExtensionAPI, UpdateLifecycleParams];
'series:afterupdate': [GlobalModel, ExtensionAPI, UpdateLifecycleParams];
// 'series:beforeeachupdate': [GlobalModel, ExtensionAPI, SeriesModel]
// 'series:aftereachupdate': [GlobalModel, ExtensionAPI, SeriesModel]
afterupdate: [GlobalModel, ExtensionAPI];
}

这在扩展的时候很有用。

install 流程(产业链为例)

install

(TODO)坐标系

custom.renderItem.api.coord 配置项的实现原理:

CustomView.ts 坐标系

coordSys.dataToPoint(data)

原则

  • Model 中不应该有渲染内容,比如 fill、color 等函数配置项,应该放 view 里面处理

  • 源码内部不该调用外部 api,会死循环,比如写扩展时,update 内部别用 setOption,因为 setOption 本身会调用 update

  • 写扩展时,考虑到扩展性,一般需要拆分为 3 个模块:seriesModel、seriesView、layout

  • 不能调用下划线开头的属性,这些属性和方法本身就是设计为不开放的,使用这些内容会违反封装性

resize 机制

StandardChart 的 resize 调用方法:

1
2
3
4
5
6
chart.on('dv:afterinit', () => {
// window.addEventListener('resize', chart.getECharts().resize);
window.onresize = function () {
chart.getECharts().resize();
};
});

echarts 的 resize 调用方法:

1
window.addEventListener('resize', myChart.resize);

echarts.ts 的 resize 入口:

1
2
3
4
5
6
7
8
9
10
updateMethods.update.call(this, {
type: 'resize',
animation: extend(
{
// Disable animation
duration: 0,
},
opts && opts.animation
),
});

updateMethods :

1
2
3
4
5
6
prepareAndUpdate;
update;
updateLayout;
updateTransform;
updateView;
updateVisual;

update()->具体图表的 render()

BarView.ts:

1
2
3
4
5
6
7
const itemModel = data.getItemModel<BarDataItemOption>(newIndex);
// 这里重新计算了元素的高宽和位置,比如柱状图的单个柱子,这里layout的结果类似这样:
// height: -282
// width: 109.37485714285714
// x: 163.2697142857143
// y: 530
const layout = getLayout[coord.type](data, newIndex, itemModel);

getLayout:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const getLayout: {
[key in 'cartesian2d' | 'polar']: GetLayout;
} = {
// itemModel is only used to get borderWidth, which is not needed
// when calculating bar background layout.
cartesian2d(data, dataIndex, itemModel?): RectLayout {
const layout = data.getItemLayout(dataIndex) as RectLayout;
const fixedLineWidth = itemModel ? getLineWidth(itemModel, layout) : 0;

// fix layout with lineWidth
const signX = layout.width > 0 ? 1 : -1;
const signY = layout.height > 0 ? 1 : -1;
return {
x: layout.x + (signX * fixedLineWidth) / 2,
y: layout.y + (signY * fixedLineWidth) / 2,
width: layout.width - signX * fixedLineWidth,
height: layout.height - signY * fixedLineWidth,
};
},

polar(data, dataIndex, itemModel?): SectorLayout {
const layout = data.getItemLayout(dataIndex);
return {
cx: layout.cx,
cy: layout.cy,
r0: layout.r0,
r: layout.r,
startAngle: layout.startAngle,
endAngle: layout.endAngle,
clockwise: layout.clockwise,
} as SectorLayout;
},
};

Q&A

为什么要分为 model 和 view?

可视化 = 数据 model 驱动 图形渲染 view

单一职责(能力边界、理解成本、心智负担)、扩展性,牵一发而动全身(联动效应)、解耦

函数式编程的思想

扩展 ECharts

自己实现拖拽

借助于grapic实现,这个很有用,通过不可见的图形元素,让用户可以轻松扩展自定义功能。

注意,tooltip 也是可以自己控制的。

桑基图

Echarts 默认的桑基图,其实就是由矩形组成的,可以明显的识别到这些外接矩形。

将图表抽象为基础图元,或者现有的组件,或者现有的图形,是方案设计中很重要的一步。

而我们这次的需求,和常规桑基图不一样,可以将其拆解为:双向树图 + 富文本:

sankey

主要的工作,包括:

写布局

如果用 ZRender,一个下午就能搞定了,难在要用 echarts 的钩子实现该布局,即走 echarts 流程。

可以参考 echarts 中sankeyLayout.ts这个文件,注意下node.setLayout()这个语法糖。

图形写完后,是通过注册机制install到 ECharts 中的。

这个 install 机制可以学习下,很赞,这是 ECharts 的核心机制之一。

这个注册布局机制,可以迁移到我们的 3D 开发框架里面去。

不过我们和默认的 Sankey 图有个区别,默认的桑基图是单布局的,我们要在保留默认布局的基础上,扩展新的布局,这样就变成多布局了,多布局方案的实现,可以参考 ECharts 的另外一个多布局图表:关系图。里面有多布局注册选择机制

比如在circularLayout.ts中,就可以看到是这样通过 layout 参数,来选择布局的:

1
2
3
4
5
6
7
export default function graphCircularLayout(ecModel: GlobalModel) {
ecModel.eachSeriesByType('graph', function (seriesModel: GraphSeriesModel) {
if (seriesModel.get('layout') === 'circular') {
circularLayout(seriesModel, 'symbolSize');
}
});
}

实现多周期数据的切换动画

桑基图没做 diff(数据驱动),每次重新设置数据是直接重新绘制元素,导致动画是不连续的硬过渡。

尝试用seriesuniversalTransition(全局过渡动画)实现,结果发现会有一个额外的变动效果,不符合预期。

按理说 universalTransition 一定能解决,因为从原理上来讲,从一个形状变成另外一个形状,都是一样的去修改其几何属性。
因此怀疑是不是 universalTransition 默认是修改了元素的所有属性,导致触发了多余的改动?

尝试设置 transition 的属性,去掉 x 和 y 的过渡。不过要注意:universalTransition 默认是没有指定过渡属性的配置项的,可以在源码里渲染图形的时候,给图形设置 trasition 属性,规定图形的哪些属性参与动画。

(TODO)为什么桑基图没有做数据驱动(diff-add-update-remove)?

富文本方案

富文本的问题,在于 ECharts 是在绘制前的数据计算环节算出来每个元素的大小,进而布局绘制的,而富文本内容在绘制之前是不知道大小的,可能导致后面绘制后,和其他元素出现重叠的情况。

解决的方案就是:针对动态内容,先将其绘制到内存中,算出包围盒,再和其他元素计算碰撞检测,然后再走 ECharts 默认的绘制流程。

蜂群图

D3 各种力的仿真:
https://zhuanlan.zhihu.com/p/449796520

力导向是可以设置多个引力中心点的,因此给每个圆形设置其中心点为最终应该在的位置,这样就可以实现类似一条线的力导向动画了。

patch-package 修改源码

最近有功能需要给 ECharts 扩展一个生命周期,修改 ECharts 源码后,打包出来的 patch 文件非常大,上万行。

经排查,发现是因为package.json中的这个脚本大量改动了 echarts 文件导致的:

“postprepare”: “node scripts/enableEChartsLibsDTS.mjs”,
因此后续需要通过 patch-package 修改 ECharts 源码时,请按照如下流程操作:

1、注释掉 scripts/enableEChartsLibsDTS.mjs 的这一行:

1
await copyFiles(SOURCE_DTS_DIRECTORY, TARGET_DTS_DIRECTORY);

2、删除 node_modules/echarts 这个包

3、npm i 安装 echarts

4、修改 echarts 源码

5、执行如下命令,生成 patch 文件:

1
npx patch-package echarts

6、恢复第一步的注释

7、提交代码

新的图形

日历图

https://www.echartsjs.com/zh/tutorial.html#%E5%B0%8F%E4%BE%8B%E5%AD%90%EF%BC%9A%E5%AE%9E%E7%8E%B0%E6%97%A5%E5%8E%86%E5%9B%BE

旭日图

https://echarts.apache.org/examples/zh/editor.html?c=sunburst-book

这个用来做分类统计非常合适,比如我今年看过的书籍的分类统计。

自定义系列(custom series)

https://www.echartsjs.com/zh/tutorial.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E7%B3%BB%E5%88%97

这个其实还是有一些限制的,shape 受限于 ZRender 所支持的图形类型。

自定义的含义,应该是局部自定义,比如自己设定图形的大小、样式等等。

那如果我们确实要自定义一些非常奇怪的形状,该怎么办呢?

可以采用自定义矢量图形的方式,可以参考这个例子,以及return_groupreturn_path这两个 API 文档。

虽然我们采用了矢量图的形式进行设置,不过 ECharts 应该在作图的时候,还是通过转换器将图形信息转为了 Canvas 的绝对坐标,然后通过 Canvas 画上去的。

富文本标签

用好富文本标签,可以给图形增色不少:

https://www.echartsjs.com/zh/tutorial.html#%E5%AF%8C%E6%96%87%E6%9C%AC%E6%A0%87%E7%AD%BE

(精)在布局/绘制前计算元素的尺寸

在线 Demo

ECharts 的富文本文档

这个在布局依赖动态渲染的元素尺寸时经常用到,比如产业链关系图这个组件,里面的每个节点的位置是基于其左边和上面的节点的位置来确定的,因此我们需要在页面布局和渲染之前,先算出每个节点的高宽。这种情况下,使用富文本,计算其包围盒,就非常合适。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const rect = new echarts.graphic.Text({
style: {
text: '{a|afadsafasdfad}\n{b|sdfnjsdanvjksadnjv}',
rich: {
a: {
width: 50,
height: 20,
padding: [1, 2, 3, 4],
},
b: {
width: 50,
height: 20,
padding: [1, 2, 3, 4],
},
},
},
});

console.log('rect', rect.getBoundingRect());

程序设计

install 的设计,类比装箱,很 nice。组件就像拼拼图或者建房子、玩俄罗斯方块。

  • 先定义数据结构
  • 视图和数据处理分离
  • mixin 的设计

常用 API

可以参考王渊的文档:https://wang1212.github.io/echarts-api-docs/

获取当前实例的 Option 配置信息

比如获取 legend 的选中状态的配置,将其传递给下一个周期进行渲染:

1
2
3
4
5
6
const ecIns = chartIns.getECharts();
const legendModel = ecIns.getModel().getComponent('legend', 0);
const selected = legendModel.get('selected');

// 传递legend的选中状态
nextOption.legend.selected = selected;

使用方式

vue-echarts:
https://github.com/ecomfe/vue-echarts

根据 option 自动生成 import 信息:
https://vue-echarts.dev/#codegen

这是自动生成 import 的逻辑(手动配置依赖):
https://github.com/ecomfe/vue-echarts/blob/main/src/demo/utils/codegen.js

工具

JSON 可视化

https://jsontr.ee/

很适合用来分析和教学 ECharts 的配置。

参考资料

Code wiki for ECharts
https://codewiki.google/github.com/apache/echarts

1
This standardized data can then undergo various transformations, including filtering, aggregation, and stacking, before being prepared for visualization.

这个概念是不是和数据处理的ETL很像?

ECharts 万字入门指南
https://mp.weixin.qq.com/s/9iFcGMyiQjUZo4uiQ_yGSg

(官方资料,新人必读)ECharts:快速构建基于 Web 的可视化的声明性框架
https://www.sciencedirect.com/science/article/pii/S2468502X18300068

该论文的读书笔记:

https://zhuanlan.zhihu.com/p/347325932

API 可视化速查

https://www.echartsjs.com/zh/cheat-sheet.html

特性文档

https://www.echartsjs.com/zh/feature.html
可以按照 Echarts 的特性页面的内容,去分块深入学习(这些特性就是 Echarts 的竞争力所在)
这些都是基石,掌握了就能创造出各种效果

(精)数据集与数据转换

数据集:

(https://echarts.apache.org/handbook/zh/concepts/dataset)

(https://vega.github.io/vega-lite/docs/encoding.html)

(https://vega.github.io/vega/docs/transforms/bin/)

数据转换:

(https://echarts.apache.org/handbook/zh/concepts/data-transform)

(https://vega.github.io/vega/docs/transforms/)

(https://vega.github.io/vega-lite/docs/transform.html)

按需打包

https://www.echartsjs.com/zh/builder.html

可以按需引入的模块列表:
https://github.com/apache/incubator-echarts/blob/master/index.js

主题编辑器

https://echarts.baidu.com/theme-builder/
这就是我们要的,给出几套设计主题,规范化

(精)通过 Flappy Bird 学习 ECharts

https://juejin.cn/post/7034290086111871007

Echarts6 规划

ECharts 6.0 · GitHub