ECharts 自定义系列(custom-series)完全指南
文档类型: 深度技术文档
难度等级: ⭐⭐⭐⭐⭐
源码版本: ECharts 5.x
本文行数: 约680行
📋 目录
🎯 custom系列基础
什么是custom系列?
custom系列是ECharts提供的自定义渲染接口,允许开发者使用Canvas API绘制任意图形,突破内置图表类型的限制。
typescript
const option = {
series: [{
type: 'custom',
renderItem: (params: any, api: any) => {
// 自定义渲染逻辑
return {
type: 'rect',
shape: {
x: 0,
y: 0,
width: 100,
height: 50
},
style: {
fill: '#5470C6'
}
};
},
data: [10, 20, 30]
}]
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
应用场景:
- 甘特图
- 盒须图(Boxplot)
- 蜡烛图
- 自定义标注
- 复杂组合图形
🔧 renderItem函数详解
renderItem参数说明
typescript
type RenderItemFunction = (
params: RenderItemParams,
api: RenderItemAPI
) => ECElement | ECElement[] | false | void;
interface RenderItemParams {
dataIndex: number; // 数据项索引
dataType: string; // 数据类型
seriesId: string; // 系列ID
seriesName: string; // 系列名称
start: number; // 动画起始值
end: number; // 动画结束值
}
interface RenderItemAPI {
// 坐标转换
coord(layout: string, value: any): number[];
// 尺寸计算
size(valueSize: any[], dataSize?: number[]): number[];
// 样式获取
style(opt?: ItemStyleOption): PathStyleProps;
// 高亮样式
highlightStyle(opt?: ItemStyleOption): PathStyleProps;
// 条形图辅助
barLayout(params: BarLayoutParams): BarLayoutResult;
// 其他工具
getWidth(): number;
getHeight(): number;
getZr(): ZRenderType;
}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
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
基础示例: 自定义柱状图
typescript
const option = {
xAxis: {
type: 'category',
data: ['A', 'B', 'C', 'D']
},
yAxis: {
type: 'value'
},
series: [{
type: 'custom',
renderItem: (params: any, api: any) => {
const categoryIndex = api.value(0);
const value = api.value(1);
// 获取坐标位置
const startPoint = api.coord([categoryIndex, 0]);
const endPoint = api.coord([categoryIndex, value]);
// 计算柱子宽度
const width = api.size([1, 0])[0] * 0.6;
// 构建矩形
return {
type: 'rect',
shape: {
x: endPoint[0] - width / 2,
y: endPoint[1],
width: width,
height: startPoint[1] - endPoint[1]
},
style: api.style()
};
},
data: [
[0, 100],
[1, 200],
[2, 150],
[3, 250]
]
}]
};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
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
🎨 高级自定义图形
1. 圆形标记
typescript
const circleOption = {
series: [{
type: 'custom',
renderItem: (params: any, api: any) => {
const point = api.coord([api.value(0), api.value(1)]);
const size = api.size([1, 1]);
const radius = Math.min(size[0], size[1]) * 0.3;
return {
type: 'circle',
shape: {
cx: point[0],
cy: point[1],
r: radius
},
style: {
fill: '#5470C6',
stroke: '#fff',
lineWidth: 2
}
};
},
data: [[0, 50], [1, 80], [2, 60]]
}]
};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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2. 多边形
typescript
const polygonOption = {
series: [{
type: 'custom',
renderItem: (params: any, api: any) => {
const center = api.coord([api.value(0), api.value(1)]);
const size = api.size([1, 1]);
const radius = Math.min(size[0], size[1]) * 0.4;
// 创建六边形顶点
const points: number[][] = [];
for (let i = 0; i < 6; i++) {
const angle = (Math.PI / 3) * i;
points.push([
center[0] + radius * Math.cos(angle),
center[1] + radius * Math.sin(angle)
]);
}
return {
type: 'polygon',
shape: { points },
style: {
fill: '#91CC75',
stroke: '#fff',
lineWidth: 2
}
};
},
data: [[0, 50], [1, 80]]
}]
};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
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
3. 文本标签
typescript
const textOption = {
series: [{
type: 'custom',
renderItem: (params: any, api: any) => {
const point = api.coord([api.value(0), api.value(1)]);
return {
type: 'group',
children: [
{
type: 'circle',
shape: {
cx: point[0],
cy: point[1],
r: 20
},
style: { fill: '#FAC858' }
},
{
type: 'text',
style: {
text: api.value(1).toString(),
x: point[0],
y: point[1],
textAlign: 'center',
textVerticalAlign: 'middle',
fill: '#333',
fontSize: 12
}
}
]
};
},
data: [[0, 100], [1, 200]]
}]
};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
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
4. 组合图形
typescript
const groupOption = {
series: [{
type: 'custom',
renderItem: (params: any, api: any) => {
const point = api.coord([api.value(0), api.value(1)]);
return {
type: 'group',
children: [
// 外圆
{
type: 'circle',
shape: {
cx: point[0],
cy: point[1],
r: 25
},
style: {
fill: 'rgba(84, 112, 198, 0.3)',
stroke: '#5470C6',
lineWidth: 2
}
},
// 内圆
{
type: 'circle',
shape: {
cx: point[0],
cy: point[1],
r: 15
},
style: {
fill: '#5470C6'
}
},
// 十字线
{
type: 'line',
shape: {
x1: point[0] - 30,
y1: point[1],
x2: point[0] + 30,
y2: point[1]
},
style: {
stroke: '#5470C6',
lineWidth: 1
}
},
{
type: 'line',
shape: {
x1: point[0],
y1: point[1] - 30,
x2: point[0],
y2: point[1] + 30
},
style: {
stroke: '#5470C6',
lineWidth: 1
}
}
]
};
},
data: [[0, 50], [1, 80], [2, 60]]
}]
};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
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
🚀 性能优化
大数据量优化
typescript
const optimizedOption = {
series: [{
type: 'custom',
data: largeDataset, // 10000+ 数据点
// 渐进式渲染
progressive: 1000,
progressiveThreshold: 5000,
// 简化图形
renderItem: (params: any, api: any) => {
// 使用简单图形而非复杂组合
return {
type: 'rect', // 避免使用group嵌套
shape: { /* ... */ },
style: api.style()
};
}
}]
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
缓存复用
typescript
class ShapeCache {
private cache = new Map<string, any>();
getOrCreate(key: string, factory: () => any): any {
if (!this.cache.has(key)) {
this.cache.set(key, factory());
}
return this.cache.get(key);
}
clear() {
this.cache.clear();
}
}
const shapeCache = new ShapeCache();
// 在renderItem中使用
renderItem: (params: any, api: any) => {
const key = `shape_${params.dataIndex}`;
return shapeCache.getOrCreate(key, () => {
// 只在首次创建时执行
return {
type: 'rect',
shape: { /* ... */ },
style: api.style()
};
});
}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
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
💻 实战案例
案例1: 甘特图
typescript
import * as echarts from 'echarts';
class GanttChart {
private chart: echarts.ECharts;
constructor(container: HTMLElement) {
this.chart = echarts.init(container);
this.render();
}
private render() {
const tasks = [
{ name: '需求分析', start: 1, end: 5, progress: 100 },
{ name: '系统设计', start: 4, end: 10, progress: 80 },
{ name: '前端开发', start: 8, end: 20, progress: 50 },
{ name: '后端开发', start: 8, end: 22, progress: 45 },
{ name: '测试阶段', start: 20, end: 28, progress: 0 }
];
const option = {
title: { text: '项目进度甘特图' },
tooltip: {
formatter: (params: any) => {
const task = tasks[params.dataIndex];
return `
<strong>${task.name}</strong><br/>
第${task.start}天 - 第${task.end}天<br/>
进度: ${task.progress}%
`;
}
},
grid: {
left: '15%',
right: '10%',
top: '15%',
bottom: '10%'
},
xAxis: {
type: 'value',
min: 0,
max: 30,
name: '天数'
},
yAxis: {
type: 'category',
data: tasks.map(t => t.name),
inverse: true
},
series: [{
type: 'custom',
renderItem: (params: any, api: any) => {
const task = tasks[params.dataIndex];
const start = api.coord([task.start, params.dataIndex]);
const end = api.coord([task.end, params.dataIndex]);
const height = api.size([0, 1])[1] * 0.6;
// 进度条宽度
const progressWidth = (end[0] - start[0]) * (task.progress / 100);
return {
type: 'group',
children: [
// 背景条
{
type: 'rect',
shape: {
x: start[0],
y: start[1] - height / 2,
width: end[0] - start[0],
height: height
},
style: { fill: '#e0e0e0' }
},
// 进度条
{
type: 'rect',
shape: {
x: start[0],
y: start[1] - height / 2,
width: progressWidth,
height: height
},
style: {
fill: task.progress === 100 ? '#91CC75' :
task.progress > 0 ? '#FAC858' : '#5470C6'
}
}
]
};
},
data: tasks.map((_, index) => index)
}]
};
this.chart.setOption(option);
}
resize() {
this.chart.resize();
}
dispose() {
this.chart.dispose();
}
}
// 使用
const gantt = new GanttChart(document.getElementById('chart')!);
window.addEventListener('resize', () => gantt.resize());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
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
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
案例2: 箱线图(Boxplot)
typescript
const boxplotOption = {
xAxis: {
type: 'category',
data: ['实验组A', '实验组B', '对照组']
},
yAxis: {
type: 'value'
},
series: [{
type: 'custom',
renderItem: (params: any, api: any) => {
const data = api.value(1); // [min, Q1, median, Q3, max]
const index = api.value(0);
const bandWidth = api.size([1, 0])[0];
const boxWidth = bandWidth * 0.6;
const minPoint = api.coord([index, data[0]]);
const q1Point = api.coord([index, data[1]]);
const medianPoint = api.coord([index, data[2]]);
const q3Point = api.coord([index, data[3]]);
const maxPoint = api.coord([index, data[4]]);
return {
type: 'group',
children: [
// 箱体
{
type: 'rect',
shape: {
x: q1Point[0] - boxWidth / 2,
y: q3Point[1],
width: boxWidth,
height: q1Point[1] - q3Point[1]
},
style: {
fill: '#5470C6',
stroke: '#333',
lineWidth: 2
}
},
// 中位数线
{
type: 'line',
shape: {
x1: q1Point[0] - boxWidth / 2,
y1: medianPoint[1],
x2: q1Point[0] + boxWidth / 2,
y2: medianPoint[1]
},
style: {
stroke: '#fff',
lineWidth: 2
}
},
// 上须
{
type: 'line',
shape: {
x1: medianPoint[0],
y1: q3Point[1],
x2: medianPoint[0],
y2: maxPoint[1]
},
style: { stroke: '#333', lineWidth: 2 }
},
// 下须
{
type: 'line',
shape: {
x1: medianPoint[0],
y1: q1Point[1],
x2: medianPoint[0],
y2: minPoint[1]
},
style: { stroke: '#333', lineWidth: 2 }
}
]
};
},
data: [
[0, [10, 20, 30, 40, 50]],
[1, [15, 25, 35, 45, 55]],
[2, [12, 22, 32, 42, 52]]
]
}]
};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
82
83
84
85
86
87
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
🎯 最佳实践总结
✅ DO - 推荐做法
使用api.style()获取样式
typescriptstyle: api.style() // 自动继承主题样式1简化图形结构
typescript// ✅ 好 - 简单图形 return { type: 'rect', ... }; // ❌ 避免 - 过深嵌套 return { type: 'group', children: [...] };1
2
3
4
5启用渐进式渲染
typescriptprogressive: 10001
❌ DON'T - 避免做法
- 避免在renderItem中进行复杂计算typescript
// ❌ 不好 renderItem: () => { const result = heavyCalculation(); // 每次渲染都执行 return { /* ... */ }; } // ✅ 好 - 预计算 const precalculated = heavyCalculation(); renderItem: () => { return { /* 使用precalculated */ }; }1
2
3
4
5
6
7
8
9
10
11
🔗 相关资源
下一篇: graphic组件
