npm install @antv/f2 --savenpm install @antv/f2-react --save
在index文件中复制F2官网Demoimport Canvas from '@antv/f2-react';import { Chart, Interval } from '@antv/f2';const data = [ { genre: 'Sports', sold: 275 }, { genre: 'Strategy', sold: 115 }, { genre: 'Action', sold: 120 }, { genre: 'Shooter', sold: 350 }, { genre: 'Other', sold: 150 },];export default () => <Canvas> <Chart data={data}> <Interval x='genre' y='sold' /> </Chart></Canvas>
执行代码通过上述代码,执行后,钉钉小程序控制台报错创建Canvas绘图上下文查看错误代码,发现错误文件为@antv/f2-react报错通过查看代码,发现钉钉小程序是通过调用dd.createCanvasContext(canvasId) 创建canvas绘图上下文,调整相关代码class ReactCanvas extends React.Component<CanvasProps> {// 81行 getProps = () => { const { canvasRef, props } = this; // 删除改代码 start // const canvasEl = canvasRef.current; // const context = canvasEl.getContext('2d'); // 删除结束 end // context根据canvasId获取 const context = Taro.createCanvasContext(props.id ?? 'canvasId'); }; render: () => { const { props } = this; return React.createElement(TaroCanvas, // 添加canvas属性Id,获取上下文 id: props.id ?? 'f2Canvas', }); }}
调整代码后,控制台不报错了,并且也渲染出来图表的,不过图表渲染的有点奇怪,只有左上角一点点图形通过查看F2源码,发现在初始化图表时,需要获取Canvas的宽高如果外部没有传入宽高,代码中通过DOM API获取元素宽高,但是小程序不支持该方式,所以显示图形异常以下为相关代码获取Canvas宽高class Canvas extends EventEmit { _initCanvas() { // 获取canvas的宽高,对于小程序,如果外部不传入,则宽高为0 const width = this.get('width') || getWidth(canvas) || canvas.width; const height = this.get('height') || getHeight(canvas) || canvas.height; }}export default Canvas;
初始化设置canvas宽高既然无法默认获取元素宽高,那我们可以使用小程序提供获取元素宽高的方式,来手动的获取元素的宽高,并且赋值给Props对象// 调整f2-canvas相关代码class ReactCanvas extends React.Component<CanvasProps> { renderOrUpdateCanvas = async (type: 'init' | 'update') => { // 接收一个id参数,并且通过小程序API获取元素宽高 const canvasId = props.id ?? 'f2Canvas' // 伪代码,获取元素的宽高 const { width, height } = await getWidthAndHeight(canvasId) return { width, height, }; }; componentDidMount() { // 元素可能没有渲染出来,等下一帧执行 nextTick(() => { this.renderOrUpdateCanvas("init") }) }}
设置宽高后,柱状图可以正常显示虽然柱状图正常显示出来了,不过图表很模糊,像是带了老花镜看似的通过查看F2文档,发现可以通过配置pixelRatio来设置图表清晰度并且钉钉小程序可以通过getSystemInfoSync获取设备的分辨率pixelRatio方案设置// 调整f2-canvas相关代码class ReactCanvas extends React.Component<CanvasProps> { pixelRatio: number; renderOrUpdateCanvas = async (type: 'init' | 'update') => { const config = { // 已经有高清方案,这里使用设备分辨率 pixelRatio: this.pixelRatio, ...props, }; }; componentDidMount() { // 获取设备的分辨率 this.pixelRatio = Taro.getSystemInfoSync().pixelRatio // 元素可能没有渲染出来,等下一帧执行 nextTick(() => { this.renderOrUpdateCanvas("init") }) }}
设置完成之后,查看柱状图,直接不显示数据了通过查找钉钉小程序API,没有找到任何关于Canvas精确度的问题不过最终通过查看钉钉小程序老大哥支付宝小程序的开发文档,发现了相关内容 支付宝小程序文档-canvas画布问题,通过给Canvas元素设置高分辨率宽高来解决// 调整f2-canvas相关代码class ReactCanvas extends React.Component<CanvasProps> { pixelRatio: number; canvasIsInit: boolean; // 保存变量值 state: Readonly<{ width: number; height: number }>; renderCanvas = async () => { return { // 已经有高清方案,这里默认用1 pixelRatio: this.pixelRatio, }; }; componentDidMount() { this.pixelRatio = Taro.getSystemInfoSync().pixelRatio nextTick(() => { getWidthAndHeight(this.props.id ?? 'f2Canvas').then(res => { // 设置高精度宽高 this.setState({ width: res.width this.pixelRatio, height: res.height this.pixelRatio, }) }) }) } render() { const { props } = this; return React.createElement(TaroCanvas, { id: props.id ?? 'f2Canvas', // 设置Canvas的宽高,用于高清展示图表 width: this.state.width || '100%', height: this.state.height || '100%', }); }}
设置完成之后,图表就可以高清展示了notice:设置Canvas宽高后,TS会报错,width属性不存在是因为Taro中没有定义Canvas的width和height属性可以手动添加一下ts文件declare module '@tarojs/components' { export from '@tarojs/components/types/index'; import { CanvasProps } from '@tarojs/components/types/Canvas'; export const Canvas: ComponentType<CanvasProps & { width?: number | string; height?: number | string }>;}
抹平context差异图表虽然可以正常展示了,不过并没有坐标信息当我们尝试给图标添加坐标信息时,发现页面代码报错会报错 <F2Canvas id='wrap'> <Chart data={data}> <Interval x='genre' y='sold' /> <Axis field='genre' /> </Chart> </F2Canvas>
通过查看在小程序中使用F2相关文档,发现F2 是基于 CanvasRenderingContext2D 的标准接口绘制的,但是小程序中给的 context 对象不是标准的 CanvasRenderingContext2D , 所以需要将context对象进行封装兼容处理,详情可见: https://github.com/antvis/f2-context, 其他小程序也可以按同样的思路封装继续修改相关代码,抹平小程序context差异import { my } from '@antv/f2-context'class ReactCanvas extends React.Component<CanvasProps> { renderCanvas = async () => { // 抹平context实例,引用f2-context文件 const context = my(Taro.createCanvasContext(canvasId)); return { // 上下文 context, }; }; componentDidUpdate() { this.renderCanvas() }}
当我们完成所有操作后,图表和坐标信息就可以完整的展示出来了事件传递图表已经可以正常展示,不过当我们使用Tooltip组件时,我们所有的事件都没有作用通过查看f-my代码时,我们需要当触发Canvas容器组件事件时,触发图表组件事件function wrapEvent(e) { if (e && !e.preventDefault) { e.preventDefault = function () {}; } return e;}class ReactCanvas extends React.Component<CanvasProps> { canvasEl; handleTouchStart = (e) => { const canvasEl = this.canvasEl; if (!canvasEl) { return; } canvasEl.dispatchEvent('touchstart', wrapEvent(e)); }; render() { return React.createElement(TaroCanvas, { onTouchStart: this.handleTouchStart, }); }}
事件传递后,可以正常显示文案提示小结本章节在Taro+React中接入F2的过程中遇到了不少问题通过查看React与小程序的接入方式,一步一步的解决下面的问题在小程序中Canvas的上下文获取方式和web不一致每次执行代码时需要手动获取Canvas的宽高(无法通过DOM API获取)小程序模糊问题(pixelRatio的设置)小程序中Canvas的context不是标准的CanvasRenderingContext2D对象,需要对齐添加补丁目前查询支付宝小程序文档,发现最新版本已经调整为标准对象了,不过钉钉小程序目前还不支持小程序事件需要显式定义,并且传递给图表库目前已经有人封装好了对应的接入代码,我们可以直接在github中查看使用Taro+React+F2实战使用当我们完成上述代码后,就可以正常的根据F2的官网示例在钉钉小程序中使用图表了不过部分功能需要额外开发自定义Tooltip目前F2中默认的Tooltip提示都是在图表顶部显示,并且展示上面只能设置部分属性,不太满足这边的UI规范好在Tooltip提供了自定义实现的方式,让我们可以自定义显示Tooltip提示目前古茗通过使用View标签,并且绝对定位的方式来显示对应的文本信息自定义配置方式通过设置属性custom,F2不会显示默认Tooltip通过onChange获取当前的选中的元素信息,可以拿到对应的位置信息与数据信息从而可以自定义显示对应文本// 自定义配置 <Tooltip custom ref={tooltipRef} triggerOn="click" onChange={handleTooltipChange} />
View标签位置获取import { View } from '@tarojs/components';import { nextTick } from '@tarojs/taro';import classNames from 'classnames';import { Ref, forwardRef, useImperativeHandle, useRef, useState } from 'react';import { CustomTooltipRef, TooltipChangeEvent } from './types';import { PREFIX, getEleClientRect } from '../../../../utils';import './index.less';/样式设置.custom-tooltip { position: absolute; z-index: 2; display: none; padding: 16px 20px; color: #ffffff; font-weight: 400; background: rgba(12, 12, 12, 0.7); border-radius: 4px; transform: translateY(50%); pointer-events: none;}/let num = 0;const cls = `custom-tooltip`;export const CustomTooltip = forwardRef( <T extends {}>( props: { children: React.ReactNode; placement?: 'center'; }, ref?: Ref<CustomTooltipRef> ) => { const { children, placement } = props; const customTooltipRef = useRef<HTMLDivElement>(null); const [classNum] = useState(num++); const handleTooltipChange = (_items: Array<TooltipChangeEvent<T> & T>) => { // 优先取第一条数据 const [{ x, y, yMax }] = _items; const otherY = _items?.[1]?.y; let yTop = y || otherY; / 在图片中间位置显示 / if (placement === 'center') { yTop = (yMax || _items?.[1]?.yMax || y) / 2; } // 处理 props 并更新状态 // 更新 Tooltip 样式 const elStyle = customTooltipRef.current?.style; if (elStyle) { elStyle.left = '0'; elStyle.top = String(yTop); elStyle.visibility = 'hidden'; elStyle.display = 'block'; nextTick(() => { getEleClientRect(`.${cls}-${classNum}`).then((res) => { elStyle.left = String(x > res.width ? x - res.width : x); elStyle.visibility = 'visible'; }); }); } }; const hide = () => { const elStyle = customTooltipRef.current?.style; if (elStyle) { elStyle.display = 'none'; } }; useImperativeHandle(ref, () => ({ handleTooltipChange, hide, })); return ( <View ref={customTooltipRef} className={classNames(cls, `${cls}-${classNum}`)}> {children} </View> ); });
实现效果使用过程中存在的“坑”示例横坐标为0,无法触发Tooltip事件当Axis坐标轴的值为0时,Tooltip点击事件不能点击执行折线图反转后表现不一致折线图反转后,空值直接链接了没有截断处理解决方案一般上解决三方库中的问题对于不在维护的库,通过patch的方式进行处理升级库版本解决对于class组件,可以通过本地覆盖式更新代码目前F2上面所有的组件都是class组件,所以可以通过继承或者修改原型链的方式来解决上述相关问题因为上面两个问题属于明显的bug,在我们这边采用修改原型链上面的方法来解决bug横坐标为0问题分析因为是Tooltip的show方法没有执行,通过寻找代码,找到最终原因为判断date值时,没有处理0导致的5.x已优化该问题Tooltip中withTooltip的show方法 show(point, _ev?) { const { props } = this; const { chart } = props; // 该代码获取坐标相关位置 const snapRecords = chart.getSnapRecords(point, true); // 超出边界会自动调整 this.showSnapRecords(snapRecords); }
Chart的getSnapRecords方法 getSnapRecords(point, inCoordRange?) { const geometrys = this.getGeometrys(); if (!geometrys.length) return; // geometrys[0]为点击的相关线Line return geometrys[0].getSnapRecords(point, inCoordRange); }
Line的getSnapRecords方法继承Geometry中的getSnapRecords getSnapRecords(point, inCoordRange?): any[] { // 该处理没有对value等于0的判断,导致没有返回坐标轴相关信息 if (!value) { return rst; } }
解决方案,重写Geometry的getSnapRecords方法const resetGeometryGetSnapRecords = () => { Geometry.prototype.getSnapRecords = function (point, inCoordRange?) { // 省略代码 const value = this._getXSnap(invertPoint.x); // 放过value为0的场景 if (!value && value !== 0) { return rst; } return rst; }}
折线图反转后表现不一致该问题为折线图展示的线不一致问题第一个图为两条线,第二个图为一条线查看Line相关代码,发现折线图在render的时候通过this.mapping()获取了对应的记录点折线图没有处理坐标反转时的坐标import { jsx } from '../../jsx';import { isArray } from '@antv/util';import Geometry from '../geometry';import { LineProps } from './types';export default (View) => { return class Line extends Geometry<LineProps> { splitNulls(points, connectNulls) { // 该方法只是判断了y轴是否为空,但是坐标轴反转的时候需要判断x轴是否为空 for (let i = 0, len = points.length; i < len; i++) { const point = points[i]; const { y } = point; if (isArray(y)) { if (isNaN(y[0])) { if (tmpPoints.length) { result.push(tmpPoints); tmpPoints = []; } continue; } tmpPoints.push(point); continue; } if (isNaN(y)) { if (tmpPoints.length) { result.push(tmpPoints); tmpPoints = []; } continue; } tmpPoints.push(point); } if (tmpPoints.length) { result.push(tmpPoints); } return result; } mapping() { return records.map((record) => { // 获取坐标点位 const splitPoints = this.splitNulls(points, connectNulls); }); } render() { // 获取点位信息 const records = this.mapping(); // 省略其他代码... return <View {...props} coord={coord} records={records} clip={clip} />; } };};
解决方案,重写Line的splitNulls方法 Line.prototype.splitNulls = function (points, connectNulls) { const result = []; let tmpPoints = []; for (let i = 0, len = points.length; i < len; i++) { const point = points[i]; const { x, y } = point; / start 打补丁,处理坐标轴转换引起折线渲染链接问题 / if (this.props.coord.transposed) { if (Array.isArray(x)) { if (isNaN(x[0])) { if (tmpPoints.length) { result.push(tmpPoints); tmpPoints = []; } continue; } tmpPoints.push(point); continue; } if (isNaN(x)) { if (tmpPoints.length) { result.push(tmpPoints); tmpPoints = []; } continue; } } / end 打补丁,处理坐标轴转换引起折线渲染链接问题 / } return result; };
总结以上我们通过分析世面上的图表库,选择了在钉钉小程序中使用F2图表库进行开发因为小程序不支持DOM相关API和Canvas不是标准的CanvasRenderingContext2D对象,接入过程中踩了不少的坑,对新人不太友好不过最后还是根据官方的接入文档完成了小程序的接入强烈建议F2官网可以在官网中加入Taro的接入,降低使用门槛作者:千梦凯 来源-微信公众号:Goodme前端团队出处:https://mp.weixin.qq.com/s/sIfIFDkCaU0HLRFAk0n4Hw(图片来源网络,侵删)
0 评论