自定义系列
# 自定义系列
自定义系列(custom series),是一种系列的类型。它把绘制图形元素这一步留给开发者去做,从而开发者能在坐标系中自由绘制出自己需要的图表。
Apache EChartsTM 为什么会要支持 自定义系列
呢?
ECharts 内置支持的图表类型是最常见的图表类型,但是图表类型是难于穷举的,有很多小众的需求 echarts 并不能内置的支持。那么就需要提供一种方式来让开发者自己扩展。另一方面,所提供的扩展方式要尽可能得简单,例如图形元素创建和释放、过渡动画、tooltip、数据区域缩放(dataZoom)、视觉映射(visualMap)等功能,尽量在 ECharts 中内置得处理,使开发者不必纠结于这些细节。综上考虑形成了 自定义系列(custom series)。
例如,下面的例子使用 custom series 扩展出了 x-range 图:

图表来源:https://echarts.apache.org/examples/zh/editor.html?c=custom-profile (opens new window)
FastWeb TUgURLFrame示例:
如需在FastWeb中的TUgURLFrame中实现上述效果,则修改其HTML
属性,使用下列内容:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>ECharts</title>
<!-- 引入 echarts.js -->
<script src="https://cdn.staticfile.org/echarts/5.1.2/echarts.min.js"></script>
</head>
<body>
<!-- 为ECharts准备一个具备大小(宽高)的Dom -->
<div id="main" style="width: 800px;height:500px;"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'));
var option;
var data = [];
var dataCount = 10;
var startTime = +new Date();
var categories = ['categoryA', 'categoryB', 'categoryC'];
var types = [
{name: 'JS Heap', color: '#7b9ce1'},
{name: 'Documents', color: '#bd6d6c'},
{name: 'Nodes', color: '#75d874'},
{name: 'Listeners', color: '#e0bc78'},
{name: 'GPU Memory', color: '#dc77dc'},
{name: 'GPU', color: '#72b362'}
];
// Generate mock data
categories.forEach(function (category, index) {
var baseTime = startTime;
for (var i = 0; i < dataCount; i++) {
var typeItem = types[Math.round(Math.random() * (types.length - 1))];
var duration = Math.round(Math.random() * 10000);
data.push({
name: typeItem.name,
value: [
index,
baseTime,
baseTime += duration,
duration
],
itemStyle: {
normal: {
color: typeItem.color
}
}
});
baseTime += Math.round(Math.random() * 2000);
}
});
function renderItem(params, api) {
var categoryIndex = api.value(0);
var start = api.coord([api.value(1), categoryIndex]);
var end = api.coord([api.value(2), categoryIndex]);
var height = api.size([0, 1])[1] * 0.6;
var rectShape = echarts.graphic.clipRectByRect({
x: start[0],
y: start[1] - height / 2,
width: end[0] - start[0],
height: height
}, {
x: params.coordSys.x,
y: params.coordSys.y,
width: params.coordSys.width,
height: params.coordSys.height
});
return rectShape && {
type: 'rect',
transition: ['shape'],
shape: rectShape,
style: api.style()
};
}
option = {
tooltip: {
formatter: function (params) {
return params.marker + params.name + ': ' + params.value[3] + ' ms';
}
},
title: {
text: 'Profile',
left: 'center'
},
dataZoom: [{
type: 'slider',
filterMode: 'weakFilter',
showDataShadow: false,
top: 400,
labelFormatter: ''
}, {
type: 'inside',
filterMode: 'weakFilter'
}],
grid: {
height: 300
},
xAxis: {
min: startTime,
scale: true,
axisLabel: {
formatter: function (val) {
return Math.max(0, val - startTime) + ' ms';
}
}
},
yAxis: {
data: categories
},
series: [{
type: 'custom',
renderItem: renderItem,
itemStyle: {
opacity: 0.8
},
encode: {
x: [1, 2],
y: 0
},
data: data
}]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
</script>
</body>
</html>
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
更多的例子参见:自定义示例 (opens new window)
下面来介绍开发者怎么使用 自定义系列(custom series)。
# 1. renderItem 方法
开发者自定义的图形元素渲染逻辑,是通过书写 renderItem
函数实现的,例如:
var option = {
...,
series: [{
type: 'custom',
renderItem: function (params, api) {
// ...
},
data: data
}]
}
2
3
4
5
6
7
8
9
10
在渲染阶段,对于 series.data 中的每个数据项(为方便描述,这里称为 dataItem
),会调用此 renderItem 函数。这个 renderItem
函数的职责,就是返回一个(或者一组)图形元素定义
,图形元素定义
中包括图形元素的类型、位置、尺寸、样式等。echarts 会根据这些 图形元素定义
来渲染出图形元素。如下的示意:
var option = {
...,
series: [{
type: 'custom',
renderItem: function (params, api) {
// 对于 data 中的每个 dataItem,都会调用这个 renderItem 函数。
// (但是注意,并不一定是按照 data 的顺序调用)
// 这里进行一些处理,例如,坐标转换。
// 这里使用 api.value(0) 取出当前 dataItem 中第一个维度的数值。
var categoryIndex = api.value(0);
// 这里使用 api.coord(...) 将数值在当前坐标系中转换成为屏幕上的点的像素值。
var startPoint = api.coord([api.value(1), categoryIndex]);
var endPoint = api.coord([api.value(2), categoryIndex]);
// 这里使用 api.size(...) 获得 Y 轴上数值范围为 1 的一段所对应的像素长度。
var height = api.size([0, 1])[1] * 0.6;
// shape 属性描述了这个矩形的像素位置和大小。
// 其中特殊得用到了 echarts.graphic.clipRectByRect,意思是,
// 如果矩形超出了当前坐标系的包围盒,则剪裁这个矩形。
// 如果矩形完全被剪掉,会返回 undefined.
var rectShape = echarts.graphic.clipRectByRect({
// 矩形的位置和大小。
x: startPoint[0],
y: startPoint[1] - height / 2,
width: endPoint[0] - startPoint[0],
height: height
}, {
// 当前坐标系的包围盒。
x: params.coordSys.x,
y: params.coordSys.y,
width: params.coordSys.width,
height: params.coordSys.height
});
// 这里返回为这个 dataItem 构建的图形元素定义。
return rectShape && {
// 表示这个图形元素是矩形。还可以是 'circle', 'sector', 'polygon' 等等。
type: 'rect',
shape: rectShape,
// 用 api.style(...) 得到默认的样式设置。这个样式设置包含了
// option 中 itemStyle 的配置和视觉映射得到的颜色。
style: api.style()
};
},
data: [
[12, 44, 55, 60], // 这是第一个 dataItem
[53, 31, 21, 56], // 这是第二个 dataItem
[71, 33, 10, 20], // 这是第三个 dataItem
...
]
}]
}
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
renderItem 函数提供了两个参数:
- params:包含了当前数据信息(如
seriesIndex
、dataIndex
等等)和坐标系的信息(如坐标系包围盒的位置和尺寸)。 - api:是一些开发者可调用的方法集合(如
api.value()
、api.coord()
)。
renderItem 函数须返回根据此 dataItem
绘制出的图形元素的定义信息,参见 renderItem.return。
一般来说,renderItem 函数的主要逻辑,是将 dataItem
里的值映射到坐标系上的图形元素。这一般需要用到 renderItem.arguments.api 中的两个函数:
- api.value(...),意思是取出
dataItem
中的数值。例如api.value(0)
表示取出当前dataItem
中第一个维度的数值。 - api.coord(...),意思是进行坐标转换计算。例如
var point = api.coord([api.value(0), api.value(1)])
表示dataItem
中的数值转换成坐标系上的点。
有时候还需要用到 api.size(...) 函数,表示得到坐标系上一段数值范围对应的长度。
返回值中样式的设置可以使用 api.style(...) 函数,他能得到 series.itemStyle 中定义的样式信息,以及视觉映射的样式信息。也可以用这种方式覆盖这些样式信息:api.style({fill: 'green', stroke: 'yellow'})
。
书写完 renderItem
方法后,自定义系列的 90% 工作就做完了。剩下的是一些精化工作。
# 2. 使坐标轴的范围自适应数据范围
在 直角坐标系(grid)、极坐标系(polar) 中都有坐标轴。坐标轴的刻度范围需要自适应当前显示出的数据的范围,否则绘制出的图形会超出去。所以,例如,在 直角坐标系(grid) 中,使用自定义系列的开发者,需要设定,data
中的哪些维度会对应到 x
轴上,哪些维度会对应到 y
轴上。这件事通过 encode 来设定。例如:
option = {
series: [{
type: 'custom',
renderItem: function () {
...
},
encode: {
// data 中『维度1』和『维度2』对应到 X 轴
x: [1, 2],
// data 中『维度0』对应到 Y 轴
y: 0
},
data: [
// 维度0 维度1 维度2 维度3
[ 12, 44, 55, 60 ], // 这是第一个 dataItem
[ 53, 31, 21, 56 ], // 这是第二个 dataItem
[ 71, 33, 10, 20 ], // 这是第三个 dataItem
...
]
}]
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3. 设定 tooltip
当然,使用 tooltip.formatter 可以任意定制 tooltip 中的内容。但是还有更简单的方法,通过encode 和 dimensions 来设定:
option = {
series: [{
type: 'custom',
renderItem: function () {
...
},
encode: {
x: [1, 2],
y: 0,
// 表示『维度2』和『维度3』要显示到 tooltip 中。
tooltip: [2, 3]
},
// 表示给『维度2』和『维度3』分别取名为『年龄』和『满意度』,显示到 tooltip 中。
dimensions: [null, null, '年龄', '满意度'],
data: [
// 维度0 维度1 维度2 维度3
[ 12, 44, 55, 60 ], // 这是第一个 dataItem
[ 53, 31, 21, 56 ], // 这是第二个 dataItem
[ 71, 33, 10, 20 ], // 这是第三个 dataItem
...
]
}]
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
上面,一个简单的 custome series 例子完成了。
下面介绍几个其他细节要点。
# 4. 超出坐标系范围的截取
与 dataZoom 结合使用的时候,常常使用会设置 dataZoom.filterMode 为 'weakFilter'。这个设置的意思是:当 dataItem
部分超出坐标系边界的时候,dataItem
不会整体被过滤掉。例如:
option = {
dataZoom: {
xAxisIndex: 0,
filterMode: 'weakFilter'
},
series: [{
type: 'custom',
renderItem: function () {
...
},
encode: {
// data 中『维度1』和『维度2』对应到 X 轴
x: [1, 2],
y: 0
},
data: [
// 维度0 维度1 维度2 维度3
[ 12, 44, 55, 60 ], // 这是第一个 dataItem
[ 53, 31, 21, 56 ], // 这是第二个 dataItem
[ 71, 33, 10, 20 ], // 这是第三个 dataItem
...
]
}]
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
在这个例子中,『维度1』和『维度2』对应到 X 轴,dataZoom
组件控制 X 轴的缩放。假如在缩放的过程中,某个 dataItem 的『维度1』超出了 X 轴的范围,『维度2』还在 X 轴的范围中,那么只要设置 dataZoom.filterMode = 'weakFilter'
,这个 dataItem 就不会被过滤掉,从而还能够使用 renderItem
绘制图形(可以使用上面提到过的 echarts.graphic.clipRectByRect
把图形绘制成被坐标系剪裁过的样子)。参见上面提到过的例子:Profile
# 5. 关于 dataIndex
开发者如果使用到的话应注意,renderItem.arguments.params 中的 dataIndex
和 dataIndexInside
是有区别的:
dataIndex
指的dataItem
在原始数据中的 index。dataIndexInside
指的是dataItem
在当前数据窗口(参见 dataZoom)中的 index。
renderItem.arguments.api 中使用的参数都是 dataIndexInside
而非 dataIndex
,因为从 dataIndex
转换成 dataIndexInside
需要时间开销。
# 6. 事件监听
chart.setOption({
// ...
series: {
type: 'custom',
renderItem: function () {
// ...
return {
type: 'group',
children: [{
type: 'circle'
// ...
}, {
type: 'circle',
name: 'aaa',
// 用户指定的信息,可以在 event handler 访问到。
info: 12345,
// ...
}]
};
}
}
});
chart.on('click', {element: 'aaa'}, function (params) {
// 当 name 为 'aaa' 的图形元素被点击时,此回调被触发。
console.log(params.info);
});
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
# 7. 自定义矢量图形
自定义系列能支持使用 SVG PathData (opens new window) 定义矢量路径。从而可以使用矢量图工具中做出的图形。参见:path,以及例子:icons 和 shapes。
更多的自定义系列的例子参见:自定义示例 (opens new window)