1. 为什么要在小程序里用ECharts做图表
做过小程序数据展示的朋友大概都有过这种体验:官方自带的canvas组件画个柱状图还行,一旦要画折线、饼图、雷达图,还得处理坐标轴、图例、tooltip、动画,代码量直接爆炸。更别提产品经理隔三差五就要改配色、改交互,手写canvas的维护成本高得离谱。
ECharts这套图表库在Web端已经非常成熟,配置项丰富、文档齐全、社区案例多,如果能直接搬到小程序里用,等于白捡一套完整的图表解决方案。但问题在于,小程序没有DOM,ECharts默认依赖的document、window这些对象全都不存在,直接引入会报一堆错。所以核心思路就是:用小程序原生canvas模拟一个ECharts能识别的渲染环境,让ECharts以为自己在浏览器里跑。
目前主流的做法是使用社区维护的echarts-for-weixin组件,它本质上是对ECharts做了一层适配,把canvas操作映射到小程序的canvas接口上。这个方案能覆盖绝大多数常用图表类型,包括折线图、柱状图、饼图、散点图、雷达图、K线图等,对于中小型数据可视化需求完全够用。
这篇文章适合谁看?如果你正在做小程序,需要展示销售数据、用户增长趋势、设备监控指标这类可视化内容,又不想从零造轮子,那这套方案可以直接抄作业。下面我会从整体设计思路、核心细节、实操步骤到踩坑排查,完整走一遍。
2. 整体方案设计与技术选型拆解
2.1 为什么选echarts-for-weixin而不是其他方案
市面上在小程序里做图表,大概有三条路:一是用官方canvas手写,二是用第三方图表库如wx-charts、F2,三是用echarts-for-weixin。我三个方案都试过,最后稳定用ECharts方案,原因很直接。
手写canvas的问题前面说了,维护成本高,而且交互(比如点击某个数据点弹出详情)要自己算坐标,非常麻烦。wx-charts轻量但图表类型少,配置项也简陋,稍微复杂一点的需求就撑不住。F2是移动端优先的,但它的语法和ECharts差异较大,团队里如果前端同学熟悉ECharts,迁移成本反而更高。
echarts-for-weixin的优势在于:配置项和Web端ECharts几乎一致,你可以在ECharts官方示例站上调好配置,直接复制到小程序里用,学习成本极低。而且它支持按需引入,只打包用到的图表类型,体积可控。实测下来,一个折线图+柱状图的组合,打包后大概200KB左右,对于小程序2MB的主包限制来说完全可以接受。
2.2 核心架构:三层分离的设计思路
整个方案我建议按三层来组织:数据层、配置层、渲染层。
数据层负责从后端接口拿数据,做清洗和格式化。比如后端返回的时间戳要转成MM-DD格式,数值要保留两位小数,这些都在数据层处理完,不要带到配置层。
配置层就是ECharts的option对象,负责定义图表长什么样——什么类型、什么颜色、坐标轴怎么显示、tooltip怎么弹。这一层尽量保持纯配置,不要混入业务逻辑,方便复用。
渲染层就是ec-canvas组件,负责把配置和数据画出来。这一层要处理的是初始化时机、尺寸计算、重绘触发这些和渲染相关的事情。
这样分层的好处是:换图表类型只需要改配置层,换数据源只需要改数据层,渲染层基本不用动。团队协作时,一个人调样式,一个人接数据,互不干扰。
2.3 按需引入与体积控制策略
ECharts完整包体积很大,小程序里绝对不能全量引入。echarts-for-weixin支持按需引入,你需要在ec-canvas目录下找到echarts.js,然后根据项目实际用到的图表类型做裁剪。
具体做法是:先引入echarts/core,然后按需引入需要的图表类型和组件。比如你只需要折线图和柱状图,就只引入LineChart和BarChart,再加上GridComponent、TooltipComponent、LegendComponent这几个基础组件。这样打包出来的体积能控制在150KB以内。
注意:按需引入时,
echarts/core必须引入,它是核心运行时。另外CanvasRenderer也必须引入,否则图表渲染不出来。
3. 核心细节解析与实操要点
3.1 ec-canvas组件的初始化时机
ec-canvas的初始化有个坑:它依赖canvas的尺寸,如果在小程序页面还没布局完成时就初始化,拿到的宽高可能是0,图表会画不出来或者画得很小。
正确的做法是在onReady生命周期里初始化,而不是onLoad。因为onLoad时页面还没渲染,onReady时页面首次渲染完成,此时canvas的尺寸才是准确的。
如果你用的是自定义组件,那就在组件的ready生命周期里初始化。另外,如果图表所在的容器是动态显示的(比如在tab切换里),那每次显示时都要调用init方法重新初始化,或者用resize方法调整尺寸。
3.2 数据格式化的几个关键点
ECharts对数据格式有要求,不同图表类型要求还不一样。折线图和柱状图的series.data是一个数组,饼图是{name, value}对象数组,雷达图是嵌套数组。从后端拿到的数据往往不能直接用,需要做一层转换。
我一般会写一个formatData函数,根据图表类型做不同的处理。比如时间序列数据,后端可能返回的是时间戳数组和数值数组,我需要把它们合并成[[timestamp, value], ...]的格式,同时把时间戳转成可读的日期字符串。
还有一个容易忽略的点:空值处理。如果某个数据点是null或undefined,ECharts会断线或者显示异常。我通常会把空值转成'-'或者用前一个有效值填充,具体看业务需求。
3.3 响应式与尺寸适配
小程序的屏幕尺寸五花八门,图表要能自适应。ec-canvas组件本身支持传入width和height属性,但更推荐的做法是用CSS控制容器尺寸,然后让canvas自动填满。
具体来说,给ec-canvas外面包一个view,设置width: 100%; height: 400rpx;,然后ec-canvas设置width: 100%; height: 100%;。这样在不同屏幕上,图表会自动缩放。
但要注意:canvas的尺寸变化不会自动触发ECharts重绘。如果你在运行时改变了容器尺寸(比如横竖屏切换),需要手动调用chart.resize()方法。这个在ec-canvas组件里可以通过this.selectComponent('#chart').chart.resize()来调用。
4. 完整实操流程与核心代码实现
4.1 环境准备与组件引入
第一步,把echarts-for-weixin的ec-canvas目录拷贝到你的小程序项目里,一般放在components目录下。这个目录包含ec-canvas.js、ec-canvas.wxml、ec-canvas.wxss、echarts.js和wx-canvas.js五个文件。
第二步,在页面的json文件里注册组件:
{ "usingComponents": { "ec-canvas": "/components/ec-canvas/ec-canvas" } }第三步,在wxml里使用组件:
<view class="chart-container"> <ec-canvas id="line-chart" canvas-id="line-canvas" ec="{{ ec }}"></ec-canvas> </view>这里的ec是一个对象,包含onInit方法,后面会详细说。
4.2 按需引入ECharts的配置方法
打开ec-canvas/echarts.js,找到文件末尾的引入部分,改成按需引入。下面是一个只包含折线图和柱状图的配置示例:
import * as echarts from 'echarts/core'; import { LineChart, BarChart } from 'echarts/charts'; import { GridComponent, TooltipComponent, LegendComponent, TitleComponent } from 'echarts/components'; import { CanvasRenderer } from 'echarts/renderers'; echarts.use([ LineChart, BarChart, GridComponent, TooltipComponent, LegendComponent, TitleComponent, CanvasRenderer ]); export default echarts;这样打包出来的echarts.js只包含用到的模块,体积会小很多。如果你后续要加饼图,就再引入PieChart,以此类推。
4.3 页面逻辑与图表初始化
在页面的js文件里,先定义ec对象:
import * as echarts from '../../components/ec-canvas/echarts'; let chartInstance = null; function initChart(canvas, width, height, dpr) { chartInstance = echarts.init(canvas, null, { width: width, height: height, devicePixelRatio: dpr }); canvas.setChart(chartInstance); const option = { title: { text: '近7日访问趋势', left: 'center', textStyle: { fontSize: 14 } }, tooltip: { trigger: 'axis' }, legend: { data: ['访问量', '下单量'], bottom: 0 }, grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true }, xAxis: { type: 'category', boundaryGap: false, data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] }, yAxis: { type: 'value' }, series: [ { name: '访问量', type: 'line', smooth: true, data: [820, 932, 901, 934, 1290, 1330, 1320] }, { name: '下单量', type: 'line', smooth: true, data: [120, 132, 101, 134, 90, 230, 210] } ] }; chartInstance.setOption(option); return chartInstance; } Page({ data: { ec: { onInit: initChart } }, onReady() { // 页面渲染完成后,图表已经初始化好了 } });这段代码里,initChart是核心,它接收canvas、宽高和像素比,然后调用echarts.init创建实例,设置配置项,最后返回实例。ec-canvas组件会自动调用这个方法。
4.4 动态更新数据的正确姿势
图表初始化后,数据变了要更新,不能重新init,那样会创建多个实例导致内存泄漏。正确做法是拿到已有的chartInstance,调用setOption方法。
// 假设从接口拿到了新数据 const newData = [900, 1000, 950, 1100, 1400, 1500, 1450]; chartInstance.setOption({ series: [ { name: '访问量', data: newData } ] });注意这里只传了要更新的部分,ECharts会自动合并。如果你传了完整的option,它会覆盖之前的配置,可能导致一些样式丢失。所以增量更新时只传变化的部分。
如果数据变化很大,比如从折线图变成柱状图,那就需要先dispose掉旧实例,再重新init。但这种情况很少见,一般只改数据不改类型。
4.5 多图表同页面的处理
一个页面里放多个图表时,每个图表都要有独立的ec对象和canvas-id。不能共用一个ec,否则第二个图表会覆盖第一个。
Page({ data: { ec1: { onInit: initChart1 }, ec2: { onInit: initChart2 } } });对应的wxml:
<ec-canvas id="chart1" canvas-id="canvas1" ec="{{ ec1 }}"></ec-canvas> <ec-canvas id="chart2" canvas-id="canvas2" ec="{{ ec2 }}"></ec-canvas>每个initChart函数里创建独立的chartInstance,互不干扰。但要注意,多个图表同时渲染会消耗较多性能,如果页面图表超过3个,建议做懒加载,滚动到可视区域再初始化。
5. 常见问题与排查技巧实录
5.1 图表不显示或显示空白
这是最常见的问题,原因通常有三个:一是初始化时机不对,在onLoad里初始化导致canvas尺寸为0;二是ec-canvas的父容器没有设置高度,导致canvas高度为0;三是echarts.js没有正确按需引入,缺少必要的组件。
排查步骤:先在initChart里打印width和height,如果是0,说明容器尺寸有问题。检查父容器的CSS,确保有明确的高度值。然后检查echarts.use里是否引入了CanvasRenderer,这个漏了图表肯定画不出来。
5.2 图表模糊、锯齿严重
这是像素比问题。小程序的canvas默认像素比是1,在高清屏上会模糊。ec-canvas组件已经处理了这个问题,它会把devicePixelRatio传给echarts.init。但如果你手动改了canvas的尺寸,可能会破坏这个机制。
解决办法:不要手动设置canvas的width和height属性,让ec-canvas自己处理。如果确实需要调整,确保传入的dpr是正确的,一般用wx.getSystemInfoSync().pixelRatio获取。
5.3 tooltip不显示或位置错乱
tooltip在小程序里的表现和Web端有差异。Web端tooltip是DOM元素,可以随意定位;小程序里tooltip是画在canvas上的,位置计算依赖触摸事件的坐标。
如果tooltip不显示,先检查tooltip配置里是否设置了show: true。如果位置错乱,可能是grid配置的边距太小,导致tooltip被裁剪。适当增大grid的left、right、top、bottom值,给tooltip留出空间。
另外,小程序的ec-canvas对tooltip的触摸支持需要canvas的disable-scroll属性配合,如果页面可以滚动,触摸图表时可能会触发页面滚动而不是tooltip。可以在ec-canvas上加上disable-scroll="true"。
5.4 数据更新后图表不刷新
前面说过,更新数据要用setOption而不是重新init。但有时候调了setOption图表还是没变,原因可能是数据格式不对。比如你传了一个新数组,但数组里的元素类型和之前不一致,ECharts可能无法正确合并。
解决办法:确保新数据的格式和旧数据完全一致。如果还是不行,可以尝试先clear再setOption,但这样会丢失动画效果。更稳妥的做法是用setOption(option, true),第二个参数传true表示不合并,直接替换。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 图表空白 | 初始化时机不对 | 打印宽高是否为0 | 改到onReady初始化 |
| 图表模糊 | 像素比未设置 | 检查dpr参数 | 使用ec-canvas默认处理 |
| tooltip不显示 | 触摸事件冲突 | 检查disable-scroll | 加上disable-scroll="true" |
| 数据不更新 | 格式不一致 | 对比新旧数据格式 | 统一数据格式或强制替换 |
| 多图表覆盖 | 共用ec对象 | 检查canvas-id | 每个图表独立配置 |
| 内存泄漏 | 重复init | 检查是否多次创建实例 | 复用chartInstance |
实操心得:每次修改图表配置后,先在小程序开发者工具的“调试器”里看console有没有报错。ECharts的报错信息通常比较明确,比如“Component series.line not exists”,说明你没引入
LineChart。根据报错反查引入列表,比盲目试错快得多。
6. 性能优化与进阶技巧
6.1 大数据量下的渲染优化
当数据点超过1000个时,图表渲染会明显变卡。这时候可以开启ECharts的sampling配置,它会自动对数据进行降采样,只渲染关键点。
series: [{ type: 'line', sampling: 'lttb', data: largeData }]lttb是一种保形降采样算法,能在减少数据点的同时保留趋势特征。实测下来,10000个点降到1000个点,视觉上几乎看不出差异,但渲染速度提升好几倍。
另外,如果不需要动画,可以把animation设为false,也能省不少性能。
6.2 图表懒加载与销毁
页面里图表多的时候,不要一次性全部初始化。可以用IntersectionObserver监听图表容器是否进入可视区域,进入后再初始化。
const observer = wx.createIntersectionObserver(this); observer.relativeToViewport().observe('.chart-container', (res) => { if (res.intersectionRatio > 0) { // 进入可视区域,初始化图表 this.initChart(); observer.disconnect(); } });页面卸载时,记得调用chartInstance.dispose()销毁实例,释放内存。这个在onUnload里做。
6.3 主题定制与暗黑模式适配
ECharts支持自定义主题,你可以把常用的配色、字体、边距抽成一个主题对象,在不同图表里复用。小程序支持暗黑模式,可以通过wx.getSystemInfoSync().theme获取当前主题,然后动态切换ECharts的配色。
const theme = wx.getSystemInfoSync().theme; const isDark = theme === 'dark'; const option = { backgroundColor: isDark ? '#1a1a1a' : '#ffffff', textStyle: { color: isDark ? '#e0e0e0' : '#333333' }, // ...其他配置 };这样用户在切换系统主题时,图表也能跟着变,体验会好很多。
6.4 导出图表为图片
有时候需要把图表保存成图片分享,ECharts提供了getDataURL方法,但在小程序里不能直接用,因为小程序的canvas接口不同。
ec-canvas组件提供了一个canvasToTempFilePath方法,可以拿到临时文件路径:
this.selectComponent('#chart').canvasToTempFilePath({ success: (res) => { wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath }); } });注意这个操作需要用户授权相册权限,记得在app.json里声明scope.writePhotosAlbum。
7. 我踩过的坑与实战建议
第一个坑是按需引入时漏了组件。有一次我只引入了LineChart,忘了引入GridComponent,结果图表画出来了但没有坐标轴,排查了半天才发现是组件缺失。后来我养成了习惯,每次按需引入后,先跑一个最简单的示例,确认基础功能正常再往上加配置。
第二个坑是在onLoad里初始化图表。早期没注意生命周期,图表时好时坏,后来统一改到onReady,再也没出现过空白问题。如果你的图表在弹窗或tab里,记得在显示时重新resize。
第三个坑是数据更新时传了完整option。有一次我为了省事,把整个option重新setOption了一遍,结果图表的动画每次都从头播放,用户体验很差。后来改成只传series部分,动画就正常了。
最后一个建议:把图表的配置抽成独立的模块。我现在的项目里,每个图表都有一个对应的option.js文件,导出的是一个函数,接收数据返回配置。这样页面逻辑很干净,配置也能单独测试和复用。团队里其他人要改样式,直接改option.js就行,不用动页面逻辑。
这套方案我已经在三个小程序项目里用过,覆盖了折线、柱状、饼图、雷达图,最复杂的页面有5个图表同时渲染,性能表现稳定。如果你刚开始做小程序图表,建议先从折线图入手,跑通整个流程后再扩展其他类型。遇到问题优先看console报错,ECharts的提示信息通常很准,顺着报错查基本都能解决。