1. 会画一张折线图,和能交付一块可视化大屏,完全是两件事
很多人第一次接触ECharts都是被它的入门示例"骗"进来的:一个 div、一个 option、一行 setOption,折线图就出来了,确实比手写 canvas 舒服太多。但等到接手真正的项目——比如做一块可视化大屏,你会发现真正让人加班的不是"怎么画出图",而是"为什么图是扁的""为什么换台电脑字号全乱""为什么切了三次 Tab 之后页面开始卡"。配置项参数有几百个,官方文档写得也清楚,但文档解决的是"这个参数干什么用",解决不了"我的场景该用哪个参数、为什么"。
这篇文章想干的事,是把 ECharts 从"能画图"推到"能交付"这段路补齐。前半部分我会按图表渲染的真实顺序,把 option 的骨架、坐标系四件套、轴刻度控制、tooltip 富文本这些配置项一个一个拆开讲清楚它们为什么存在;后半部分会完整走一遍可视化大屏的落地流程,包含布局适配、地图注册、实时刷新节奏、部署前自查。适合两类人:刚学完基础图表想往上走一步的前端,以及被大屏适配和性能问题反复折磨、想找一份系统参照的同学。文中所有参数都以 ECharts 5.x 为准,如果你还在 4.x,个别地方我会指出差异。
有一点先说在前面:ECharts 的学习成本不在 API 数量,而在"约束条件"——容器的尺寸约束、渲染器的能力约束、浏览器重绘时机的约束。把这三条约束想明白,剩下的配置项都是填空题。
2. 初始化阶段就该定下来的三件事:容器、渲染器、实例
2.1 容器宽高为 0,是新手第一大坑
ECharts 初始化时会读取容器clientWidth和clientHeight作为画布基准。如果此时容器还没渲染出来,或者被display: none、visibility: hidden隐藏着,读到的就是 0,ECharts 会退而使用默认的 100×100,于是你看到一条被压扁的细线,或者整个图缩在左上角一小块。这个现象在弹窗、Tab 切换、折叠面板里出现率极高。
正确的处理思路分两层。第一层是容器本身必须有确定的尺寸来源,最省事的写法是给图表容器显式写死宽高:
.chart-box { width: 100%; height: 320px; min-height: 200px; }注意height必须是有值的。父级用flex: 1撑起来的时候,记得给父级加min-height: 0,否则 flex 子项在内容溢出时会拒绝收缩,图表会越撑越高。
第二层是初始化时机。弹窗里的图表不要跟着弹窗一起创建,而是在弹窗打开、nextTick之后、确认容器已经有实际宽高再init:
// Vue 3 场景 const openDialog = async () => { visible.value = true; await nextTick(); // 此时 DOM 已插入,尺寸可读 chart = echarts.init(chartRef.value); chart.setOption(option); };如果容器在初始化时确实不可见,也有一个补救手段:先init再在可见后调用chart.resize(),ECharts 会重新读取容器尺寸并重算布局。但要注意,resize()只重算布局,不会改变你已经写死在 option 里的字号——这一点后面第 5 章会重点展开。
2.2 渲染器选 canvas 还是 svg
echarts.init(dom, theme, { renderer: 'canvas' })这个第三个参数里,renderer是第一个要做决策的配置。默认是canvas,绝大多数场景够用,尤其是数据点很多的时候,canvas 的绘制性能优势明显。
什么时候该换svg?我总结了三类:图表数量多但每个图数据量很小(比如一屏十几个仪表盘卡片),需要给图表元素加 CSS 交互或者导出矢量图,以及需要在服务端做无头渲染导出图片。svg 渲染出来的 DOM 节点多,几千个数据点就开始卡了,所以数据密集型场景别碰。
顺带说一个容易被忽略的参数devicePixelRatio。ECharts 5 默认会自动读取window.devicePixelRatio,在 Retina 屏上文字会清晰。但如果你在跨屏拖动的场景下发现图表变模糊,可以手动传一个固定值,代价是内存占用会上去。
2.3 实例管理:别在同一块 DOM 上反复 init
echarts.init在同一个 DOM 上调用多次,是内存泄漏的经典来源。ECharts 5 会给出警告:"There is a chart instance already initialized on the dom.",但它不会帮你销毁旧实例,被丢弃的那个实例持有的 canvas、事件监听、定时器都还在。
稳妥的写法是统一用一个获取实例的函数兜底:
function getChart(dom) { let instance = echarts.getInstanceByDom(dom); if (!instance) { instance = echarts.init(dom); } return instance; }组件卸载时必须dispose(),而不是只把变量置空:
onBeforeUnmount(() => { chart?.dispose(); chart = null; });这里有个经验值:如果页面里图表是长期驻留的,dispose之外还要清掉自己挂的ResizeObserver和setInterval,ECharts 不管这些。我见过一个项目,切一次路由泄漏一个 200KB 的 canvas,切二十次之后浏览器直接掉帧,排查了半天才定位到是忘了断开观察器。
3. option 配置项拆解:把骨架背下来比记参数有用
3.1 坐标系系的四件套:grid、xAxis、yAxis、series
直角坐标系图表的一切布局问题,都落在这四个配置上。grid决定绘图区在容器里占哪一块,xAxis/yAxis决定轴的位置和刻度,series决定数据怎么画。新手最常抱怨的"标签被切掉""图例压住图",八成是grid没设。
option = { grid: { left: 48, // 左侧留白,给 y 轴名称和刻度值 right: 24, top: 60, // 顶部留白,给标题和图例 bottom: 40, // 底部留白,给 x 轴名称 containLabel: false }, xAxis: { type: 'category', data: [] }, yAxis: { type: 'value' }, series: [] }containLabel这个参数值得单独说。当它为true时,grid的left/bottom会被解释成"刻度标签之外还要留这么多",ECharts 会自动把标签空间算进去。所以不确定 y 轴数值有几位小数的时候,把它设成true更保险。但代价是:如果你同时用left: '10%'这种百分比,结果会和你预期的不一致,因为百分比是相对容器算的,再叠加标签宽度,布局会飘。我的习惯是数值型grid配containLabel: true,百分比型grid配containLabel: false,两套别混。
yAxis.min/max/interval也是一组容易被忽略的参数。默认情况下 ECharts 会算一个"好看"的范围,但业务上经常要求 y 轴从 0 开始,或者刻度必须是整数。数据是人数、订单量这种离散量时,务必显式设minInterval: 1,否则会出现 0.5 个人的刻度。
3.2 x 轴刻度挤在一起:interval、rotate 和 hideOverlap 的取舍
类目轴标签太多是高频问题,一年 365 天、一小时 60 个点,标签必然叠字。可选方案有四种,适用场景不同:
| 方案 | 配置 | 适用场景 | 副作用 |
|---|---|---|---|
| 自动隐藏 | axisLabel.interval: 'auto' | 默认行为,数据量中等 | 隐藏规则不可控 |
| 固定间隔 | axisLabel.interval: 3 | 需要每隔固定数量显示 | 末尾标签可能被截 |
| 倾斜 | axisLabel.rotate: 45 | 标签文字较长(如日期) | 占高度,移动端挤 |
| 溢出隐藏 | axisLabel.hideOverlap: true | 类目多且不想倾斜 | 只隐藏不省略 |
rotate的取值范围是 -90 到 90,写 45 时文字会向右上倾斜,配合grid.bottom加大才有空间。我的经验是:屏幕宽度大于 1200px 时优先用interval控制数量,小于 768px 时干脆用axisLabel.formatter把标签缩短(比如2024-06-01显示成06-01),比倾斜好看得多。
formatter支持字符串模板和回调函数两种写法。回调写法更灵活:
xAxis: { type: 'category', axisLabel: { formatter: (value) => value.length > 6 ? value.slice(5) : value } }注意回调返回的值会被当作纯文本渲染,想加样式得返回富文本对象,或者改用rich配置。这一点和 tooltip 不一样,别搞混。
3.3 tooltip 换行、溢出与富文本的实战写法
tooltip.formatter返回的是 HTML 字符串,所以换行直接用<br/>就行。但"自动换行"是另一回事——你需要给 tooltip 限宽,并让 CSS 允许折行:
tooltip: { trigger: 'axis', confine: true, // 限制在图表容器内,防止溢出屏幕 enterable: false, // 鼠标能否进入 tooltip 内部 extraCssText: 'max-width: 280px; white-space: normal; word-break: break-all;', formatter: (params) => { const list = Array.isArray(params) ? params : [params]; return list.map(p => `${p.marker}${p.seriesName}: <b>${p.value}</b>` ).join('<br/>'); } }这里三个点是踩过坑才总结出来的。第一,extraCssText里如果不写white-space: normal,ECharts 默认给 tooltip 加的是white-space: nowrap,你的max-width只会把内容裁掉而不是换行。第二,confine: true在边缘位置非常有用,否则鼠标移到最右侧数据点时 tooltip 会顶出屏幕外,用户看不到。第三,formatter里用p.marker能拿到和当前系列颜色一致的小圆点标记,比自己写<span>拼色块省事。
数据点数量多的时候还有一个细节:axisPointer的type建议显式设为'line'或'shadow'。默认值会跟随trigger变化,在多系列折线图上不设的话,指示线有时候会被阴影盖住。
3.4 主题抽取:把颜色和字号从 option 里赶出去
同一块大屏几十个图表,如果每个 option 里都散落着#1f7ee8、#4fd1c5这样的颜色值,改一次配色要改几十处。正确做法是做一份主题对象,用echarts.registerTheme注册:
const theme = { color: ['#5b8ff9', '#61ddaa', '#65789b', '#f6bd16', '#7262fd'], textStyle: { fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif' }, categoryAxis: { axisLine: { lineStyle: { color: '#2c3e5a' } } } }; echarts.registerTheme('darkBlue', theme); const chart = echarts.init(dom, 'darkBlue');好处不只是好维护。registerTheme注册的主题会被缓存,同一块大屏的所有图表共用一份,内存上也更友好。另外textStyle.fontFamily在大屏上要特别注意:投屏设备不一定装了你本机的字体,中文字体建议按"苹方 → 微软雅黑 → 系统默认"的顺序降级,别只写一个。
4. 折线、柱状、饼图三类高频图的参数级实操
4.1 折线图:数据点过多时的两种降采样策略
一个传感器每秒上报一次,一天下来 86400 个点直接扔给折线图,结果就是浏览器卡死、线条糊成一团。ECharts 提供了开箱即用的降采样能力:
series: [{ type: 'line', sampling: 'lttb', // 或 'average' / 'max' / 'min' / 'sum' large: false, smooth: false, showSymbol: false, // 数据点超过 100 个就该关掉 lineStyle: { width: 1.5 }, areaStyle: { opacity: 0.12 } }]sampling: 'lttb'是最常用的,它按"最大三角形三桶"算法保留视觉上最关键的拐点,视觉失真最小。'average'在温度、湿度这类需要看均值的场景更合适。showSymbol一定要关,否则每个点都画圆点,性能直接崩。areaStyle的opacity别开太高,多个系列堆叠时面积会互相遮挡。
还有一个联动配置值得一提:tooltip.trigger用'axis'的时候,axisPointer.snap控制鼠标是否吸附到最近的数据点。移动端用手指操作时,吸附比精确 hover 友好得多。
4.2 柱状图用自定义图片显示柱子:两条技术路线
"柱子能不能用图片显示",这个需求在业务大屏里特别多——比如用小人图标表示人数、用树木图标表示林地面积。ECharts 有两条完全不同的路线,很多人一开始就走错了。
第一条是pictorialBar(象形柱图),适合"用图标重复堆叠表示数量"的场景:
series: [{ type: 'pictorialBar', symbol: 'image://./assets/tree.png', symbolRepeat: true, // 图标重复填充 symbolSize: [24, 24], symbolMargin: 2, symbolBoundingData: 100, // 以 100 为满格基准 data: [42, 68, 91] }]这条路线的关键参数是symbolRepeat、symbolSize和symbolBoundingData三个协作:symbolBoundingData决定"多少个图标算满",symbolSize决定单个图标多大,symbolRepeat: true让图标沿柱子方向平铺。调不好会出现图标被拉变形,那是因为symbolSize的第一个值设得比第二个值大太多,图标会按比例缩放失真。
第二条是bar + 图案填充,适合"整根柱子贴一张纹理图"的场景:
series: [{ type: 'bar', itemStyle: { color: { image: document.getElementById('bgTexture'), repeat: 'repeat-x' } } }]image接受图片元素、Image 对象或者图片 URL(URL 形式需要图片已加载)。repeat支持'repeat'、'repeat-x'、'repeat-y'、'no-repeat'四种。要注意的是,图片是异步加载的,如果图片没加载完就setOption,柱子会渲染成纯色。稳妥做法是先把图片new Image()加载完,onload之后再 setOption。
顺带提一个 3D 柱状图。搜索里经常出现"3D 柱状图"的需求,但bar3D必须配合echarts-gl扩展包,独立引入后才有type: 'bar3D'。它的体积不小(几百 KB),如果只是想要"看起来有立体感",用bar加渐变色的itemStyle.color配一个深色顶面,视觉上够用,成本低得多。这是我个人很推荐的一条取巧路线。
4.3 饼图 labelLine 末端小圆点偏移:成因与两种修法
饼图的标签线分两段:labelLine.length是从扇区边缘算起的第一段长度,labelLine.length2是第二段水平段的长度,标签文字最终贴在第二段末端。很多教程教你在label.formatter前面手动加一个'●'来做小圆点,然后就出现了"小圆点位置不对/跟着容器缩放后偏移"的问题。
成因有两个。一是label.align和labelLine.length2的配合关系:默认 ECharts 会根据扇区在左半圈还是右半圈自动决定align是'left'还是'right',但如果你手动写了align: 'left',右侧的标签就会出现圆点和文字重叠,看起来像是圆点"跑"了。二是padding的影响,label.padding是相对文字块算的,圆点是文字块的一部分,padding 一变大,圆点就被推远了。
第一种修法是老老实实把对齐交给 ECharts 自动处理,只控制长度:
labelLine: { show: true, length: 12, length2: 16, minTurnAngle: 100, // 转折角太小就不画折线,避免线条打结 smooth: false }minTurnAngle默认 90,扇区很小的时候折线会拐成一个锐角,看起来很脏,调到 120 到 150 之间能明显改善。这个参数知道的人不多,但效果立竿见影。
第二种修法是干脆不用字符做圆点,改用rich富文本,让圆点成为一个有明确尺寸的样式块:
label: { formatter: '{dot|●} {name|{b}}\n{val|{d}%}', rich: { dot: { fontSize: 10, lineHeight: 18, color: 'inherit' }, name: { fontSize: 12, lineHeight: 18, color: '#c9d8f0' }, val: { fontSize: 12, lineHeight: 18, color: '#ffffff', fontWeight: 'bold' } } }富文本的好处是圆点大小、行高、对齐都受你控制,容器缩放时不会因为字体渲染差异导致错位。代价是写起来啰嗦,但饼图一般也就一两个,值。
5. 可视化大屏适配:pxtorem 为什么对 ECharts 不起作用
5.1 根因不在插件,在渲染链路
这是我在群里被问得最多的问题之一:项目里配了postcss-pxtorem,页面上所有元素的尺寸都能跟着屏幕缩放,唯独 ECharts 里的文字、图例、内边距纹丝不动。
根因其实很简单。postcss-pxtorem是一个构建期的 CSS 转换工具,它扫描的是.css、.scss、.vue文件里<style>块中的px字面量,把font-size: 14px改写成font-size: 0.875rem。而 ECharts 的文字不是 CSS 渲染的——它在 canvas 里用fillText绘制,字号来自 JavaScript 对象里的数值fontSize: 14。这个14是一段 JS 代码,插件根本不认识它,也没有能力去改它。
所以正确的做法不是"让 pxtorem 管到 ECharts",而是"在 JS 里自己做一次换算"。核心就一个函数:
// 设计稿宽度 1920,1rem = 100px 的基准 const BASE_WIDTH = 1920; const REM_BASE = 100; function px2rem(px) { const scale = document.documentElement.clientWidth / BASE_WIDTH; return (px * scale) / REM_BASE; } // 使用 const option = { textStyle: { fontSize: px2rem(14) } // 直接算成 rem 数值喂给 ECharts };注意这里返回的是"rem 数值",因为document.documentElement的font-size已经被你的 rem 适配脚本设成了REM_BASE * scale对应值。ECharts 的fontSize单位会被解释成和 canvas 一致的坐标单位,所以直接喂rem数值是可以的——但这条链路很绕,字体大小是能跟上的,而grid的padding、symbolSize这些就没那么直观了,稍不留神就漏掉几处。
5.2 三种适配方案的实测对比
踩过几轮之后,我把常见方案整理成这张表,判断标准是"改动成本"和"是否容易漏":
| 方案 | 原理 | 改动成本 | 漏改风险 | 推荐场景 |
|---|---|---|---|---|
| JS 逐项换算 | 写 px2rem 函数,逐个属性调用 | 高,容易漏 | 高 | 图表数量少 |
| 整体 scale 缩放 | 容器transform: scale(k) | 低 | 极低 | 大屏固定分辨率投屏 |
| vw/vh 单位 | option 里直接写 vw 字符串 | 中 | 中 | 需要响应式的网页端 |
transform: scale 是我个人最推荐的方案,尤其适合"一块 1920×1080 的设计稿投到大屏上"这种需求。做法是外层套一个固定 1920×1080 的容器,所有图表按设计稿的绝对尺寸来写,然后在最外层根据实际窗口尺寸算一个缩放比例:
function resizeStage() { const stage = document.getElementById('stage'); const scaleX = window.innerWidth / 1920; const scaleY = window.innerHeight / 1080; const scale = Math.min(scaleX, scaleY); // 等比,留黑边 stage.style.transform = `scale(${scale})`; stage.style.transformOrigin = 'left top'; // 关键:缩放后居中,需要补偏移 stage.style.left = `${(window.innerWidth - 1920 * scale) / 2}px`; stage.style.top = `${(window.innerHeight - 1080 * scale) / 2}px`; }这个方案的巨大优势是:ECharts 内部的字体、间距、图例全都跟着一起缩放,你一行 ECharts 配置都不用改。代价是缩放后 canvas 是位图放大,会略微模糊。解决办法是把devicePixelRatio传大一点:
echarts.init(dom, theme, { devicePixelRatio: window.devicePixelRatio * 1.5 });实测下来,1.5 倍已经足够清晰,2 倍以上内存消耗得不偿失。另外如果不想留黑边,可以用scaleX和scaleY分别缩放,但那样圆形图表会被拉成椭圆,饼图尤其明显,一般不推荐。
5.3 resize 监听写错会带来什么
大屏上最常见的性能问题,是window.resize里直接调chart.resize(),没有节流。用户拖动窗口时浏览器每帧都在派发 resize 事件,几十个图表同时重算布局、重绘 canvas,画面直接卡成幻灯片。
正确的写法有两层保护。第一层是防抖:
let timer = null; function onResize() { clearTimeout(timer); timer = setTimeout(() => { chart?.resize({ animation: { duration: 200 } }); }, 120); }第二层是用ResizeObserver替代window.resize。大屏里经常有侧边栏折叠、栅格布局拖拽这类操作,容器尺寸变了但窗口尺寸没变,window.resize根本不会触发。ResizeObserver监听的是元素本身:
const ro = new ResizeObserver(() => onResize()); ro.observe(chartDom); // 卸载时 // ro.disconnect();resize()的animation参数也值得说一下。不传的话 resize 是瞬时完成的,视觉上会有跳变;传一个 200ms 的过渡,缩放过程会平滑很多,投屏时观感差别很明显。
6. 大屏案例:一块森林防火监测屏从数据到落地
6.1 先定信息层级,再谈图表选型
森林防火监测屏的典型业务是:展示林区分布、火险等级、实时监测点状态、告警趋势。很多人一上来就想着"中间放个地图好看",然后把各种图表塞满边角,结果重点信息被淹没。
我的做法是先画一张信息层级表,把"必须一眼看到"的信息排到第一屏中心,其余降级:
| 层级 | 信息内容 | 图表形式 | 位置 |
|---|---|---|---|
| 一级 | 林区整体分布、火点位置 | 地图 + 散点 + 飞线 | 中央,占 60% 宽 |
| 二级 | 火险等级占比 | 环形饼图 | 左上 |
| 三级 | 24 小时温湿度趋势 | 双轴折线图 | 左下 |
| 四级 | 各监测站数据 | 滚动列表 | 右侧栏 |
| 五级 | 告警条数统计 | 象形柱图 | 底部 |
选型上有个原则:能用一个图说清楚的别拆成两个。比如温度用折线、湿度用柱状本来是两个图,用双 y 轴叠在一个图里,视觉关联性反而更强——因为温湿度本来就有相关性,看峰值是否同时出现是业务上真实的需求。
6.2 地图 + 散点 + 飞线的联动写法
ECharts 5 之后,地图数据不再内置,必须自己注册。这是升级后"地图变空白"的根本原因:
import * as echarts from 'echarts'; import geoJson from './geo/region.json'; echarts.registerMap('region', geoJson); const option = { geo: { map: 'region', roam: false, label: { show: false }, itemStyle: { areaColor: '#132a4a', borderColor: '#2f5f96', borderWidth: 1 }, emphasis: { itemStyle: { areaColor: '#1d4272' }, label: { show: true, color: '#fff' } } }, series: [ { type: 'scatter', coordinateSystem: 'geo', symbolSize: (val) => Math.max(6, val[2] / 8), data: stationList, // [[lng, lat, value], ...] itemStyle: { color: '#ff7a45' } }, { type: 'lines', coordinateSystem: 'geo', effect: { show: true, period: 4, trailLength: 0.35, symbolSize: 5 }, lineStyle: { color: '#4fd1c5', width: 1, curveness: 0.25 }, data: flyLines } ] };几个实操细节。第一,geo和series.map二选一即可,不要同时开,否则会渲染两层地图,性能浪费。第二,symbolSize用回调按数值动态算大小是地图散点的标配,但要设下限,否则数值小的点会小到看不见。第三,lines的effect.trailLength别设太大,0.35 到 0.5 之间比较自然,设成 1 就变成一条静止的粗线了。第四,地图底图数据一定从正规渠道获取,并确认其使用许可范围,涉及边界表达的数据不要随意替换来源,避免出现表达不准确的问题。
飞线还有个隐藏坑:lines系列在数据量大(超过 200 条)的时候,动画会明显掉帧。解决办法是给effect加constantSpeed,牺牲一点视觉一致性换帧率稳定。
6.3 实时刷新的节奏控制
监测类大屏必然要定时刷新。最常见的错误写法是每秒重新setOption(完整option),结果整个图表从头重绘、动画重播,画面像在抽搐。
ECharts 的setOption第二个参数决定了合并策略,这个参数非常关键:
// 只更新数据,保留其他配置和动画状态 chart.setOption({ series: [{ data: newData }] }, { lazyUpdate: true }); // 需要替换整个系列数量时 chart.setOption(newOption, { notMerge: true });日常刷新数据用第一种,不带notMerge,ECharts 会做增量合并,动画是平滑过渡的。只有系列数量本身变了才用notMerge: true,它会销毁重建,动画会重播。
刷新间隔建议按数据源的真实频率来定,不要硬写 1000ms。传感器 30 秒上报一次,你每秒去拉一次接口,99% 的请求是重复数据。我一般的做法是配置一个refreshInterval常量,配合接口的缓存头一起用,减少无谓的网络请求。另外记得在页面visibilitychange时暂停刷新——投屏切到后台的时候还在跑定时器,纯属浪费。
6.4 投屏前一晚的自查清单
大屏项目上线前我固定跑一份检查清单,都是血泪教训换来的:
- 断网状态下页面能否正常渲染(本地 mock 兜底是否生效)
- 接口超时是否会导致图表空白(要有 loading 态和空数据占位)
- 容器尺寸从 1920 缩到 1366 再拉回,有没有错位残留
- 图表字号在投屏设备上是否可读(远距离看,14px 肯定不够,正文至少 16px)
- 长时间运行 24 小时后内存是否稳定(打开任务管理器观察)
- 深色背景下的网格线对比度是否足够(很多设计稿在显示器上没问题,投到大屏全糊了)
- 所有图表的
tooltip是否配置了confine: true
最后一条看似小,但演示时鼠标滑到边缘卡片上 tooltip 飞出屏幕,是很尴尬的事故。
7. 从能跑到好维护:工程化和性能上的几条经验
7.1 按需引入能把体积砍掉一半以上
全量import * as echarts from 'echarts'在开发时很爽,打包出来主包轻松 900KB+。按需引入的写法:
import * as echarts from 'echarts/core'; import { LineChart, BarChart, PieChart, ScatterChart, LinesChart } from 'echarts/charts'; import { GridComponent, TooltipComponent, LegendComponent, TitleComponent, GeoComponent, VisualMapComponent } from 'echarts/components'; import { CanvasRenderer } from 'echarts/renderers'; echarts.use([ LineChart, BarChart, PieChart, ScatterChart, LinesChart, GridComponent, TooltipComponent, LegendComponent, TitleComponent, GeoComponent, VisualMapComponent, CanvasRenderer ]);注意组件要和图表配套引。用bar3D必须引Grid3DComponent和echarts-gl,用dataZoom必须引DataZoomComponent,漏了会在运行时报"Component xxx not exists"。我的习惯是在项目里封一个echarts.js统一出口,所有图表从这里引,避免各处重复引、也避免漏引。
7.2 大数据量下的三档性能策略
数据量决定了你要打开哪些开关,我一般按三档处理:
| 数据点规模 | 关键配置 | 预期帧率 |
|---|---|---|
| < 1000 | 默认配置 | 流畅 |
| 1000 - 10000 | sampling: 'lttb'、showSymbol: false | 流畅 |
| > 10000 | 加large: true、animation: false、progressive: 2000 | 可接受 |
large: true会启用大数据量优化模式,代价是失去一些视觉效果(比如 hover 高亮可能不生效)。progressive: 2000表示分片渲染,每片 2000 个图形,避免一次性绘制卡住主线程。但要注意,large模式下tooltip的trigger: 'item'有时会失效,需要改成'axis'。
还有一条很多人不知道的:animationThreshold默认是 2000,超过这个数据量的图表会自动关闭动画。如果数据量在 1800 左右,动画还开着,但每次刷新都在做动画计算,反而比彻底关掉更卡。这种临界值场景,手动设animation: false更稳。
7.3 定时器和观察器的生命周期管理
组件化项目里最容易出问题的不是 ECharts 本身,而是围绕它的那些"外围资源":setInterval、ResizeObserver、IntersectionObserver、事件监听。ECharts 不管这些,你得自己管。
我的做法是在组件里维护一个cleanupList数组,每个监听注册时把注销函数推进去,卸载时统一执行:
const cleanups = []; const timer = setInterval(fetchData, 30000); cleanups.push(() => clearInterval(timer)); const ro = new ResizeObserver(onResize); ro.observe(chartDom); cleanups.push(() => ro.disconnect()); onBeforeUnmount(() => { cleanups.forEach(fn => fn()); cleanups.length = 0; chart?.dispose(); });这个模式看着朴素,但它是唯一能保证"新增一个监听时不会忘记注销"的写法。我在一个后台项目里推广之后,长时间驻留页面的内存曲线从持续爬升变成了平稳波动,效果非常直观。
另外补一个和地图有关的经验:如果项目里注册了多个地图(省、市、区),registerMap的 key 一定要做命名空间隔离,比如region-a-city。用同名 key 注册两次,后一次会覆盖前一次,而且不会有任何警告,线上排查起来极其痛苦。
8. 一些零散但真金白银的经验
做到这里,前面那些配置项其实已经够支撑大部分项目了。最后补几个零碎的、文档里不太写但很实用的点。
第一个是导出图片。chart.getDataURL({ pixelRatio: 2, backgroundColor: '#0b1a2e' })能拿到 base64,但注意backgroundColor必须显式传,否则导出的图背景是透明的,粘到 Word 里就是一坨黑字。大屏截图同理,深色主题下背景不传会很难看。
第二个是空数据的处理。接口挂了或者筛选后没有数据,ECharts 默认是空白一片,用户以为页面坏了。title里可以配一个"无数据"的副标题,或者用graphic元素画一段提示文字:
graphic: { type: 'text', left: 'center', top: 'middle', style: { text: '暂无数据', fill: '#5a7391', fontSize: 16 }, silent: true }配合数据判断动态设置graphic.show,体验会好很多。
第三个是关于打印和 PDF 导出。浏览器打印是走 CSS 渲染的,canvas 里的内容会被打印出来,但字体和颜色可能失真,深色背景默认不会打印(浏览器省墨策略)。如果有报表打印需求,建议给打印单独准备一套浅色 option。
第四个是调试技巧。chart.getOption()能把当前完整配置打出来,排查"我明明设了参数为什么没效果"特别有用——很多时候是setOption的合并策略把你后面的设置覆盖了,看一遍完整 option 比猜快得多。另外浏览器的 canvas 性能面板能看到每一帧的绘制耗时,超过 16ms 就是掉帧了,这个比凭感觉判断靠谱。
我个人在实际操作中的体会是,ECharts 的配置项看着多,但真正每天用的就那么二三十个,剩下的都是特定场景的补救手段。与其背文档,不如养两个习惯:一是每次调完样式都调一次getOption()对照,确认真生效了;二是每个新项目开工前先把容器尺寸、适配方案、实例管理、颜色主题这四件事定下来,后面就是填数据。这四条定得早,能省掉的返工时间比学十个冷门参数加起来都多。