当卧龙遇上凤雏:钉钉小程序+F2图表库的踩坑指南

作者:千梦凯

前言

古茗加盟商在经营店铺的过程中,需要一款可视化的工具来将门店的经营情况。通过图表将数据呈现得更加直观、易懂;并且可以根据数据趋势,分析门店经营情况。

目前古茗主要通过钉钉小程序来辅助加盟商经营,并且开发使用Taro+React的方式进行开发小程序,本文将介绍如何使用Taro在钉钉小程序中实现数据图表化展示。

可视化图表库技术选型

目前市面上比较流行的可视化工具库有

  • ECharts:一个基于 JavaScript 的开源可视化图表库官网地址
  • Chart.js:基于HTML5的简单易用的JavaScript图表库 官方网站
  • D3.js:一个数据驱动的可视化库,通过使用HTML、SVG和CSS来操作数据,创造出交互式和动态的图表官方网站
  • AntV:蚂蚁企业级数据可视化解决方案官网地址

在wbe端我们使用任何一个库都可以实现可视化展示。不过针对钉钉小程序,我们要根据一下几个关键字来进行筛选使用的图表库(移动端小程序手势交互

  • Echarts上面有丰富的图表库,但是代码体积过大。全量代码将近1000k,选择常用功能也要500k左右,在小程序主包只有2m的情况下还是占用了太多的内容。
  • Chart.js 依赖于DOM API,而钉钉小程序的环境并不提供完整的DOM API。所以无法直接使用
  • D3.js是通过使用SVG来支持图表,但是目前小程序不支持使用SVG,所以无法在小程序中使用
  • Antv/F2 F2 是一个专注于移动端,面向常规统计图表,开箱即用的可视化引擎,完美支持 H5 环境同时兼容多种环境(Node, 小程序)

对比之下,F2图表方案更适合在小程序中使用。

由于我们在接入图表库时,4.x版本尚未更新,所以第一版本使用了3.x版本。在后续F2的更新迭代中,由于图表库是在主包中展示,考虑到文件大小的原因,最终使用了4.x版本。

如果是初次接入,推荐直接使用最新版本,功能更加强大。

在Taro+React中使用F2 4.x

在查看F2的文档时,发现文档中有React和小程序的接入,并没有教程说明如何使用Taro的接入。既然可以在小程序和React工程中使用,理论上在Taro+React工程中也可以使用。说干就干,让我们先跟着文档上面的React教程进行接入。

创建Taro+React工程

根据Taro文档,创建React工程,并且新增钉钉小程序编译选项文档教程

安装F2相关依赖

PowerShell 复制代码
npm install @antv/f2 --save
npm install @antv/f2-react --save

在index文件中复制F2官网Demo

typescript 复制代码
import 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绘图上下文,调整相关代码

typescript 复制代码
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宽高

scala 复制代码
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对象

scala 复制代码
// 调整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方案设置

scala 复制代码
// 调整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元素设置高分辨率宽高来解决

kotlin 复制代码
// 调整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文件

typescript 复制代码
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差异

图表虽然可以正常展示了,不过并没有坐标信息。当我们尝试给图标添加坐标信息时,发现页面代码报错会报错

typescript 复制代码
    <F2Canvas id='wrap'>
      <Chart data={data}>
        <Interval x='genre' y='sold' />
        <Axis field='genre' /> 
      </Chart>
    </F2Canvas>

通过查看在小程序中使用F2相关文档,发现F2 是基于 CanvasRenderingContext2D 的标准接口绘制的,但是小程序中给的 context 对象不是标准的 CanvasRenderingContext2D , 所以需要将context对象进行封装兼容处理,详情可见: github.com/antvis/f2-c..., 其他小程序也可以按同样的思路封装。继续修改相关代码,抹平小程序context差异。

scala 复制代码
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容器组件事件时,触发图表组件事件

typescript 复制代码
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获取当前的选中的元素信息,可以拿到对应的位置信息与数据信息。从而可以自定义显示对应文本

ini 复制代码
// 自定义配置 
<Tooltip custom ref={tooltipRef} triggerOn="click" onChange={handleTooltipChange} />

View标签位置获取

ini 复制代码
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方法

arduino 复制代码
  show(point, _ev?) {
      const { props } = this;
      const { chart } = props;
      // 该代码获取坐标相关位置
      const snapRecords = chart.getSnapRecords(point, true); // 超出边界会自动调整

      this.showSnapRecords(snapRecords);
  }

Chart的getSnapRecords方法

kotlin 复制代码
  getSnapRecords(point, inCoordRange?) {
    const geometrys = this.getGeometrys();
    if (!geometrys.length) return;
    // geometrys[0]为点击的相关线Line
    return geometrys[0].getSnapRecords(point, inCoordRange);
  }

Line的getSnapRecords方法继承Geometry中的getSnapRecords

arduino 复制代码
  getSnapRecords(point, inCoordRange?): any[] {
    // 该处理没有对value等于0的判断,导致没有返回坐标轴相关信息
    if (!value) {
      return rst;
    }
  }
解决方案,重写Geometry的getSnapRecords方法
javascript 复制代码
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()获取了对应的记录点

折线图没有处理坐标反转时的坐标

javascript 复制代码
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方法
ini 复制代码
  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前端团队」,获取更多干货实践,欢迎交流分享。

相关推荐
却尘15 分钟前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare16 分钟前
浅浅看一下设计模式
前端
Lee川20 分钟前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix1 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人1 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl1 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空1 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust