Apache ECharts 是 Apache 基金会下的开源可视化库,底层基于 ZRender,覆盖折线、柱状、饼图、散点、雷达、热力、地图等大量图表类型,文档与示例以中文友好见长。本篇按官方快速上手、引入说明、数据集、事件与行为等手册章节整理,尽量给出可直接粘贴改跑的 option / API 片段。
和 Chart.js 等比一句话:Chart.js 更轻、上手快;ECharts 图表种类与交互组件更全,国内业务看板、大屏里更常见。选库看「要不要地图 / 多种坐标系 / 复杂联动」,而不是谁更「高级」。
简介与适用场景
适合:
- 管理后台、分析报表、数据大屏中的趋势、对比、占比、分布
- 需要 tooltip、legend、dataZoom、toolbox、visualMap 等开箱交互
- 希望用声明式
option驱动,而不是从零写 Canvas / SVG
不太适合或要慎重:
- 包体积必须极小、只要一两张极简图 → 可看更轻的方案
- 出版级统计图、强科学计算叙事 → Observable Plot / Vega-Lite / D3 可能更对口
- 高度定制的「信息图插画」→ 往往仍要 D3 或手绘
对多数业务可视化,先把数据映射和坐标系想清楚,再谈美化。
引入方式:npm、按需引入、CDN
对照官方「获取 → 引入 → 绘制」流程。
npm 全量引入(原型最快)
npm install echarts --save
import * as echarts from 'echarts';
const myChart = echarts.init(document.getElementById('main'));
myChart.setOption({
title: { text: 'ECharts 入门示例' },
tooltip: {},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'],
},
yAxis: {},
series: [{ name: '销量', type: 'bar', data: [5, 20, 36, 10, 10, 20] }],
});
按需引入(官方推荐减小体积)
按需引入时必须自行注册渲染器(CanvasRenderer 或 SVGRenderer),否则无法绘制。
import * as echarts from 'echarts/core';
import { BarChart, LineChart, PieChart, ScatterChart, RadarChart, HeatmapChart } from 'echarts/charts';
import {
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
DatasetComponent,
TransformComponent,
ToolboxComponent,
DataZoomComponent,
VisualMapComponent,
PolarComponent,
RadarComponent,
GeoComponent,
} from 'echarts/components';
import { LabelLayout, UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
echarts.use([
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
DatasetComponent,
TransformComponent,
ToolboxComponent,
DataZoomComponent,
VisualMapComponent,
PolarComponent,
RadarComponent,
GeoComponent,
BarChart,
LineChart,
PieChart,
ScatterChart,
RadarChart,
HeatmapChart,
LabelLayout,
UniversalTransition,
CanvasRenderer,
]);
export { echarts };
示例编辑页「完整代码」可根据当前 option 生成最小按需引入清单,很实用。v5.5.0 起默认模块规范为 ESM,升级时注意 breaking changes。
CDN(无构建演示)
从 jsDelivr 等取 dist/echarts.min.js(生产请钉版本号):
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>ECharts</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
</head>
<body>
<!-- 必须有明确宽高的 DOM 容器 -->
<div id="main" style="width: 600px; height: 400px;"></div>
<script>
var myChart = echarts.init(document.getElementById('main'));
var option = {
title: { text: 'ECharts 入门示例' },
tooltip: {},
legend: { data: ['销量'] },
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'],
},
yAxis: {},
series: [{ name: '销量', type: 'bar', data: [5, 20, 36, 10, 10, 20] }],
};
myChart.setOption(option);
</script>
</body>
</html>
第一个完整图表:init → setOption → dispose
生命周期记住三步:
echarts.init(dom, theme?, opts?)创建实例myChart.setOption(option, opts?)渲染 / 更新- 离开页面或销毁组件时
myChart.dispose(),避免泄漏
import * as echarts from 'echarts';
const el = document.getElementById('main') as HTMLDivElement;
const myChart = echarts.init(el);
const option: echarts.EChartsOption = {
title: { text: '周活用户' },
tooltip: { trigger: 'axis' },
legend: { data: ['UV'] },
grid: { left: 48, right: 24, top: 64, bottom: 40, containLabel: true },
xAxis: {
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
},
yAxis: { type: 'value' },
series: [
{
name: 'UV',
type: 'line',
smooth: true,
data: [820, 932, 901, 934, 1290, 1330, 1320],
},
],
};
myChart.setOption(option);
// SPA / 组件卸载:
window.addEventListener('resize', () => myChart.resize());
// ...
// myChart.dispose();
在 React / Vue 中务必在 DOM 已挂载 后 init,在 unmount / onBeforeUnmount 里 dispose。setOption 默认合并更新;需要整表替换时用:
myChart.setOption(fullOption, { notMerge: true });
// 或只替换 series:
myChart.setOption(partial, { replaceMerge: ['series'] });
坐标系概念:cartesian / polar / geo
ECharts 把「画在哪」和「画什么」拆开:
| 概念 | 作用 | 常见搭配 |
|---|---|---|
| Cartesian(直角坐标) | 由 grid + xAxis + yAxis 组成 | line / bar / scatter / heatmap |
| Polar(极坐标) | polar + radiusAxis + angleAxis | 极坐标柱/线、部分雷达相关布局 |
| Geo(地理坐标) | geo 组件 + 地图数据 | map / scatter(经纬度)/ lines |
| 单轴 / 平行坐标等 | singleAxis、parallel | 特殊分析图 |
直角坐标最小骨架:
option = {
grid: { left: '10%', right: '8%', top: 60, bottom: 40 },
xAxis: { type: 'category', data: ['A', 'B', 'C'] },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: [1, 2, 3] }],
};
极坐标示意(柱状沿角度展开):
option = {
polar: {},
angleAxis: {
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
},
radiusAxis: {},
series: [
{
type: 'bar',
coordinateSystem: 'polar',
data: [1, 2, 3, 4, 3, 5, 1],
},
],
};
地理坐标需要先注册地图 GeoJSON(注意版权与合规),再:
echarts.registerMap('demo', geoJson);
option = {
geo: { map: 'demo', roam: true },
series: [
{
type: 'scatter',
coordinateSystem: 'geo',
data: [{ name: '某点', value: [116.4, 39.9, 100] }],
},
],
};
系列通过 coordinateSystem: 'cartesian2d' | 'polar' | 'geo' | ...(多数直角图可省略,默认直角坐标系)声明落在哪套坐标系上。
常用 series 与可运行 option
下列片段均可在官方示例编辑器里粘贴运行(需已有 myChart)。
line 折线
option = {
title: { text: '近 7 日转化率' },
tooltip: { trigger: 'axis' },
legend: { data: ['自然', '投放'] },
xAxis: {
type: 'category',
boundaryGap: false,
data: ['10-03', '10-04', '10-05', '10-06', '10-07', '10-08', '10-09'],
},
yAxis: { type: 'value', axisLabel: { formatter: '{value}%' } },
series: [
{ name: '自然', type: 'line', data: [2.1, 2.4, 2.3, 2.8, 3.1, 2.9, 3.4] },
{ name: '投放', type: 'line', data: [1.4, 1.6, 1.9, 2.0, 2.2, 2.1, 2.5] },
],
};
bar 柱状
option = {
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { data: ['本周', '上周'] },
xAxis: { type: 'category', data: ['搜索', '社交', '邮件', '直接', '其他'] },
yAxis: { type: 'value' },
series: [
{ name: '本周', type: 'bar', data: [320, 240, 180, 410, 90] },
{ name: '上周', type: 'bar', data: [280, 210, 160, 380, 110] },
],
};
横向条形:把 xAxis / yAxis 的 type 对调(一个 value、一个 category),series.type 仍为 'bar'。
pie 饼 / 环
option = {
title: { text: '错误类型占比', left: 'center' },
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { orient: 'vertical', left: 'left' },
series: [
{
name: '错误',
type: 'pie',
radius: ['40%', '68%'],
data: [
{ value: 48, name: '超时' },
{ value: 32, name: '解析失败' },
{ value: 15, name: '鉴权' },
{ value: 5, name: '其他' },
],
},
],
};
scatter 散点
option = {
tooltip: {
trigger: 'item',
formatter: (p) => `x=${p.data[0]}, y=${p.data[1]}`,
},
xAxis: { type: 'value', scale: true, name: '时长' },
yAxis: { type: 'value', scale: true, name: '转化%' },
series: [
{
type: 'scatter',
symbolSize: 12,
data: [
[2.1, 1.2],
[3.4, 1.8],
[5.0, 2.6],
[6.2, 3.1],
[8.0, 2.4],
[9.5, 3.8],
],
},
],
};
radar 雷达
雷达图使用 radar 组件声明各轴指标,系列 type: 'radar':
option = {
legend: { data: ['预算', '实际'] },
radar: {
indicator: [
{ name: '销售', max: 100 },
{ name: '管理', max: 100 },
{ name: '信息技术', max: 100 },
{ name: '客服', max: 100 },
{ name: '研发', max: 100 },
{ name: '市场', max: 100 },
],
},
series: [
{
type: 'radar',
data: [
{ name: '预算', value: [80, 70, 90, 85, 75, 70] },
{ name: '实际', value: [70, 75, 80, 90, 85, 78] },
],
},
],
};
heatmap 热力(直角坐标系)
热力常配合 visualMap 映射颜色:
const hours = ['12a', '2a', '4a', '6a', '8a', '10a', '12p', '2p', '4p', '6p'];
const days = ['周六', '周五', '周四', '周三', '周二'];
const data = [
[0, 0, 5],
[1, 0, 1],
[2, 0, 0],
[3, 0, 3],
[0, 1, 7],
[1, 1, 2],
[2, 1, 4],
[3, 1, 6],
[0, 2, 1],
[1, 2, 8],
[2, 2, 3],
[3, 2, 5],
].map((item) => [item[0], item[1], item[2] || '-']);
option = {
tooltip: { position: 'top' },
grid: { height: '50%', top: '10%' },
xAxis: { type: 'category', data: hours, splitArea: { show: true } },
yAxis: { type: 'category', data: days, splitArea: { show: true } },
visualMap: {
min: 0,
max: 10,
calculable: true,
orient: 'horizontal',
left: 'center',
bottom: '5%',
},
series: [
{
name: '访问量',
type: 'heatmap',
data,
label: { show: true },
emphasis: {
itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0, 0, 0, 0.5)' },
},
},
],
};
核心组件速查
| 组件 | 职责 | 常用字段 |
|---|---|---|
title | 主/副标题 | text subtext left top |
legend | 图例显隐 | data orient selected |
tooltip | 提示框 | trigger: 'axis'|'item' formatter |
toolbox | 工具栏 | feature.saveAsImage dataZoom restore |
dataZoom | 缩放漫游 | type: 'inside'|'slider' |
visualMap | 视觉映射 | min max inRange dimension |
grid | 直角坐标绘图区 | left/right/top/bottom containLabel |
xAxis / yAxis | 轴 | type: 'category'|'value'|'time'|'log' |
组合示例:
option = {
color: ['#7A2438', '#B46C78', '#4E1424'],
title: { text: '看板', subtext: '数据截至今日', left: 'left' },
tooltip: { trigger: 'axis' },
legend: { top: 28 },
toolbox: {
feature: {
saveAsImage: {},
dataZoom: {},
restore: {},
},
},
dataZoom: [
{ type: 'inside', start: 0, end: 100 },
{ type: 'slider', start: 0, end: 100 },
],
grid: { left: 56, right: 24, top: 72, bottom: 64, containLabel: true },
xAxis: { type: 'category', data: ['A', 'B', 'C', 'D', 'E'] },
yAxis: { type: 'value', splitLine: { show: true } },
series: [{ type: 'bar', name: '指标', data: [10, 22, 18, 30, 16] }],
};
visualMap 常与散点/热力一起用,把某一维度映射到颜色或尺寸(见上一节 heatmap,以及 dataset 一节的气泡尺寸例子)。
dataset + encode(官方推荐的数据方式)
从 ECharts 4 起更推荐用 dataset 管数据、用 series.encode 做映射,把「常变的数据」和「少变的样式配置」拆开,并让多个 series 复用同一份表。
二维表 + 默认映射
option = {
legend: {},
tooltip: {},
dataset: {
source: [
['product', '2015', '2016', '2017'],
['Matcha Latte', 43.3, 85.8, 93.7],
['Milk Tea', 83.1, 73.4, 55.1],
['Cheese Cocoa', 86.4, 65.2, 82.5],
['Walnut Brownie', 72.4, 53.9, 39.1],
],
},
xAxis: { type: 'category' }, // 默认映射第一列
yAxis: {},
series: [{ type: 'bar' }, { type: 'bar' }, { type: 'bar' }], // 后续各列一个系列
};
对象数组 + dimensions
option = {
legend: {},
tooltip: {},
dataset: {
dimensions: ['product', '2015', '2016', '2017'],
source: [
{ product: 'Matcha Latte', 2015: 43.3, 2016: 85.8, 2017: 93.7 },
{ product: 'Milk Tea', 2015: 83.1, 2016: 73.4, 2017: 55.1 },
{ product: 'Cheese Cocoa', 2015: 86.4, 2016: 65.2, 2017: 82.5 },
],
},
xAxis: { type: 'category' },
yAxis: {},
series: [{ type: 'bar' }, { type: 'bar' }, { type: 'bar' }],
};
显式 encode
option = {
dataset: {
source: [
['score', 'amount', 'product'],
[89.3, 58212, 'Matcha Latte'],
[57.1, 78254, 'Milk Tea'],
[74.4, 41032, 'Cheese Cocoa'],
],
},
xAxis: {},
yAxis: { type: 'category' },
series: [
{
type: 'bar',
encode: {
x: 'amount',
y: 'product',
tooltip: ['product', 'score'],
},
},
],
};
要点:
seriesLayoutBy: 'column' | 'row'决定按列还是按行生成系列- 多份数据:
dataset: [{ source: ... }, { source: ... }],系列用datasetIndex引用 - 饼图等可用
encode: { itemName: ..., value: ... } treemap/graph等仍常用series.data;百万级增量用appendData,此时不走 dataset
事件与行为:click、dispatchAction
监听鼠标事件
事件名为小写字符串:click dblclick mouseover mouseout globalout contextmenu 等。
myChart.on('click', function (params) {
// params.componentType / seriesType / name / dataIndex / value / color ...
console.log(params.name, params.value);
});
// 只监听折线系列
myChart.on('click', 'series.line', function (params) {
/* ... */
});
// 按系列名过滤
myChart.on('mouseover', { seriesName: '销量' }, function () {
/* ... */
});
组件交互也会抛事件,例如图例切换触发的是 legendselectchanged(注意不是 legendselected):
myChart.on('legendselectchanged', function (params) {
console.log(params.name, params.selected);
});
点击「空白处」要用 ZRender 层事件(target 为空表示点在空白):
myChart.getZr().on('click', function (event) {
if (!event.target) {
// 重置选中、关闭浮层等
}
});
程序触发行为:dispatchAction
// 高亮某一数据项并弹出 tooltip(大屏轮播常用)
myChart.dispatchAction({ type: 'highlight', seriesIndex: 0, dataIndex: 2 });
myChart.dispatchAction({ type: 'showTip', seriesIndex: 0, dataIndex: 2 });
myChart.dispatchAction({ type: 'downplay', seriesIndex: 0, dataIndex: 2 });
完整 action / event 列表见官方 action 与 events 文档。
主题、暗色与 registerTheme
内置可直接:
const chart = echarts.init(dom, 'dark');
自定义主题先注册再 init(也可在主题编辑器导出 JSON):
echarts.registerTheme('cabernet', {
color: ['#7A2438', '#B46C78', '#C8909A', '#4E1424', '#8A6068'],
backgroundColor: 'transparent',
textStyle: { color: '#3a2a2e' },
title: { textStyle: { color: '#7A2438', fontWeight: 600 } },
line: { smooth: true },
});
const chart = echarts.init(dom, 'cabernet');
从 JSON 加载:
const theme = await fetch('/theme/cabernet.json').then((r) => r.json());
echarts.registerTheme('cabernet', theme);
const chart = echarts.init(dom, 'cabernet');
站点明暗切换常见两种策略:
dispose()后用新主题重新init- 保留实例,按主题改写
textStyle/ 轴线颜色再setOption
function applySiteTheme(chart, dark) {
chart.setOption({
backgroundColor: 'transparent',
textStyle: { color: dark ? '#e8dde0' : '#3a2a2e' },
title: { textStyle: { color: dark ? '#e6b3bc' : '#7A2438' } },
});
}
响应式:resize 与容器高度陷阱
容器尺寸变化后必须调用 myChart.resize(),否则图会拉伸变形或留白。
window.addEventListener('resize', () => myChart.resize());
// 更稳:监听容器本身(侧栏折叠、分栏拖动)
const ro = new ResizeObserver(() => myChart.resize());
ro.observe(el);
// 卸载:ro.disconnect(); myChart.dispose();
高度陷阱(最常见白屏原因):
- 父级是 flex / grid,子项高度为
auto,图表 div 实际高度为 0 - Tab / 抽屉初始
display: none,此时init量到的宽高为 0;显示后要再resize() - 只写了
width: 100%却没写高度;百分比高度要求祖先有明确高度
推荐:给图表容器固定 height: 360px(或 min-height),或由父级明确撑开后再 init。
异步数据与 loading
官方建议:先 init 出坐标系骨架,数据返回后再 setOption;加载中用内置 loading。
myChart.showLoading(); // 可传配置:{ text: '加载中', maskColor: 'rgba(255,255,255,0.6)' }
fetch('/api/metrics')
.then((r) => r.json())
.then((data) => {
myChart.hideLoading();
myChart.setOption({
xAxis: { data: data.categories },
series: [
{ name: '销量', type: 'bar', data: data.values },
],
});
})
.catch(() => {
myChart.hideLoading();
// 错误提示 UI
});
动态刷新时尽量带上 series.name,方便 ECharts 做差分动画;高频更新可设 animation: false 或 lazyUpdate: true。
大数据与性能
- 按需引入:只注册用到的 chart / component / renderer。
- 折线采样:
sampling: 'lttb'(或'average'等),大数据降采样。 - progressive:达到阈值后分片绘制,避免一次卡死主线程。
- large / largeThreshold:散点等大量图形时开启大规模模式。
- Canvas vs SVG:默认 Canvas 更适合海量图形;需要超清打印、CSS 动画叠层时可换
SVGRenderer(按需引入时二选一注册,init第三参renderer: 'svg')。 - 百万级增量:用
appendData,此时不要用 dataset。 - 减少重绘:能局部
setOption就不要notMerge: true全量砸。
series: [
{
type: 'line',
showSymbol: false,
sampling: 'lttb',
large: true,
progressive: 4000,
progressiveThreshold: 10000,
data: hugeArray,
},
],
import { SVGRenderer } from 'echarts/renderers';
echarts.use([SVGRenderer /* ... */]);
const chart = echarts.init(dom, null, { renderer: 'svg' });
TypeScript 类型提示
全量引入可用 echarts.EChartsOption。按需引入时,官方推荐用 ComposeOption 拼出最小且严格的 option 类型,少注册组件时类型就能报错:
import * as echarts from 'echarts/core';
import { BarChart, LineChart } from 'echarts/charts';
import {
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
} from 'echarts/components';
import { LabelLayout, UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import type { BarSeriesOption, LineSeriesOption } from 'echarts/charts';
import type {
TitleComponentOption,
TooltipComponentOption,
GridComponentOption,
DatasetComponentOption,
} from 'echarts/components';
import type { ComposeOption } from 'echarts/core';
type ECOption = ComposeOption<
| BarSeriesOption
| LineSeriesOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
| DatasetComponentOption
>;
echarts.use([
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
BarChart,
LineChart,
LabelLayout,
UniversalTransition,
CanvasRenderer,
]);
const option: ECOption = {
title: { text: 'TS 示例' },
tooltip: {},
xAxis: { type: 'category', data: ['A', 'B'] },
yAxis: {},
series: [{ type: 'bar', data: [1, 2] }],
};
const chart = echarts.init(document.getElementById('main')!);
chart.setOption(option);
事件回调可参考手册中的 EventParams 字段(componentType、seriesType、dataIndex 等)自行标注类型。
常见错误与调试
| 现象 | 排查 |
|---|---|
| 完全空白 | 容器宽高是否为 0;按需引入是否漏了 Renderer / Chart |
| 只有轴没有线 | series.data 是否为空;类目轴 length 是否与 data 对齐 |
| 更新后旧系列残留 | 使用 notMerge 或 replaceMerge: ['series'] |
| 饼图怪坐标轴 | 饼图通常不需要 xAxis/yAxis |
| encode「不生效」 | 维度名拼写是否一致(空格、大小写) |
| SSR 报错 | 仅在浏览器侧 init(Next/Nuxt 用 client-only) |
| 越来越卡 | 是否重复 init 未 dispose;定时器是否清理 |
| 暗色看不清 | 检查文字色、轴线色、backgroundColor |
调试技巧:
console.log(myChart.getOption())看合并后的真实配置- 官方示例里对比同类型图的最小 option
- 暂时改回全量
import * as echarts from 'echarts'排除「漏注册组件」 - 打开浏览器 Performance 看是否主线程被大数据绘制占满
官方文档与示例站
- 快速上手 get-started
- 在项目中引入 / 按需引入
- 配置项手册 Option
- 示例中心 Examples
- 数据集 dataset
- 事件与行为
- 异步数据与 loading
- 样式与主题
- API:echarts.init / setOption / dispatchAction
先保证映射正确、容器有尺寸、组件注册齐全,再调颜色与动画。把本篇的 option 片段丢进示例编辑器改一改数据,通常比从零读完整 Option 手册更快建立手感。
评论