简介:ECharts 3.8.5 是百度开源数据可视化库的稳定版本,这份压缩包面向需要快速上手图表开发的前端工程师,解决从配置到交互的系列问题。包内共 3 个文件,包含两个 JavaScript 库文件与一个 RAR 格式的 docs 文档包,整体大小 15.33MB,既可引用主库开发,也能离线查阅图表类型、配置项及 API 接口。已有 461 人学习下载,适合初学者系统学习,也适合有经验者对比 3.x 与 4.x 的版本差异。3.8.5 主库支持柱状图、折线图、饼图、散点图和地图等多种图表,具备悬停高亮、点击事件、缩放等交互能力;附带的 4.0.4 文件则呈现了性能优化与新特性。docs 文档还包含从实例创建到更新图表、事件处理的完整指引,可帮助快速定位样式配置和数据绑定的写法,减少试错成本。
1. echart3.8.5:老项目里最让人省心的图表版本
echart3.8.5 是 ECharts 3.x 分支里相当稳定的一版,也是很多存量系统的 package.json 和 vendor 目录里躺着的那份版本。它没有 4.x、5.x 那么多新特性,但胜在体积可控、配置模型成熟、对旧浏览器兼容度高:在需要支持 IE9 级别的内网环境,或者不能轻易升级构建链路的项目里,3.8.5 是被反复验证过的选择。这篇笔记从最基础的引入开始,把一个折线图跑起来,再覆盖柱状图、饼图、时序缩放三种高频场景的 option 写法,最后把容易踩的坑逐个摊开。适合接手老项目、需要在受限环境做可视化的人。
2. 把 echart3.8.5 拉到项目里:三种引入方式和第一个折线图
2.1 无构建项目的引入方式
老项目最常见的是在 index.html 里用 script 标签引入echarts.min.js。3.8.5 的全局包把 echarts 挂在 window 上,不需要任何模块化工具,直接拿echarts对象就能干活。内网环境如果没法访问公网 CDN,就把文件拷到本地 lib 目录,路径指向相对路径即可。
<script src="./lib/echarts.min.js"></script> <script> var chart = echarts.init(document.getElementById('chart')); </script>这段代码的逻辑没什么特别,但要注意两点。一是 script 的加载顺序,echarts.min.js必须在业务脚本之前,否则拿不到echarts对象;二是echarts.init的调用时机,如果脚本放在 head 里且没有包window.onload,此时 DOM 还没解析完,getElementById会拿到 null,init 直接报错。稳妥做法是把脚本放在 body 末尾,或者监听 DOMContentLoaded。
如果项目用了 npm,安装命令是npm install echarts@3.8.5,然后在入口文件里import echarts from 'echarts'。这个 import 默认拿的是 dist/echarts.js 完整包,包含全部图表类型和组件,对于只想画一两个图的小页面来说偏大。关注体积的话可以走按需引入,具体写法放在最后一章。
2.2 容器尺寸与 init 的三个参数
在 3.8.5 里,图表容器必须要有明确宽度和高度。一个常见的反直觉点是:容器设了宽度但没设高度,或者父级用了display:none,init 不报错,但图表画出来是一块空白。原因是 init 的时候 zrender 会读取容器的 clientWidth 和 clientHeight,取到 0 就按 0 尺寸初始化。
var dom = document.getElementById('chart'); var chart = echarts.init(dom, null, { renderer: 'canvas', devicePixelRatio: window.devicePixelRatio || 1 });init 的第二个参数是主题,这里传 null 表示用默认主题,也可以传主题名字或主题对象。第三个参数是可选选项,renderer决定用 canvas 还是 svg 渲染,devicePixelRatio控制高分屏下的清晰度。3.x 支持canvas和svg两种渲染器,canvas 是默认值,大数据量场景下性能更好;svg 适合导出矢量图、或者图表数量少且对清晰度要求高的场景。
容器尺寸的常见做法是给一个固定定位的外层,比如大屏项目通常用position: absolute; left: 0; right: 0; top: 0; bottom: 0,或者 flex 布局的子元素加flex: 1和height: 100%。关键是让容器在 init 时能取到非零尺寸。如果确实需要在隐藏容器里预初始化,等容器显示之后必须调用chart.resize()重新测量。
2.3 第一个折线图:option 最小可运行结构
3.8.5 的配置模型已经非常成熟,全部内容都在一个 option 对象里。下面这份配置是折线图的最小可运行结构,画一条近七日访问量的曲线:
var option = { title: { text: '近七日访问量' }, tooltip: { trigger: 'axis' }, legend: { data: ['UV'] }, xAxis: { type: 'category', data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] }, yAxis: { type: 'value' }, series: [{ name: 'UV', type: 'line', data: [820, 932, 901, 934, 1290, 1330, 1320] }] }; chart.setOption(option);逐项说几个关键点。xAxis 的type: 'category'表示横轴是离散类目,data是类目列表;yAxis 的type: 'value'表示纵轴是数值轴,由 ECharts 自动计算 min/max。series 里的type: 'line'决定图形是折线,data顺序对应 xAxis 的类目。legend 的 data 数组必须包含 series 的 name 才会显示对应图例,名字对不上图例就会空白。tooltip 的trigger: 'axis'表示鼠标悬停在坐标轴附近时显示该垂直位置上所有系列的数据,这个在折线图里比trigger: 'item'更常用。
chart.setOption(option)在这里是首次渲染。这个 API 在 3.8.5 里的默认行为是「合并」而不是「替换」,这个语义差异会在数据更新时带来很多坑,第四章专门讲。
3. 三个高频场景的 option 落地:堆叠柱、环形饼图与时序缩放
3.1 堆叠柱状图:stack 分组与双轴对齐
柱状图在 3.8.5 里最常用的变体是堆叠柱。多个 series 只要把stack字段设成同一个字符串,就会堆叠在一个柱子上:
series: [ { name: 'A组', type: 'bar', stack: '总量', data: [5, 20, 36, 10] }, { name: 'B组', type: 'bar', stack: '总量', data: [10, 12, 8, 20] } ]stack: '总量'是分组的 key,A 组和 B 组堆在一起形成一根柱子;如果又来了 C 组且 stack 写的不是'总量',C 组就会另起一组柱子。堆叠时建议把 legend 的 data 写全,否则切换图例时堆叠关系会显得奇怪。
双 y 轴是另一个高频需求,比如左边画销售额、右边画增长率。核心是给两个数值轴分别定义 yAxis,再在 series 里用yAxisIndex指定归属:
yAxis: [ { type: 'value', name: '销售额', position: 'left' }, { type: 'value', name: '增长率', position: 'right', min: 0, max: 100 } ], series: [ { name: '销售额', type: 'bar', yAxisIndex: 0, data: [1200, 1400, 1600] }, { name: '增长率', type: 'line', yAxisIndex: 1, data: [23, 45, 67] } ]这里最容易翻车的地方是忘记写yAxisIndex。默认情况下所有 series 都挂在第一个 yAxis 上,第二条线如果数值量级不同,会被压成一条贴着底部的直线。另一个坑是右侧轴的 min/max 没有显式设置,导致它在数据量变化时自动缩放,两条曲线看起来永远不在同一视觉坐标里。做双轴时建议把两个轴的min、max都钉死已知范围,或者至少固定右侧轴。
3.2 环形饼图:半径、引导线与极小扇区
饼图在 3.8.5 里的配置要点和 5.x 差异不大,但有几个参数在老版本里容易被忽略。环形效果用radius的数组形式实现,第一个值是内半径,第二个是外半径,单位可以是像素也可用百分比:
series: [{ type: 'pie', name: '流量来源', radius: ['42%', '68%'], center: ['50%', '50%'], minAngle: 3, label: { show: true, formatter: '{b}: {c} ({d}%)' }, labelLine: { length: 12, length2: 24 }, data: [ { name: '直接访问', value: 335 }, { name: '搜索引擎', value: 410 }, { name: '外部链接', value: 234 } ] }]radius: ['42%', '68%']做出的是环,不是实心饼,大屏可视化里环形的视觉负担更小。center控制饼图圆心位置,默认就是 50% 50%,可以不显式写。minAngle: 3的作用是给最小扇区一个角度下限,防止某个占比极小的数据在图上缩成一条细线。label.formatter里{b}是名称、{c}是数值、{d}是百分比,顺序和写法容易记混,建议写完后在页面上 hover 一下确认。
环形饼图一个比较隐蔽的坑是图例和 label 文字重叠。占比大、名称又长的项会把 label 撑得很宽,盖住旁边的扇区。处理办法是给labelLine设置length和length2,把引导线拉长一些,让标签离图远一点;或者在 option 里设置label: { overflow: 'truncate', width: 40 }强制截断。3.8.5 对 overflow 的支持比 4.x 弱一些,遇到截断不生效时,直接在数据里把 name 改短是最省事的解法。
3.3 时序图:dataZoom 缩放与 tooltip 联动
带时间轴的图表是监控大屏的主角。xAxis 用type: 'time',数据里直接传时间戳或日期字符串,ECharts 会自动处理坐标刻度。配合dataZoom组件可以实现在同一个图表内拖动看局部细节:
xAxis: { type: 'time', name: '时间' }, yAxis: { type: 'value', name: 'CPU使用率(%)', min: 0, max: 100 }, dataZoom: [ { type: 'inside', start: 50, end: 100 }, { type: 'slider', start: 50, end: 100, height: 20, bottom: 10 } ], series: [{ type: 'line', data: [ [1535000000000, 23], [1535003600000, 45], [1535007200000, 67] ], sampling: 'lttb' }]dataZoom数组里可以同时放inside和slider两个实例。inside让鼠标滚轮和拖拽直接作用于坐标系,slider在底部生成一个可拖动的缩放条,两者共享start和end表示初始显示的数据窗口百分比。sampling: 'lttb'是 3.x 就有的降采样算法,当数据点上万时能保留曲线形态又显著降低渲染压力,这个参数在线性数据超过 1000 个点之后我会建议一律打开。
时序图里 tooltip 的联动配置是:tooltip: { trigger: 'axis' }。如果图里有两条线,默认轴上会同时显示两个系列的数据,如果只要一个系列的提示,可以给 series 里不想要的项加tooltip: { show: false }。dataZoom和 tooltip 是独立组件,但实际使用中要一起调:缩放窗口很大时 tooltip 的内容会很密,建议同时在tooltip里设置confine: true让它不要溢出容器边界。
4. 让数据动起来:setOption 更新、异步加载与事件下钻
4.1 setOption 的 merge 语义:遗留图层的根源
3.8.5 里setOption默认是 merge 模式。意思是新 option 里的配置会按组件 id、series 的 name 和索引去和旧配置合并,没有出现在新 option 里的旧配置会继续保留。这个机制在数据刷新时会造成一种典型「翻车」:新数据只有一条线,但图上还留着上一条线。
// 第一次 chart.setOption(option1); // option1 里有两根柱子 // 数据切换后 chart.setOption(option2); // option2 只给了一个系列,旧系列残留,出现三根柱子解决方法是明确告诉 setOption 要整份替换:
chart.setOption(option2, true); // true 表示 notMerge,完全清除旧配置第二个参数传true之后,所有旧的组件和系列会被卸载,再按新 option 重建。代价是重建过程中动画状态会丢失,数据刷新时看起来是直接跳变而不是平滑过渡。如果业务要求保留进出场动画,一个折中方案是先chart.clear()再setOption(option2, false),clear 会把实例清空,后一次 setOption 的 merge 就不会有残留问题。我一般习惯在数据刷新的场景里直接用 notMerge,动画在展示型页面才保留。
4.2 异步取数渲染:loading 状态和流程固定写法
图表数据来自接口是这个方向的常态。3.8.5 提供了showLoading和hideLoading两个方法,在请求期间给用户一个反馈。完整流程是:init 之后先showLoading,发起请求,拿到数据后 setOption,最后 hideLoading。
chart.showLoading({ text: '数据加载中', maskColor: 'rgba(255, 255, 255, 0.6)' }); fetch('/api/dashboard/trend') .then(function(res) { return res.json(); }) .then(function(data) { chart.setOption({ xAxis: { type: 'category', data: data.dates }, series: [{ type: 'line', data: data.values }] }); chart.hideLoading(); }) .catch(function(err) { chart.hideLoading(); console.error('数据加载失败:', err); });这段代码里showLoading的参数是 loading 样式配置,text是提示文字,maskColor是遮罩色。请求用的是 fetch,如果你的目标环境是 IE9,fetch 不存在,需要换 XMLHttpRequest 或者引入 polyfill。细节上要注意:setOption 和 hideLoading 的顺序,必须 setOption 成功后再 hideLoading,否则请求失败或返回空数据时用户会看到图表区域空白,还以为是 loading 没结束。
另一个容易忽略的点是接口返回的数据结构和 xAxis 的 data 必须对得上。比如后端返回对象数组[{ date: '2024-01-01', value: 820 }],渲染前要 map 成两个平行数组,或者直接用dataset组件。3.8.5 已经支持 dataset,但老项目更多还是手工转换:
var dates = data.map(function(item) { return item.date; }); var values = data.map(function(item) { return item.value; });4.3 事件绑定:click 下钻的常用写法
图表交互最常见的诉求是点击柱子或扇区做下钻。3.x 的事件模型是chart.on注册,事件的回调参数是一个包含当前点击元素信息的对象。
chart.on('click', function(params) { if (params.componentType === 'series') { // params.name 是类目名,params.value 是数值 // params.seriesIndex 是点击的系列索引 loadDetailData(params.name, params.seriesIndex); } }); function loadDetailData(category, seriesIndex) { fetch('/api/detail?category=' + encodeURIComponent(category)) .then(function(res) { return res.json(); }) .then(function(data) { chart.setOption({ title: { text: category + ' 详情' }, series: [{ type: 'bar', data: data.items }] }, true); }); }params.componentType用来判断点击的是 series 还是别的组件,点击坐标轴空白处时这个字段是别的值,直接访问params.name会拿到 undefined。下钻时建议把当前选中的类目记录下来,并提供返回按钮,而不是让用户无法回退。用 notMerge 模式重设 option 是切换页面内容的捷径,但注意要清掉旧的事件监听,否则每次渲染都会叠加一次chart.on,点击一次会触发多次回调。这个问题通过chart.off('click')移除再重新绑定可以解决。
5. echart3.8.5 避坑:六个高频问题与排查顺序
5.1 容器不可见导致图表空白
现象:图表初始化在弹窗或 Tab 页里,弹窗打开后图表区域是空的,或者宽度/高度只有默认的几十像素。原因:init 时容器是display: none,clientWidth 取到 0,图表按 0 尺寸初始化,后续容器显示但图表不会自己重新测量。解决:把 init 移到容器显示之后执行,或者在容器显示后调用chart.resize()。我在某项目里见过把 init 放在弹窗内容渲染前、依赖 setTimeout 延时的写法,那个不稳定,网络慢时会拿不到尺寸,改成visible后再初始化最可靠。
5.2 地图系列空白或报 "Map not exists"
现象:series 里配了type: 'map'和map: 'china',页面一片空白,控制台报错提示地图不存在。原因:ECharts 3.x 本身不再内置地图 GeoJSON,需要开发者自己准备地理数据并用registerMap注册。解决:下载对应范围的 GeoJSON 文件(全国、省份、市区),在 setOption 之前注册:
fetch('/geo/china.json') .then(function(res) { return res.json(); }) .then(function(geoJson) { echarts.registerMap('china', geoJson); chart.setOption(mapOption); });注意注册名称的大小写,注册的是'china',option 的map字段就必须也是'china',不一致时不会报错但图表空白。GeoJSON 的数据量通常几百 KB 到几 MB,内网环境没问题,外网页面要考虑加载进度和缓存策略。
5.3 数据刷新后旧图形残留
现象:接口轮询刷新图表,新数据只有两个系列,图上有三个,而且图例点击后行为怪异。原因:setOption 默认 merge,旧 series 的配置残余在实例里。解决:轮询刷新场景在 setOption 第二个参数传true做整份替换,或者每次刷新前chart.clear()。如果同时发现图表动画卡顿,把animation: false加上,轮询场景本来也不该每次刷新都播动画。
5.4 点点点就卡顿:大数据渲染优化
现象:折线图几千个点,拖拽缩放明显掉帧,mouseover 提示也卡。原因:默认情况下所有点都参与渲染,tooltip 在 hover 时反复计算。解决:sampling: 'lttb'、large: true、animation: false三个参数配合。large开大数据量模式,不过它会让拖拽交互变成「移动整块图形」而不是逐帧重绘,视觉上会有差异,先只开 sampling,还卡再加 large。tooltip 的triggerOn改成'click'可以避免鼠标滑过时频繁触发。
5.5 tooltip formatter 里拿不到数据
现象:formatter 用了箭头函数或者模板字符串,结果params是 undefined,或者{a0}显示为 undefined。原因:3.x 的 formatter 支持字符串模板和函数两种写法,用函数时参数是一个对象或对象数组,很多人在多 series 场景下误用了数组的索引。解决:统一用函数形式,并且先打印参数再写逻辑:
tooltip: { trigger: 'axis', formatter: function(params) { if (Array.isArray(params)) { return params.map(function(p) { return p.seriesName + ': ' + p.value; }).join('<br>'); } return params.seriesName + ': ' + params.value; } }params在 axis trigger 下通常是数组,在 item trigger 下通常是对象,Array.isArray判断是最稳的分支。
5.6 组件销毁后图表还在占用资源
现象:SPA 页面切换后,图表所在 DOM 被移除,但浏览器内存居高不下,或者切换回来后再 setOption 报错。原因:没有在销毁时调用chart.dispose(),实例和事件监听停留在内存里。解决:在组件卸载回调中执行chart.dispose(),同时解绑 window 上的 resize 监听。dispose 之后再调用任何实例方法都会报错,判断实例是否已释放可以加一个标志位。resize 监听建议做节流,每 100ms 最多触发一次,避免大屏缩放时图表实例疯狂重绘。
6. 再往深走:按需引入瘦身、主题注册与封装习惯
如果项目对首屏体积有要求,3.8.5 的按需引入可以绕开完整包。用 webpack 时从 lib 路径逐模块引入:
import echarts from 'echarts/lib/echarts'; import 'echarts/lib/chart/line'; import 'echarts/lib/chart/bar'; import 'echarts/lib/chart/pie'; import 'echarts/lib/component/tooltip'; import 'echarts/lib/component/legend'; import 'echarts/lib/component/title'; import 'echarts/lib/component/dataZoom';这个写法的前提是项目里已经用了模块打包器。只引入了 line、bar、pie 和常用组件,打包体积比import echarts from 'echarts'明显小。每多一个图表类型就多一行 import,漏掉哪个图表就渲染不出对应图形,建议把项目里用到的类型和组件集中放在一个echarts-vendor.js文件里统一出口:
// echarts-vendor.js import echarts from 'echarts/lib/echarts'; import 'echarts/lib/chart/line'; // ... export default echarts;业务代码从echarts-vendor.js里拿到 echarts 对象,统一入口方便后续升级时对照。这个模式是 3.x 时代最通用的打包瘦身方式,也适合迁移到 4.x 时做差异对比。
主题定制方面,3.8.5 用registerTheme注册,init 时通过第二个参数引用。做深色大屏时我会先注册一个基础主题,再在业务里覆盖局部颜色:
echarts.registerTheme('dark-board', { backgroundColor: '#0f1c2c', color: ['#4ea8de', '#7ad7f0', '#f4a261', '#e9c46a'], textStyle: { color: '#d3e3f0' }, legend: { textStyle: { color: '#d3e3f0' } }, tooltip: { backgroundColor: '#0f1c2c', borderColor: '#4ea8de', textStyle: { color: '#d3e3f0' } } }); var chart = echarts.init(dom, 'dark-board');再补一个我自己的封装习惯:把 init 和 resize 收进一个工厂函数,避免每个页面重复处理容器尺寸和页面缩放。所有 setOption 统一走一层包装,方便后续埋点和切换主题。
function mountChart(domId) { var chart = echarts.init( document.getElementById(domId), 'dark-board', { renderer: 'canvas' } ); var resizeHandler = function() { chart.resize(); }; window.addEventListener('resize', resizeHandler); return { instance: chart, destory: function() { window.removeEventListener('resize', resizeHandler); chart.dispose(); } }; }这个封装我在好几个项目里复用,规避了 ini_t 时机和 resize 泄漏两个雷。做图表开发时习惯是:先在页面把 option 调出想要的效果,再抽公共配置,最后才封装。顺序反过来的话,封装的通用性和实际业务数据经常打架,改起来反而更慢。3.8.5 本身是个老版本,但它的配置心智、按需引入方法和主题机制,放到新项目里依然能撑起一整块大屏。希望这些踩过的坑能帮到你。
本文还有配套的精品资源,点击获取