资讯详情

资讯详情

小程序折线图实战:F2 轻量方案与 ECharts 对比

1. 为什么在小程序里画折线图我最终选了 F2 而不是 ECharts微信小程序里做数据可视化折线图几乎是绕不开的需求。订单趋势、步数统计、体重变化、股价走势只要涉及随时间变化的数值折线图就是最直观的表达方式。但小程序和普通 Web 页面不一样它没有 DOM没有window也没有document那些在浏览器里跑得好好的图表库直接搬进来大概率是白屏或者报一堆找不到对象的错误。我最早用的是 ECharts 的小程序版本功能确实全但包体积也真的大。一个完整构建下来主包很容易被撑到接近 1MB而小程序主包上限是 2MB稍微再加点业务代码就顶到天花板了。后来换成 F2也就是antv/f2的微信小程序封装antv/wx-f2整个图表相关代码压缩后只有几十 KB折线图、柱状图、饼图这些常用图形都能覆盖对于绝大多数业务场景完全够用。F2 的定位是移动端图表库它从设计之初就考虑了移动设备的性能和交互特点。它不依赖 DOM而是通过 Canvas 直接绘制这一点和小程序的渲染机制天然契合。antv/wx-f2则是在 F2 基础上做了一层小程序适配把 Canvas 的获取、触摸事件的绑定、像素比的适配这些脏活累活都封装好了我们只需要关心数据怎么传、图表怎么配。这篇文章适合两类人一类是刚接触小程序图表、不知道从哪下手的新手另一类是用过 ECharts 但被包体积和性能问题困扰、想换轻量方案的开发者。我会从环境搭建讲到折线图的完整实现再补充几个实际项目中踩过的坑和优化技巧尽量让你看完就能直接抄作业。2. 环境准备wx-f2 的安装与小程序基础配置2.1 安装依赖的两种方式antv/wx-f2的引入方式有两种取决于你的小程序项目有没有用 npm 构建。如果你的项目已经开启了 npm 支持在开发者工具里点工具 - 构建 npm能正常跑那直接装包就行npm install antv/wx-f2 --save装完之后在开发者工具里执行一次构建 npm然后在页面 js 里这样引入const F2 require(antv/wx-f2);如果你的项目没有用 npm或者你不想折腾构建流程也可以直接把wx-f2的源码文件下载下来放到项目目录里用相对路径引入。这种方式的好处是零配置坏处是后续升级麻烦得手动替换文件。我个人建议还是走 npm毕竟现在小程序对 npm 的支持已经很成熟了。注意antv/wx-f2和antv/f2是两个不同的包前者是专门给小程序用的封装后者是给 Web 用的。别装错了装错了引入会报错。2.2 Canvas 组件的配置要点小程序里画图必须用canvas组件而wx-f2需要拿到 canvas 的 context 才能绘制。这里有个关键点wx-f2要求 canvas 的type必须是2d不能用旧版的默认类型。在 wxml 里这样写canvas idlineChart type2d stylewidth: 100%; height: 400rpx; /canvastype2d这个属性一定要加上否则wx-f2拿到的 context 是旧版接口绘制会失败。另外 canvas 的宽高建议用 rpx 或者百分比让它自适应不同屏幕但要注意后面初始化图表时传入的宽高要和实际渲染尺寸一致否则会出现模糊或者拉伸。还有一个容易忽略的点canvas 的id必须唯一。如果你一个页面里有多个图表每个 canvas 的 id 都要不一样否则wx.createSelectorQuery查出来的节点会串。2.3 页面生命周期里的初始化时机图表的初始化不能放在onLoad里直接做因为那时候 canvas 节点还没渲染出来查询节点会返回 null。正确的时机是onReady这时候页面首次渲染完成canvas 已经存在了。Page({ onReady() { this.initChart(); }, initChart() { // 这里做图表初始化 } });如果你是在自定义组件里用图表那要用组件的ready生命周期而不是页面的onReady。这一点很多人会搞混导致图表死活出不来。3. 折线图的核心实现从数据到画布的完整链路3.1 初始化 Chart 实例的正确姿势wx-f2的初始化需要传入一个配置对象里面最关键的是context、width、height和pixelRatio。context通过wx.createSelectorQuery查询 canvas 节点后调用getContext(2d)获得。const query wx.createSelectorQuery(); query .select(#lineChart) .fields({ node: true, size: true }) .exec((res) { const canvas res[0].node; const ctx canvas.getContext(2d); const dpr wx.getSystemInfoSync().pixelRatio; canvas.width res[0].width * dpr; canvas.height res[0].height * dpr; ctx.scale(dpr, dpr); const chart new F2.Chart({ context: ctx, width: res[0].width, height: res[0].height, pixelRatio: dpr }); // 后续配置数据源和图形 });这里有几个细节值得展开说。pixelRatio是设备像素比iPhone 的 Retina 屏一般是 2 或 3。如果不处理这个图表在高清屏上会糊得像蒙了一层雾。处理方式就是先把 canvas 的实际像素尺寸放大 dpr 倍再用ctx.scale(dpr, dpr)把绘制坐标系缩回来这样逻辑坐标不变但物理像素翻倍线条就清晰了。width和height传的是逻辑尺寸也就是res[0].width和res[0].height不是乘以 dpr 之后的。这一点如果搞反了图表会画到画布外面去。3.2 数据源格式与字段映射F2 的数据源是一个数组每个元素是一个对象对象里包含横轴字段和纵轴字段。比如要画一周的订单量折线图const data [ { day: 周一, orders: 120 }, { day: 周二, orders: 200 }, { day: 周三, orders: 150 }, { day: 周四, orders: 80 }, { day: 周五, orders: 170 }, { day: 周六, orders: 260 }, { day: 周日, orders: 220 } ];然后通过chart.source(data)把数据喂进去再用chart.line().position(day*orders)告诉 F2 横轴用day字段纵轴用orders字段。这里的*是 F2 的语法左边是 x 轴字段右边是 y 轴字段顺序不能反。字段名可以是中文但我不建议这么做。中文做字段名在后续做 tooltip 格式化、图例配置的时候容易出编码问题还是用英文或者拼音更稳妥展示的时候再通过chart.axis或者chart.tooltip去改显示文本。3.3 折线样式与坐标轴配置基础的折线图只需要chart.line().position(day*orders)就够了但实际项目里通常要调不少样式。下面是一份我常用的配置chart.line() .position(day*orders) .color(#4A90D9) .shape(smooth) .size(2); chart.point() .position(day*orders) .color(#4A90D9) .size(3); chart.axis(orders, { label: { formatter: (val) val 单 }, grid: { stroke: #E8E8E8, lineDash: [4, 4] } }); chart.axis(day, { label: { fill: #666, fontSize: 11 }, tickLine: null }); chart.tooltip({ showCrosshairs: true, crosshairsStyle: { stroke: #4A90D9, lineDash: [4, 4] } }); chart.render();shape(smooth)会把折线变成平滑曲线视觉上更柔和但要注意平滑曲线在数据波动剧烈时可能产生过冲的错觉金融类数据慎用。showCrosshairs是移动端折线图的标配手指按上去会出现一条竖线跟随配合 tooltip 显示当前点的数值体验比 Web 端的 hover 更自然。坐标轴的grid配置里lineDash控制虚线样式[4, 4]表示画 4 个像素空 4 个像素。网格线颜色不要太深#E8E8E8这种浅灰就够太深会喧宾夺主。4. 移动端特有的交互处理触摸、滚动与性能4.1 触摸事件与 tooltip 的联动小程序里的触摸事件和 Web 不一样wx-f2内部已经帮我们做了适配但有一个前提canvas 上不能有会拦截触摸的元素盖着。如果你在 canvas 上面叠了一个透明的 view 做布局触摸事件就到不了 canvastooltip 自然就不响应了。wx-f2默认开启了 tooltip 的触摸响应手指在图表上滑动时crosshairs 会跟着走tooltip 显示对应点的数据。如果你发现按上去没反应先检查两件事一是 canvas 有没有被其他元素遮挡二是chart.tooltip()有没有配置showCrosshairs: true。还有一个细节tooltip 的内容默认显示字段名和数值但字段名如果是英文用户看着别扭。可以在chart.tooltip里用custom: true自定义渲染或者更简单的方式是在数据源里就把字段名改成中文但前面说了中文做字段名有风险所以推荐用chart.tooltip的itemFormatter来做映射。4.2 图表与页面滚动的冲突这是移动端图表最烦人的问题之一。折线图如果支持左右滑动查看更长时间范围的数据那手指在图表上横向滑动时页面本身也在滚动两者会打架。wx-f2本身不提供滚动能力它只负责把给定的数据画出来。如果你需要横向滚动通常的做法是用一个scroll-view包住 canvascanvas 的宽度设成数据点数量乘以每个点的间距这样scroll-view负责滚动canvas 负责绘制。但这样一来canvas 的宽度可能远超屏幕绘制性能会下降。我的建议是如果数据点不超过 30 个直接全部画在屏幕宽度内不做滚动用 tooltip 查看具体数值就够了。如果数据点确实很多比如要展示一年的每日数据那不如做数据聚合按周或按月汇总后再画而不是硬塞几百个点到一张图里。4.3 大数据量下的渲染性能F2 的渲染性能在移动端图表库里算优秀的但也不是没有上限。我实测下来单条折线在 200 个数据点以内渲染时间基本在 100ms 以内用户感知不到卡顿。超过 500 个点首次渲染会明显变慢而且在低端安卓机上可能出现掉帧。优化的思路有几个。一是数据抽稀比如每 5 个点取一个或者用 Douglas-Peucker 算法做曲线简化保留关键拐点。二是关闭不必要的动画chart.animate(false)可以关掉入场动画省掉一部分计算。三是如果图表不需要交互可以把chart.tooltip关掉减少事件监听的开销。还有一个容易被忽略的点页面隐藏时记得销毁图表实例。小程序的页面onHide里调用chart.destroy()避免后台还在跑渲染逻辑浪费性能。5. 踩坑实录那些让我加班到深夜的报错5.1 canvas 节点查询返回 null 的三种原因TypeError: Cannot read property node of undefined 这个报错我见过太多次了。wx.createSelectorQuery().select(#lineChart)查不到节点res[0]就是 undefined后面取.node自然就崩了。原因一初始化时机太早。放在onLoad里查节点那时候页面还没渲染肯定查不到。改成onReady就好。原因二canvas 在自定义组件里但查询用的是页面的 query。自定义组件里的节点要用this.createSelectorQuery()而不是wx.createSelectorQuery()。这个坑很隐蔽因为报错信息一模一样。原因三canvas 被wx:if控制着初始化时条件为 false节点根本没渲染。这种情况要么改成hidden要么在条件变为 true 之后再初始化图表。5.2 图表模糊、线条发虚的像素比问题前面提过pixelRatio的处理但实际做的时候还是容易出错。最常见的错误是 canvas 的width和height属性设了 dpr 倍但ctx.scale忘了调或者调的顺序不对。正确的顺序是先设canvas.width width * dpr再设canvas.height height * dpr然后ctx.scale(dpr, dpr)最后创建 Chart 实例时传pixelRatio: dpr。这三步缺一不可。还有一种情况是图表本身清晰但 tooltip 的文字模糊。那是因为 tooltip 是 F2 用 canvas 绘制的如果pixelRatio没传对文字就会发虚。检查一下 Chart 配置里的pixelRatio是不是和ctx.scale用的 dpr 一致。5.3 数据更新后图表不刷新的处理小程序里数据变了图表不会自动重绘必须手动调chart.changeData(newData)或者重新chart.source(newData)再chart.render()。我推荐用changeData它只更新数据不重建图表实例性能更好。但要注意changeData要求新数据的字段结构和旧数据完全一致如果字段名变了得用chart.source重新来。还有一个坑如果在setData的回调里更新图表要确保 canvas 已经渲染完成。有时候setData回调触发时canvas 的尺寸还没稳定画出来的图会错位。稳妥的做法是在setData回调里加一个wx.nextTick等下一个渲染周期再更新图表。6. 进阶玩法让折线图更贴合业务场景6.1 多折线对比与图例配置实际业务里经常要对比多条线比如今年和去年的订单趋势。F2 支持多折线数据源里加一个分类字段就行const data [ { month: 1月, type: 今年, value: 120 }, { month: 1月, type: 去年, value: 100 }, { month: 2月, type: 今年, value: 200 }, { month: 2月, type: 去年, value: 180 } ]; chart.line() .position(month*value) .color(type);color(type)会自动根据type字段的值分配不同颜色并生成图例。图例默认在顶部可以通过chart.legend调整位置和样式。如果图例文字太长可以设置wordWrap让它换行或者干脆把图例关掉用自定义的 HTML 图例代替。多折线的时候要注意颜色区分度F2 默认的配色方案在浅色背景下还行但如果你的页面背景是深色得手动指定一组高对比度的颜色。6.2 区域填充与渐变效果折线下面填充一块半透明区域视觉上会更有层次感。F2 里用chart.area()实现chart.area() .position(day*orders) .color(l(90) 0:#4A90D9 1:#FFFFFF) .shape(smooth);l(90)表示从上到下的线性渐变0:#4A90D9是起点颜色1:#FFFFFF是终点颜色。实际效果就是折线附近颜色深往下逐渐变淡到白色。这个配置在深色背景下要把终点色改成背景色否则会突兀。区域填充和折线通常一起用先画 area 再画 line这样折线会盖在填充上面层次分明。顺序反了的话折线会被填充盖住一部分。6.3 空数据与异常值的兜底展示真实数据不可能永远完美总有某天没数据、某个值异常大的情况。空数据时如果直接画折线会断开或者跳到 0视觉上很怪。我的处理方式是在数据预处理阶段就把空值填上。如果业务上没数据意味着 0那就填 0如果意味着未知可以用前一个点的值做平滑填充或者用虚线表示这段是估算的。F2 本身不提供空值处理这些逻辑得在喂数据之前做好。异常值方面如果一个点特别大会把其他点压得很扁。可以考虑用对数坐标轴或者对数据进行截断超过阈值的点用特殊标记标出来而不是让它主导整个纵轴范围。7. 我在实际项目里总结的几条经验图表这东西写出来容易写好看了、用着舒服了难。我做了几个带折线图的小程序项目有几个体会比较深。第一别在图表上堆太多信息。一张图只讲一件事订单趋势就只画订单不要把转化率、客单价都塞进来。信息过载的图表用户看一眼就划走了。第二移动端的图表尺寸有限坐标轴标签能简则简。横轴如果显示2024年1月1日这种长文本在小屏上会挤成一团。改成1/1或者只显示月份详细信息放到 tooltip 里。第三颜色不要超过五种。多折线对比的时候颜色太多用户根本分不清哪条是哪条。超过五条线考虑用分面或者拆成多张图。第四测试的时候一定要在真机上跑。开发者工具里的渲染和真机有差异尤其是 canvas 的像素比和触摸事件模拟器上正常不代表真机正常。我遇到过模拟器上 tooltip 响应正常真机上死活不出来的情况最后发现是某个 CSS 属性在真机上拦截了触摸事件。第五wx-f2的版本更新不算频繁但每次更新还是看一下 changelog。我有次升级后发现 tooltip 的默认样式变了排查了半天才发现是版本差异。锁定版本号升级前先在测试环境验证这个习惯能省不少事。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →