ECharts 插件系统详解
📋 概述
ECharts提供了强大的插件系统,允许开发者扩展图表类型、自定义组件、增强交互等。通过插件系统,可以:
- ✅ 扩展图表类型:创建全新的可视化形式
- ✅ 自定义组件:添加业务特定的UI组件
- ✅ 增强交互:实现特殊的用户交互方式
- ✅ 数据处理:自定义数据转换和计算
- ✅ 主题定制:动态切换视觉风格
🎯 插件架构
核心接口
typescript
// ECharts插件基础接口
interface EChartsExtension {
/**
* 插件名称(唯一标识)
*/
name: string;
/**
* 插件版本
*/
version?: string;
/**
* 依赖的其他插件
*/
dependencies?: string[];
/**
* 初始化函数
*/
init(echarts: any): void;
/**
* 销毁函数
*/
dispose(echarts: any): void;
/**
* 优先级(决定执行顺序)
*/
priority?: number;
}
// 图表类型插件
interface ChartTypePlugin extends EChartsExtension {
type: 'chart';
/**
* 图表类型名称
*/
chartType: string;
/**
* 渲染器
*/
render(
seriesModel: any,
ecModel: any,
api: any
): void;
/**
* 默认配置
*/
defaultOption: any;
}
// 组件插件
interface ComponentPlugin extends EChartsExtension {
type: 'component';
/**
* 组件类型
*/
componentType: string;
/**
* 布局方法
*/
layout?(ecModel: any, api: any): void;
/**
* 渲染方法
*/
render(componentModel: any, ecModel: any, api: any): void;
}
// 功能插件
interface FeaturePlugin extends EChartsExtension {
type: 'feature';
/**
* 功能名称
*/
featureName: string;
/**
* 处理方法
*/
handler(params: any): void;
}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
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
🔧 开发实战
1. 自定义图表类型插件
瀑布图插件
typescript
// plugins/WaterfallChart.ts
import * as echarts from 'echarts/core';
import { ChartView } from 'echarts/chart';
class WaterfallChart implements ChartView {
static type = 'chart.waterfall';
static dependencies = ['grid'];
readonly type = WaterfallChart.type;
/**
* 渲染瀑布图
*/
render(seriesModel: any, ecModel: any, api: any) {
const group = this.group;
group.removeAll();
const data = seriesModel.getData();
const coordSys = seriesModel.coordinateSystem;
// 获取数据
const values: number[] = [];
const categories: string[] = [];
data.each('value', (value: number, idx: number) => {
values.push(value);
categories.push(data.getName(idx));
});
// 计算累积值和辅助线
let cumulative = 0;
const bars: Array<{ x: number; y: number; height: number; isPositive: boolean }> = [];
values.forEach((value, index) => {
const point = coordSys.dataToPoint([index, cumulative]);
const nextPoint = coordSys.dataToPoint([index, cumulative + value]);
bars.push({
x: point[0],
y: Math.min(point[1], nextPoint[1]),
height: Math.abs(nextPoint[1] - point[1]),
isPositive: value >= 0
});
cumulative += value;
});
// 绘制柱状条
bars.forEach((bar, index) => {
const rect = new echarts.graphic.Rect({
shape: {
x: bar.x - 20,
y: bar.y,
width: 40,
height: bar.height
},
style: {
fill: bar.isPositive ? '#52c41a' : '#ff4d4f',
stroke: '#fff',
lineWidth: 1
},
tooltip: {
formatter: () => {
return `${categories[index]}: ${values[index]}`;
}
}
});
group.add(rect);
});
// 绘制辅助线(连接线)
for (let i = 0; i < bars.length - 1; i++) {
const line = new echarts.graphic.Line({
shape: {
x1: bars[i].x + 20,
y1: bars[i].y + (bars[i].isPositive ? 0 : bars[i].height),
x2: bars[i + 1].x - 20,
y2: bars[i].y + (bars[i].isPositive ? 0 : bars[i].height)
},
style: {
stroke: '#999',
lineWidth: 1,
lineDash: [4, 4]
}
});
group.add(line);
}
}
/**
* 销毁时清理
*/
dispose() {
this.group.removeAll();
}
private group = new echarts.graphic.Group();
}
// 注册插件
echarts.registerChart(WaterfallChart.type, WaterfallChart);
// 导出
export default WaterfallChart;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
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
使用示例
typescript
import * as echarts from 'echarts/core';
import WaterfallChart from './plugins/WaterfallChart';
// 注册插件
echarts.use([WaterfallChart]);
// 使用瀑布图
const option = {
series: [{
type: 'waterfall',
data: [
{ name: '初始值', value: 100 },
{ name: '收入A', value: 50 },
{ name: '收入B', value: 30 },
{ name: '支出C', value: -40 },
{ name: '支出D', value: -20 },
{ name: '最终值', value: 120 }
]
}]
};
chart.setOption(option);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2. 自定义组件插件
数据表格组件
typescript
// plugins/DataTableComponent.ts
import * as echarts from 'echarts/core';
import { ComponentView } from 'echarts/component';
class DataTableComponent implements ComponentView {
static type = 'component.dataTable';
readonly type = DataTableComponent.type;
/**
* 渲染数据表格
*/
render(componentModel: any, ecModel: any, api: any) {
const group = this.group;
group.removeAll();
const option = componentModel.option;
const { data, columns, position } = option;
// 创建表格容器
const container = document.createElement('div');
container.className = 'echarts-data-table';
container.style.cssText = `
position: absolute;
${this.getPositionStyle(position)};
background: rgba(255, 255, 255, 0.9);
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
font-size: 12px;
max-height: 300px;
overflow: auto;
`;
// 创建表格
const table = document.createElement('table');
table.style.cssText = 'border-collapse: collapse; width: 100%;';
// 表头
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
columns.forEach((col: string) => {
const th = document.createElement('th');
th.textContent = col;
th.style.cssText = `
padding: 8px;
border-bottom: 2px solid #ddd;
text-align: left;
font-weight: bold;
`;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// 表体
const tbody = document.createElement('tbody');
data.forEach((row: any) => {
const tr = document.createElement('tr');
columns.forEach((col: string) => {
const td = document.createElement('td');
td.textContent = row[col];
td.style.cssText = 'padding: 6px 8px; border-bottom: 1px solid #eee;';
tr.appendChild(td);
});
tbody.appendChild(tr);
});
table.appendChild(tbody);
container.appendChild(table);
// 添加到DOM
const zr = api.getZr();
zr.painter.getViewportRoot().appendChild(container);
// 保存引用以便清理
(this as any)._container = container;
}
/**
* 获取位置样式
*/
private getPositionStyle(position: string): string {
const positions: Record<string, string> = {
'top-right': 'top: 10px; right: 10px;',
'top-left': 'top: 10px; left: 10px;',
'bottom-right': 'bottom: 10px; right: 10px;',
'bottom-left': 'bottom: 10px; left: 10px;'
};
return positions[position] || positions['top-right'];
}
/**
* 销毁时清理
*/
dispose() {
if ((this as any)._container) {
(this as any)._container.remove();
}
this.group.removeAll();
}
private group = new echarts.graphic.Group();
}
// 注册组件
echarts.registerComponent(DataTableComponent.type, DataTableComponent);
export default DataTableComponent;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
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
使用示例
typescript
import DataTableComponent from './plugins/DataTableComponent';
echarts.use([DataTableComponent]);
const option = {
dataTable: {
show: true,
position: 'top-right',
columns: ['月份', '销售额', '利润'],
data: [
{ '月份': '1月', '销售额': '¥100,000', '利润': '¥20,000' },
{ '月份': '2月', '销售额': '¥120,000', '利润': '¥25,000' },
{ '月份': '3月', '销售额': '¥150,000', '利润': '¥30,000' }
]
},
// ... 其他图表配置
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
3. 功能增强插件
数据标注插件
typescript
// plugins/DataAnnotation.ts
import * as echarts from 'echarts/core';
interface AnnotationConfig {
enable: boolean;
mode: 'click' | 'dblclick' | 'rightclick';
style?: {
color?: string;
fontSize?: number;
backgroundColor?: string;
};
onSave?: (annotation: Annotation) => void;
}
interface Annotation {
id: string;
chartId: string;
position: { x: number; y: number };
content: string;
timestamp: number;
author?: string;
}
class DataAnnotation {
static type = 'feature.dataAnnotation';
private annotations: Map<string, Annotation[]> = new Map();
private config: AnnotationConfig;
private chart: echarts.ECharts | null = null;
constructor(config: AnnotationConfig) {
this.config = {
enable: true,
mode: 'dblclick',
style: {
color: '#333',
fontSize: 12,
backgroundColor: 'rgba(255, 255, 200, 0.9)'
},
...config
};
}
/**
* 初始化插件
*/
init(chart: echarts.ECharts) {
this.chart = chart;
if (!this.config.enable) return;
const eventMap: Record<string, string> = {
'click': 'click',
'dblclick': 'dblclick',
'rightclick': 'contextmenu'
};
const eventName = eventMap[this.config.mode];
chart.on(eventName, (params: any) => {
if (params.componentType === 'series') {
this.createAnnotation(params);
}
});
}
/**
* 创建标注
*/
private createAnnotation(params: any) {
const annotation: Annotation = {
id: `anno_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
chartId: this.chart?.id || 'unknown',
position: {
x: params.event.offsetX,
y: params.event.offsetY
},
content: '',
timestamp: Date.now()
};
// 弹出输入框
const content = prompt('请输入标注内容:');
if (content) {
annotation.content = content;
// 保存标注
this.saveAnnotation(annotation);
// 显示标注
this.displayAnnotation(annotation);
// 调用回调
this.config.onSave?.(annotation);
}
}
/**
* 保存标注
*/
private saveAnnotation(annotation: Annotation) {
const chartAnnotations = this.annotations.get(annotation.chartId) || [];
chartAnnotations.push(annotation);
this.annotations.set(annotation.chartId, chartAnnotations);
// 持久化到localStorage
localStorage.setItem(
`annotations_${annotation.chartId}`,
JSON.stringify(chartAnnotations)
);
}
/**
* 显示标注
*/
private displayAnnotation(annotation: Annotation) {
if (!this.chart) return;
const marker = document.createElement('div');
marker.className = 'echarts-annotation-marker';
marker.style.cssText = `
position: absolute;
left: ${annotation.position.x}px;
top: ${annotation.position.y}px;
background: ${this.config.style?.backgroundColor};
color: ${this.config.style?.color};
padding: 4px 8px;
border-radius: 4px;
font-size: ${this.config.style?.fontSize}px;
cursor: pointer;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
`;
marker.textContent = annotation.content;
// 点击删除
marker.onclick = () => {
if (confirm('删除此标注?')) {
this.deleteAnnotation(annotation.id);
marker.remove();
}
};
const zr = this.chart.getZr();
zr.painter.getViewportRoot().appendChild(marker);
}
/**
* 删除标注
*/
private deleteAnnotation(annotationId: string) {
this.annotations.forEach((annos, chartId) => {
const filtered = annos.filter(a => a.id !== annotationId);
this.annotations.set(chartId, filtered);
});
// 更新localStorage
this.annotations.forEach((annos, chartId) => {
localStorage.setItem(
`annotations_${chartId}`,
JSON.stringify(annos)
);
});
}
/**
* 加载标注
*/
loadAnnotations(chartId: string): Annotation[] {
const stored = localStorage.getItem(`annotations_${chartId}`);
if (stored) {
const annotations = JSON.parse(stored);
this.annotations.set(chartId, annotations);
return annotations;
}
return [];
}
/**
* 清除所有标注
*/
clearAnnotations(chartId: string) {
this.annotations.delete(chartId);
localStorage.removeItem(`annotations_${chartId}`);
}
/**
* 导出标注
*/
exportAnnotations(chartId: string): string {
const annotations = this.annotations.get(chartId) || [];
return JSON.stringify(annotations, null, 2);
}
/**
* 导入标注
*/
importAnnotations(chartId: string, json: string) {
const annotations = JSON.parse(json);
this.annotations.set(chartId, annotations);
localStorage.setItem(`annotations_${chartId}`, json);
}
}
// 注册功能插件
echarts.registerFeature(DataAnnotation.type, DataAnnotation);
export default DataAnnotation;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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
💡 高级应用
插件管理器
typescript
// PluginManager.ts
class PluginManager {
private plugins: Map<string, any> = new Map();
private initialized: Set<string> = new Set();
/**
* 注册插件
*/
register(plugin: any): void {
if (this.plugins.has(plugin.type)) {
console.warn(`Plugin ${plugin.type} already registered`);
return;
}
this.plugins.set(plugin.type, plugin);
}
/**
* 卸载插件
*/
unregister(type: string): void {
const plugin = this.plugins.get(type);
if (plugin && this.initialized.has(type)) {
plugin.dispose?.();
this.initialized.delete(type);
}
this.plugins.delete(type);
}
/**
* 初始化所有插件
*/
initAll(echarts: any): void {
this.plugins.forEach((plugin, type) => {
if (!this.initialized.has(type)) {
plugin.init?.(echarts);
this.initialized.add(type);
}
});
}
/**
* 销毁所有插件
*/
disposeAll(echarts: any): void {
this.plugins.forEach((plugin, type) => {
plugin.dispose?.(echarts);
this.initialized.delete(type);
});
}
/**
* 获取插件
*/
getPlugin(type: string): any {
return this.plugins.get(type);
}
/**
* 列出所有插件
*/
listPlugins(): string[] {
return Array.from(this.plugins.keys());
}
}
export const pluginManager = new PluginManager();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
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
📊 性能考虑
插件加载策略
| 策略 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 立即加载 | 核心插件 | 使用时已就绪 | 增加初始体积 |
| 按需加载 | 可选功能 | 减小初始体积 | 首次使用有延迟 |
| 懒加载 | 大型插件 | 节省资源 | 需要处理加载状态 |
💎 总结
ECharts插件系统提供了强大的扩展能力:
- 图表扩展:创建全新图表类型
- 组件扩展:添加自定义UI组件
- 功能扩展:增强交互和数据处理
- 插件管理:统一的注册和管理机制
通过插件系统,可以将ECharts打造成适合特定业务需求的定制化可视化工具。
