Skip to content
仪表板项目拆解与布局方案
先用 HTML 和 CSS 把面板分区搭好。典型的仪表板会有一个网格布局,每个格子放一个图表容器。这里用三个 div,分别赋 id,CSS Grid 控制排列:
html
<div class="dashboard">
<div id="chart-bar" class="chart-item"></div>
<div id="chart-line" class="chart-item"></div>
<div id="chart-pie" class="chart-item"></div>
</div>css
.dashboard {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.chart-item {
width: 100%;
height: 300px;
}每个 div 必须具备可计算的宽高,否则 echarts.init 会报错。
实例初始化与容器监听
echarts.init 接收三个参数:容器 DOM、主题名(或主题对象)、以及一个可选配置对象,其中 renderer 指定 'canvas' 或 'svg' [1]。
一个常见的做法是写一个初始化函数,统一管理多个实例:
ts
const instances = new Map<string, ECharts>();
function createChart(
id: string,
option: EChartsOption,
theme?: string
): ECharts | null {
const dom = document.getElementById(id);
if (!dom) return null;
const chart = echarts.init(dom, theme);
chart.setOption(option);
instances.set(id, chart);
return chart;
}容器尺寸变化时必须同步图表大小,否则图表会停留在旧尺寸而留白或挤压。通常用 ResizeObserver 监听每个容器:
ts
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const chart = instances.get(entry.target.id);
chart?.resize();
}
});
// 初始化后绑定监听
document.querySelectorAll('.chart-item').forEach((el) => ro.observe(el));resize 方法可以传入 width/height 直接覆盖容器尺寸 [2]。如果仪表板在某个阶段需要强制设定固定像素大小,可以这样用;但对自适应来说,不传参直接调用就够了。
注意 ResizeObserver 会在元素创建后立刻触发一次回调,所以图表在首次 setOption 时就已经拿到正确尺寸。如果不用 ResizeObserver,也可以在 window.onresize 里手动调用每个实例的 resize,但前者更精确。
多图表配置与风格统一
不同图表如果各自写一套 tooltip、legend、颜色,会让仪表板看起来支离破碎。抽离公共配置是性价比很高的做法。
公共配置抽取
ECharts 的 option 顶层字段大多可以复用——tooltip、legend、grid、color 等。把这些提取出来,在创建各个 chart option 时展开合并即可 [4]:
ts
const baseOption: EChartsOption = {
tooltip: { trigger: 'axis' },
legend: { bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true },
color: ['#5470c6', '#91cc75', '#fac858', '#ee6666']
};
const barOption: EChartsOption = {
...baseOption,
xAxis: { type: 'category', data: ['A', 'B', 'C'] },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: [120, 200, 150] }]
};合并时 ...baseOption 放在前面,后面的字段会覆盖前面的同名属性,所以可以按需微调某个实例的特定组件。如果某个图表不需要图例,直接覆盖 legend 为 undefined 或 { show: false } 即可。
用 getOption 可以导出某个已生成图表的完整配置,在此基础上修改得到新图,也是一个常用渠道。
主题复用
echarts.registerTheme 注册自定义主题对象 [3],之后所有实例都能通过主题名复用这套风格:
ts
echarts.registerTheme('dashboard-theme', {
color: ['#2ec7c9', '#b6a2de', '#5ab1ef', '#ffb980', '#d87a80'],
backgroundColor: 'rgba(0,0,0,0)',
textStyle: {},
title: { textStyle: { color: '#333' } },
// ……
});
createChart('chart-bar', barOption, 'dashboard-theme');主题对象结构本质上就是去掉数据和系列部分的 option,可以覆盖颜色、字体、背景、轴线样式等。仪表板项目如果先注册主题,之后的 init 就不需要反复传递颜色数组了。
交互联动:让仪表板成为数据探索工具
静态图表只能看,仪表板的意义在于可以点、可以联动。ECharts 的事件系统允许一个图表的行为触发另一个图表的反应,核心是 on 监听 + dispatchAction 程序化触发 [5]。
例如,柱状图每根柱子对应一个品类,点击柱子后让折线图高亮同一品类的数据点:
ts
barChart.on('click', (params) => {
// 假设柱状图和折线图数据顺序一致
lineChart.dispatchAction({
type: 'highlight',
seriesIndex: 0,
dataIndex: params.dataIndex
});
});
lineChart.on('mouseout', () => {
lineChart.dispatchAction({ type: 'downplay', seriesIndex: 0 });
});dispatchAction 支持的动作类型远不止 highlight/downplay,还有 showTip、legendSelect、dataZoom 等。通过 target 参数还可以指定发给哪个图表,这在用 connect 将多个图表组成 group 时特别有用。如果只想在自己图表上触发动作,target 可以不填。
一个稍微复杂一点的例子:饼图选中某项后,柱状图和折线图都只显示相关分类。这需要结合 legend 的选中状态控制:
ts
pieChart.on('legendselectchanged', (params) => {
const name = Object.keys(params.selected)[0];
[barChart, lineChart].forEach((chart) => {
chart.dispatchAction({
type: 'legendToggleSelect',
name: name
});
});
});这里利用了 legendToggleSelect 动作,但前提是几个图表的图例数据项名称一致。用这种方式可以在不修改原始数据的情况下实现交叉筛选。
大数据渲染的挑战与性能调优
当折线图的数据点从几百条涨到几万、几十万条,默认的逐点绘制会撑满渲染线程,导致拖拽、缩放卡顿,甚至浏览器假死。ECharts 提供了几种正交的手段来缓解这个问题。
large 模式
series-line.large 设为 true 后,ECharts 启用精简绘制模式 [6]。largeThreshold 默认 2000,数据量超过这个阈值才自动开启大型绘制。开启后图形会丢失一些细节(比如拐角处的形状变为直线),但渲染速度提升明显。
ts
series: [{
type: 'line',
large: true,
largeThreshold: 2000,
data: hugeData
}]只要数据量可能超过几千,就建议直接打开这个开关。注意不是所有系列类型都支持 large,目前主要是 line、bar 和 scatter 的部分形态。
采样策略
即使开了 large,最终还是要给 Canvas 或 SVG 传绘制指令。如果能在数据层面先减少要绘制的点数,性能会进一步提升。series-line.sampling 可以在 large 模式下对数据进行降采样,策略有 'lttb'(最大三角形数量桶)、'average'、'max'、'min'、'sum' 等 [7]。
ts
series: [{
type: 'line',
large: true,
sampling: 'lttb',
data: extremelyLargeData
}]'lttb' 在保留视觉趋势方面表现最好,适合需要看清波动形态的场景。如果只是关心最大值或最小值变化,'max'/'min' 更直接。'average' 会把一个桶内的数据取平均值,平滑掉局部尖峰。
采样和 large 并不绑定,但通常一起使用。采样并不永久改变原始数据,只是渲染时生效,数据仍保留完整。
渐进式渲染
即使不启用 large,也可以用 progressive 和 progressiveThreshold 控制分块渲染 [8]。数据量超过 progressiveThreshold(默认 3000)时,ECharts 会把图形拆成多个小块,在不同帧中逐步绘制,避免单帧阻塞太久。
ts
series: [{
type: 'line',
progressive: 200, // 每帧绘制的数据点数量
progressiveThreshold: 3000,
data: hugeData
}]当数据量达到 5 万以上时,即使做了采样,渐进式渲染也能避免初次渲染时页面冻结。这个参数对用户体验的影响是:图表不会一次性出现,而是以肉眼可见的速度逐步填充,交互响应在渲染过程中依然可用。
选择正确的渲染器:Canvas 还是 SVG
echarts.init 第三个参数里可以指定渲染器类型 [9]。默认是 'canvas',绝大多数场景用它就够了。SVG 的特色在于图形是 DOM 元素,可以用 CSS 修改样式,也能保留矢量特性利于打印和导出。
切换渲染器很简单,但要重新初始化实例:
ts
const chart = echarts.init(dom, null, { renderer: 'svg' });Canvas 渲染器在面对大量图形元素时更高效,因为不需要维护庞大的 DOM 树。数据量超过万点或者涉及到持续动画(如流式数据)时,Canvas 的帧率明显更稳。SVG 则适合小规模、强调可访问性或需要精细样式控制的仪表板,比如政府报告中导出矢量图叠加到 PDF。
无法在运行时无缝切换渲染器,必须销毁实例再重新 init。
实时数据流与资源回收
仪表板有时需要接入实时推送的数据,比如每秒刷新股票价格。如果每次都调用 setOption 重建整个 chart,成本太高。appendData 可以在不重建 option 的情况下向指定系列末端追加数据 [10]:
ts
// 初始化时开启 large
chart.setOption({
xAxis: { type: 'time' },
series: [{ type: 'line', large: true, data: initialData }]
});
setInterval(() => {
const now = Date.now();
chart.appendData({
seriesIndex: 0,
data: [[now, Math.random()]]
});
}, 1000);appendData 对性能的提升很明显,因为它只处理新增片段,旧渲染结果得以保留。但该接口对系列类型和初始配置有严格要求:多数基于笛卡尔坐标系的系列(line、bar、candlestick 等)可用,且需预先开启 large。并不是所有系列都支持,使用前得查阅手册对应系列的说明。
仪表板跑得再久,终究有“停止”的时候。单页应用中路由切换、组件卸载时,如果实例没有正确销毁,事件监听、定时器、DOM 引用会一直留在内存里。dispose 可以彻底释放资源 [12]:
ts
instances.forEach((chart) => chart.dispose());
instances.clear();dispose 会清除图表、解绑所有事件、释放内部引用。与之相比,clear 只是清空绘制内容(移除所有系列和组件),实例本身还在,之后还能重新 setOption 画新图 [11]。需要“换一张图”时可以用 clear,但离开页面必须 dispose。
如果仪表板里还启用了轮询或 WebSocket 连接,销毁实例的同时也得断开这些外部资源,否则即便调了 dispose,这些外部定时器仍在引用不再需要的回调函数。
性能检查清单
以下是仪表板上线前可以逐项过一遍的要点,来自 ECharts 性能优化手册 [13]:
- 数据量大的折线/柱状图,开启
large: true并设置适当largeThreshold。 - 对超大数据集启用
sampling,常用'lttb'。 - 用
progressive系列参数避免首屏渲染卡顿。 - 能用
dataset统一管理数据时统一用,减少内存里的重复数据。 - 避免循环调用
setOption频繁重建图表,优先使用appendData增量更新。 - Canvas 作为默认渲染器,在多数场景性能优于 SVG。
- 组件销毁时务必
dispose,取消所有事件和定时器。 - 连接多个 chart 时留意联动事件的广播成本,避免雪崩。
参考链接
- [1] https://echarts.apache.org/zh/api.html#echarts.init
- [2] https://echarts.apache.org/zh/api.html#echartsInstance.resize
- [3] https://echarts.apache.org/zh/api.html#echarts.registerTheme
- [4] https://echarts.apache.org/zh/option.html
- [5] https://echarts.apache.org/zh/api.html#echartsInstance.dispatchAction
- [6] https://echarts.apache.org/zh/option.html#series-line.large
- [7] https://echarts.apache.org/zh/option.html#series-line.sampling
- [8] https://echarts.apache.org/zh/option.html#series-line.progressive
- [9] https://echarts.apache.org/zh/handbook/concepts/performance/
- [10] https://echarts.apache.org/zh/api.html#echartsInstance.appendData
- [11] https://echarts.apache.org/zh/api.html#echartsInstance.clear
- [12] https://echarts.apache.org/zh/api.html#echartsInstance.dispose
