ECharts 数据可视化:配置体系、交互与按需加载
本文目录
ECharts 的用法概括成一句话:把数据与规则写成 option 对象,交给 setOption,剩下的交给库。这句话掩盖了大量工程细节:option 的合并语义决定了动态更新的正确性,图型选择决定了图表能不能"被读懂",按需引入决定了打包体积,大数据量下采样与渐进渲染决定可用性。这篇文章按配置 → 选型 → 交互 → 体积 → 性能 → 实时更新的顺序展开,图表直接嵌在文中。
option 驱动的心智模型
option 不是"描述一次渲染的参数",而是图表状态的声明。setOption 默认是合并更新:新旧两个 option 深度合并,能复用的组件(轴、系列)不会重建。这个语义很方便,也有坑:
import * as echarts from 'echarts';
const chart = echarts.init(container);
// 第一次 setOption:建立完整状态
chart.setOption({
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: ['1月', '2月', '3月'] },
yAxis: { type: 'value' },
series: [{ type: 'line', name: '销量', data: [120, 200, 150] }],
});
// 合并更新:只改数据,轴和系列其他配置不动
chart.setOption({ series: [{ data: [150, 80, 210] }] });
// 系列数量会变化时,必须显式声明替换,否则旧系列残留
chart.setOption(
{ series: [{ type: 'bar', name: '新系列', data: [1, 2, 3] }] },
{ replaceMerge: ['series'] } // 按维度整体替换,而不是按 index 合并
);
| 更新方式 | 语义 | 适用 |
|---|---|---|
| 默认合并 | 新旧 option 递归合并 | 数据点更新、少量配置微调 |
notMerge: true | 新 option 整体替换 | 配置结构会大幅变化的场景 |
replaceMerge | 指定维度(如 series)替换 | 系列数量增减、切指标 |
appendData | 只对 scatter 追加数据 | 流式高频打点 |
数据本身建议走 dataset:数据与映射分离,换图型时不用重写数据结构:
// dataset + encode:数据只写一遍,系列声明"从哪个维度取值"
chart.setOption({
dataset: {
dimensions: ['月份', '成交量', '均线'],
source: [
['1月', 86, 80],
['2月', 92, 84],
['3月', 78, 86],
],
},
series: [
{ type: 'bar', name: '成交量', encode: { x: '月份', y: '成交量' } },
{ type: 'line', name: '均线', encode: { x: '月份', y: '均线' }, smooth: true },
],
});
图型选择:先明确问题再选图
图型不是从"好看"出发,而是从读者要回答的问题出发:
| 读者的问题 | 合适的图型 | 不合适 |
|---|---|---|
| 指标随时间怎么变 | 折线(时间序列) | 饼图(没有时间轴) |
| 几个类目谁大谁小 | 柱状(数量 ≤ 12) | 折线(类目没有顺序含义) |
| 构成占比多少 | 饼/环(切片 ≤ 5) | 柱状(占比对比不直观) |
| 两个变量是否相关 | 散点 | 任何连线图 |
| 二维分布密度 | 热力图 | 柱状 |
| 流程各环节转化 | 漏斗 | 饼图(丢失顺序信息) |
漏斗是"逐级转化"最诚实的表达——宽度即量级,缺口即流失:
选型的几条硬规则:类目超过 12 个先考虑分组或滚动;饼图切片超过 5 个把尾部合并成"其他";双轴(柱+线)只在量纲确实不同时使用,否则读者会错误比较两条轴的高度;3D 图型除非数据确实是三维分布,否则都是噪音。
坐标系与多轴
grid + xAxis + yAxis 组成一个直角坐标系,series 通过 xAxisIndex/yAxisIndex 绑定到具体的轴。多图布局用多个 grid 排版:
chart.setOption({
grid: [{ left: 48, right: 24, top: 40, bottom: 32 }],
xAxis: { type: 'category' }, // dataset 模式下类目自动取自 encode 的 x 维度
yAxis: [
{ type: 'value', name: '成交量', position: 'left' },
{ type: 'value', name: '均线', position: 'right', splitLine: { show: false } },
],
series: [
{ type: 'bar', name: '成交量', encode: { x: '月份', y: '成交量' } },
{ type: 'line', name: '均线', yAxisIndex: 1, smooth: true, encode: { x: '月份', y: '均线' } },
],
});
双轴的陷阱是刻度线不对齐,两条轴的网格线互相干扰。alignTicks: true 让多轴刻度对齐到同一套分隔;再配合把副轴的 splitLine 关掉,画面才干净。轴的单位写在 name 上("成交量(万)"),不要让读者去猜量纲。把这套配置(对齐刻度、右侧副轴、柱线组合)画出来:
tooltip 与 formatter 自定义
tooltip 是图表被读得最多的组件,默认行为只适合默认数据:
chart.setOption({
tooltip: {
trigger: 'axis', // 折线/柱状用 axis,散点/饼用 item
axisPointer: { type: 'cross' },
valueFormatter: (value: number) => `${value.toFixed(1)}%`,
position: (point, _params, _dom, rect) => {
// 跟随鼠标并防出界:优先显示在右侧,右边缘时翻到左侧
return [Math.min(point[0] + 16, rect.width - 180), point[1] - 12];
},
},
});
需要完全自定义内容时用函数 formatter 返回 HTML 字符串,可以对每个系列单独排版、加醒目标色。两条纪律:formatter 里不要做网络请求(tooltip 触发频率很高);HTML 内容如果拼接了数据来源的字段,要转义或用 ECharts 的富文本(rich)标签,防注入。
事件与联动
ECharts 的事件模型 = 图表自身行为(legend 切换、缩放)+ 图形交互(click/hover),通过 on/off 订阅:
// 点击数据项
chart.on('click', (params) => {
if (params.componentType !== 'series') return;
openDetail(params.name, params.value); // 参数里带系列名、维度名与值
});
// legend 切换后重算合计
chart.on('legendselectchanged', (params) => {
const visible = Object.keys(params.selected).filter((k) => params.selected[k]);
updateSummary(visible);
});
// 多图联动:同 group 的图表共享 dataZoom 与 tooltip
chart.setOption({ dataZoom: [{ type: 'inside' }] });
detailChart.setOption({ dataZoom: [{ type: 'inside' }] });
echarts.connect([chart, detailChart]);
联动的边界要心里有数:connect 同步的是选择与缩放行为,不会自动同步数据;两图的类目轴必须一致,否则联动出来的高亮是错位的。dataZoom 对大数据序列(type: 'inside')交互体验最好,但每次缩放都触发重新计算,数据量大时配合下文的 sampling 使用。
按需引入与体积
全量 import * as echarts from 'echarts' 会把所有图型、所有组件、两种渲染器都打进去。按需引入是标准做法:
// echarts/core 是不含任何图型的运行时核心
import * as echarts from 'echarts/core';
import { LineChart, BarChart, HeatmapChart } from 'echarts/charts';
import {
GridComponent,
TooltipComponent,
LegendComponent,
DatasetComponent,
DataZoomComponent,
VisualMapComponent,
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
echarts.use([
LineChart, BarChart, HeatmapChart,
GridComponent, TooltipComponent, LegendComponent,
DatasetComponent, DataZoomComponent, VisualMapComponent,
CanvasRenderer,
]);
| 引入物 | 说明 |
|---|---|
echarts/core | init/use/setOption 骨架,必须 |
echarts/charts | 用到的图型逐个注册,未注册的图型 setOption 报错 |
echarts/components | tooltip/legend/grid/dataset/visualMap 等按用到的注册 |
echarts/renderers | CanvasRenderer 或 SVGRenderer,一般二选一 |
| 主题 | echarts.registerTheme 注册后 init(el, 'themeName') |
体积收益与版本相关,以自己打包器的产物分析为准(全量引入的 gzip 体积通常在数百 KB 量级,按需后显著下降)。忘注册组件的症状是 setOption 不报错但某个交互缺失——遇到"配置明明写了但不生效",先查 use 列表。
响应式容器与 resize
图表尺寸跟随容器,靠 resize;容器高度为 0 时 init 出来的是空图(最常见的新手 bug):
const chart = echarts.init(container);
const observer = new ResizeObserver(() => chart.resize());
observer.observe(container);
// 组件卸载:resize 监听、图表实例一起清理
function dispose(): void {
observer.disconnect();
chart.dispose(); // dispose 后容器内容与事件全部清掉
}
注意细节:resize 是重排不是重建,通常无需重新 setOption;但在 dataset 模式下换数据源仍要显式 setOption。多图页面让多个 chart 共享一个 ResizeObserver 没问题,回调里逐个 resize 即可。SSR 环境没有 DOM,初始化要包在挂载后的生命周期里,别在模块顶层 init。
主题定制
主题把颜色、字号、间距统一起来,业务里不建议在每处 option 里散写颜色:
echarts.registerTheme('brand', {
color: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'],
textStyle: { fontFamily: 'system-ui, sans-serif', fontSize: 12 },
categoryAxis: { axisLine: { lineStyle: { color: '#cbd5e1' } } },
});
const chart = echarts.init(container, 'brand'); // init 时指定主题
// 深色模式:registerTheme 注册一套 dark 主题,跟随系统切换时
// dispose 后按新主题重新 init(换主题不重建会残留旧色)
主题色板选无障碍色系(色盲可区分、黑白打印可区分),红绿对比不要作为唯一信息通道,必要时在 tooltip/label 里冗余一次数值。暗色主题不是把亮色 invert——网格线、背景、tooltip 都要单独调,直接复用亮色主题会出现对比度不足。
大数据量渲染
数据点过万后,默认配置会卡顿甚至卡死浏览器。策略按数据形态选:
| 策略 | 作用 | 适用 |
|---|---|---|
sampling: 'lttb' | 降采样保留视觉形态 | 折线/面积,点数 1 万+ |
large: true | 特化大数据渲染路径 | scatter/lines 系列 |
progressive 渐进渲染 | 分帧渲染避免长任务 | scatter/自定义系列 |
appendData | 只追加不重算 | 实时打点的 scatter |
| 关闭动画 + 简化 tooltip | 消除交互负担 | 所有大数据场景 |
chart.setOption({
series: [
{
type: 'line',
sampling: 'lttb', // 降采样后线条仍保留峰值形态
data: bigSeries, // 原始 10 万点
animation: false, // 大数据动画是纯开销
tooltip: { trigger: 'axis' },
},
],
});
二维密度问题交给热力图,先在数据侧聚合(把 10 万个点聚成 7×24 的矩阵),别让前端逐点渲染:
判断该不该优化的顺序:先看是不是"应该聚合却在画明细"(多数情况是产品侧问题),再谈降采样与渐进渲染。降采样要向读者披露(标注"已采样"),不然尖峰被平均掉会误导判断。
实时更新与服务端配合
实时看板的更新链路是"推送 → 前端缓冲 → 节流 setOption",两端都有讲究:
const buffer: number[] = [];
let flushTimer = 0;
// 推送端:SSE / WebSocket 持续到达,只进缓冲,不直接碰图表
function onTick(point: number): void {
buffer.push(point);
if (buffer.length >= 120) buffer.shift(); // 固定窗口,防止无限增长
}
// 渲染端:固定节奏整批刷新,例如 2 秒一次
function startFlushing(): void {
flushTimer = window.setInterval(() => {
if (document.hidden) return; // 页面不可见时跳过,数据继续攒在窗口里
chart.setOption({ series: [{ data: [...buffer] }] });
}, 2000);
}
要点:
- 合并频率与推送频率解耦。服务端 10Hz 推、前端 0.5Hz 画,图表比逐条 setOption 流畅得多;节流间隔按业务节奏选(监控类 1–5 秒)。
- 数据窗口在服务端就定好(最近 N 条),前端只渲染,不做无限增长的内存累积。
- 更新走默认合并语义(只换 series 的 data),避免
notMerge导致整个图表重建、tooltip 闪烁。 - 断线重连、鉴权过期要有可见状态(图表上叠"连接中断"提示),并防止重复订阅造成双倍推送。
- 时间序列的 x 轴用
category要处理边界(新增类目),用time类型则由时间戳自动排布,实时流更省心。
注意事项与边界
echarts.init传入的容器必须有非零宽高,常见故障是 flex/grid 布局里图表容器高度塌陷,先查 CSS 再怀疑 ECharts。- dispose 是必做清理:SPA 路由切换不 dispose 会积累实例与事件监听,表现是内存缓涨、交互越来越卡。
- 一个容器只能 init 一次;重复 init 要先 dispose 或复用实例,报错信息并不直观。
- 图表导出(服务端渲染图片)与浏览器端
getDataURL都依赖当前渲染状态,注意字体加载完成后再导出。 - 双 y 轴要
alignTicks并解释量纲,否则读者会做跨轴比较;这个错误在评审时很难被肉眼发现。 - formatter 拼接用户可控字符串时注意转义;富文本标签优先。
- 移动端触屏上 tooltip 的 hover 触发体验与鼠标不同,
tooltip.triggerOn: 'click'或移动端隐藏部分交互是常见取舍。 - 无障碍:图表本身对读屏器不透明,关键图表旁给数据表格或文字摘要,不要只给一张图。
总结
ECharts 生产化的主线:配置上,吃透 setOption 的合并语义,数据进 dataset、映射进 encode;选型上,先定读者的问题再定图型;体积上,core + 图型 + 组件按 use 注册;性能上,大数据先聚合、再采样、再渐进渲染;实时上,推送与渲染解耦节流,窗口有限、断线可见。把这几条定型成团队封装(一个带 resize/dispose/按需注册的图表组件),业务侧只写 option,质量才稳定。
参考
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。