可视化图表库F2在飞书(头条)小程序中的适配

2,820 阅读8分钟

F2 是蚂蚁金服开源的一个专注于移动的可视化图表库,特点为专注移动,体验优雅,图表丰富,组件完备。但是因其目前只适配了H5,微信小程序和支付宝小程序,飞书小程序还未能很好的适配,故如果想在飞书小程序上使用需自行进行适配。


飞书小程序中Canvas API与原生的差异

F2 默认基于 Html5 Canvas 进行图表绘制,但是对于当前运行时环境来说,只要能够提供和 Html5 Canvas 上下文环境一样的上下文接口,一样也能使用 F2 进行图表绘制,我们先对照文档对接口进行整理。

下表为整理并测试后的接口对照,绿色表示已支持,黄色表示未支持(虽然飞书文档中描述为支持,但是实际并未实现)。

功能HTML5飞书小程序
绘制一个填充的矩形fillRect(x, y, width, height)
绘制一个矩形的边框strokeRect(x, y, width, height)
清除指定矩形区域,让清除部分完全透明clearRect(x, y, width, height)
新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径beginPath()
闭合路径之后图形绘制命令又重新指向到上下文中closePath()
通过线条来绘制图形轮廓stroke()
通过填充路径的内容区域生成实心的图形fill()
将笔触移动到指定的坐标x以及y上moveTo(x, y)
绘制一条从当前位置到指定x以及y位置的直线lineTo(x, y)
画一个以(x,y)为圆心的以radius为半径的圆弧(圆),从startAngle开始到endAngle结束,按照anticlockwise给定的方向(默认为顺时针)来生成arc(x, y, radius, startAngle, endAngle, anticlockwise)
根据给定的控制点和半径画一段圆弧,再以直线连接两个控制点arcTo(x1, y1, x2, y2, radius)
绘制二次贝塞尔曲线,cp1x,cp1y为一个控制点,x,y为结束点quadraticCurveTo(cp1x, cp1y, x, y)
绘制三次贝塞尔曲线,cp1x,cp1y为控制点一,cp2x,cp2y为控制点二,x,y为结束点bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
绘制一个左上角坐标为(x,y),宽高为width以及height的矩形rect(x, y, width, height)
设置图形的填充颜色fillStyle = color
设置图形轮廓的颜色strokeStyle = color
这个属性影响到 canvas 里所有图形的透明度,有效的值范围是 0.0 (完全透明)到 1.0(完全不透明),默认是 1.0globalAlpha = transparencyValue
设置线条宽度lineWidth = value
设置线条末端样式lineCap = type
设定线条与线条间接合处的样式lineJoin = type
限制当两条线相交时交接处最大长度;所谓交接处长度(斜接长度)是指线条交接处内角顶点到外角顶点的长度miterLimit = value
返回一个包含当前虚线样式,长度为非负偶数的数组getLineDash()
设置当前虚线样式setLineDash(segments)
设置虚线样式的起始偏移量lineDashOffset = value
我们可以用线性或者径向的渐变来填充或描边。我们用下面的方法新建一个 canvasGradient 对象,并且赋给图形的 fillStyle 或 strokeStyle 属性。createLinearGradient(x1, y1, x2, y2)
createRadialGradient(x1, y1, r1, x2, y2, r2)
gradient.addColorStop(position, color)
该方法接受两个参数。Image 可以是一个 Image 对象的引用,或者另一个 canvas 对象。Type 必须是下面的字符串值之一:repeat,repeat-x,repeat-y 和 no-repeatcreatePattern(image, type)
shadowOffsetX 和 shadowOffsetY 用来设定阴影在 X 和 Y 轴的延伸距离,它们是不受变换矩阵所影响的。负值表示阴影会往上或左延伸,正值则表示会往下或右延伸,它们默认都为 0shadowOffsetX = float
shadowOffsetX 和 shadowOffsetY 用来设定阴影在 X 和 Y 轴的延伸距离,它们是不受变换矩阵所影响的。负值表示阴影会往上或左延伸,正值则表示会往下或右延伸,它们默认都为 0shadowOffsetY = float
shadowBlur 用于设定阴影的模糊程度,其数值并不跟像素数量挂钩,也不受变换矩阵的影响,默认为 0shadowBlur = float
shadowColor 是标准的 CSS 颜色值,用于设定阴影颜色效果,默认是全透明的黑色shadowColor = color
在指定的(x,y)位置填充指定的文本,绘制的最大宽度是可选的fillText(text, x, y [, maxWidth])
在指定的(x,y)位置绘制文本边框,绘制的最大宽度是可选的strokeText(text, x, y [, maxWidth])
当前我们用来绘制文本的样式. 这个字符串使用和 CSS font 属性相同的语法. 默认的字体是 10px sans-seriffont = value
文本对齐选项. 可选的值包括:start, end, left, right or center. 默认值是 starttextAlign = value
基线对齐选项. 可选的值包括:top, hanging, middle, alphabetic, ideographic, bottom。默认值是 alphabetictextBaseline = value
文本方向。可能的值包括:ltr, rtl, inherit。默认值是 inheritdirection = value
将返回一个 TextMetrics对象的宽度、所在像素,这些体现文本特性的属性measureText()
其中 image 是 image 或者 canvas 对象,x 和 y 是其在目标 canvas 里的起始坐标drawImage(image, x, y)
这个方法多了2个参数:width 和 height,这两个参数用来控制 当向canvas画入时应该缩放的大小drawImage(image, x, y, width, height)
第一个参数和其它的是相同的,都是一个图像或者另一个 canvas 的引用。其它8个参数最好是参照右边的图解,前4个是定义图像源的切片位置和大小,后4个则是定义切片的目标显示位置和大小。drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
保存画布(canvas)的所有状态save()
save 和 restore 方法是用来保存和恢复 canvas 状态的,都没有参数。Canvas 的状态就是当前画面应用的所有样式和变形的一个快照restore()
translate 方法接受两个参数。x 是左右偏移量,y 是上下偏移量,如右图所示translate(x, y)
这个方法只接受一个参数:旋转的角度(angle),它是顺时针方向的,以弧度为单位的值rotate(angle)
scale 方法可以缩放画布的水平和垂直的单位。两个参数都是实数,可以为负数,x 为水平缩放因子,y 为垂直缩放因子,如果比1小,会比缩放图形, 如果比1大会放大图形。默认值为1, 为实际大小scale(x, y)
这个方法是将当前的变形矩阵乘上一个基于自身参数的矩阵transform(m11, m12, m21, m22, dx, dy)
这个方法会将当前的变形矩阵重置为单位矩阵,然后用相同的参数调用 transform 方法。如果任意一个参数是无限大,那么变形矩阵也必须被标记为无限大,否则会抛出异常。从根本上来说,该方法是取消了当前变形,然后设置为指定的变形,一步完成setTransform(m11, m12, m21, m22, dx, dy)
重置当前变形为单位矩阵,它和调用以下语句是一样的
ctx.setTransform(1, 0, 0, 1, 0, 0)
resetTransform()
将当前正在构建的路径转换为当前的裁剪路径clip()

已支持属性或方法:

console.log('fillRect', context.fillRect);
console.log('strokeRect', context.strokeRect);
console.log('clearRect', context.clearRect);
console.log('beginPath', context.beginPath);
console.log('closePath', context.closePath);
console.log('stroke', context.stroke);
console.log('fill', context.fill);
console.log('moveTo', context.moveTo);
console.log('lineTo', context.lineTo);
console.log('arc', context.arc);
console.log('arcTo', context.arcTo);
console.log('quadraticCurveTo', context.quadraticCurveTo);
console.log('bezierCurveTo', context.bezierCurveTo);
console.log('rect', context.rect);
console.log('drawImage', context.drawImage);
console.log('save', context.save);
console.log('restore', context.restore);
console.log('translate', context.translate);
console.log('rotate', context.rotate);
console.log('scale', context.scale);
console.log('transform', context.transform);
console.log('setTransform', context.setTransform);
console.log('resetTransform', context.resetTransform);
console.log('clip', context.clip);
console.log('font', context.font);          

未支持属性或方法:

console.log('fillStyle', context.fillStyle);
console.log('strokeStyle', context.strokeStyle);
console.log('globalAlpha', context.globalAlpha);
console.log('lineWidth', context.lineWidth);
console.log('lineCap', context.lineCap);
console.log('lineJoin', context.lineJoin);
console.log('miterLimit', context.miterLimit);
console.log('lineDashOffset', context.lineDashOffset);
console.log('createRadialGradient', context.createRadialGradient);
console.log('createPattern', context.createPattern);
console.log('shadowOffsetX', context.shadowOffsetX);
console.log('shadowOffsetY', context.shadowOffsetY);
console.log('shadowBlur', context.shadowBlur);
console.log('shadowColor', context.shadowColor);
console.log('fillText', context.fillText);
console.log('strokeText', context.strokeText);
console.log('textAlign', context.textAlign);
console.log('textBaseline', context.textBaseline);
console.log('direction', context.direction);

通过测试发现,以上为支持的接口飞书中其实已有实现,只是方法不一样:

console.log('setFillStyle', context.setFillStyle);
console.log('setStrokeStyle', context.setStrokeStyle);
console.log('setGlobalAlpha', context.setGlobalAlpha);
console.log('setLineWidth', context.setLineWidth);
console.log('setLineCap', context.setLineCap);
console.log('setLineJoin', context.setLineJoin);
console.log('setMiterLimit', context.setMiterLimit);
console.log('setTextAlign', context.setTextAlign);
console.log('setTextBaseline', context.setTextBaseline);
console.log('setFontSize', context.setFontSize);
console.log('setShadow', context.setShadow);

飞书小程序适配

对于Canvas属性上的差异,我们可以使用 Object.defineProperty(obj, prop, descriptor) 为相关的属性提供 setter 的方法,代码如下:

// libs\f2\context.js 飞书小程序的context适配

const CAPITALIZED_ATTRS_MAP = {
  fillStyle: 'FillStyle',
  fontSize: 'FontSize',
  globalAlpha: 'GlobalAlpha',
  opacity: 'GlobalAlpha',
  lineCap: 'LineCap',
  lineJoin: 'LineJoin',
  lineWidth: 'LineWidth',
  miterLimit: 'MiterLimit',
  strokeStyle: 'StrokeStyle',
  textAlign: 'TextAlign',
  textBaseline: 'TextBaseline',
  shadow: 'Shadow',
  font: 'Font'
};

export default ctx => {
  Object.keys(CAPITALIZED_ATTRS_MAP).map(key => {
    Object.defineProperty(ctx, key, {
      set(value) {
        // 记录最新设置的值
        ctx[`__${key}`] = value;
        if (key === 'shadow' && ctx.setShadow && Array.isArray(value)) {
          ctx.setShadow(value[0], value[1], value[2], value[3]);
          return;
        }
        const name = 'set' + CAPITALIZED_ATTRS_MAP[key];
        if (!ctx[name]) {
          return;
        }
        ctx[name](value);
      }
    });
    return key;
  });
  return ctx;
};

然后我们就可以使用了

<!-- pages/index/index.html -->
<view class="chart-wrapper">
    <canvas canvas-id="chart" id="chart" class="chart"></canvas>
</view>
.chart-wrapper {
  width: 638rpx;
  height: 469rpx;
  margin: 0 auto;
}
.chart {
  width: 638rpx;
  height: 469rpx;
}
// pages/index/index.js
// f2-all.js用的是这里的 node_modules\@antv\f2\build\f2-all.js
import F2 from '../../libs/f2/f2-all.js';
import F2Context from '../../libs/f2/context.js';

Page({

  onLoad() {
    this.createChart();
  },

  createChart() {
    tt.createSelectorQuery()
      .select(`#chart`) // id
      .boundingClientRect((rect) => {
        const data = [
          { genre: 'Sports', sold: 275 },
          { genre: 'Strategy', sold: 115 },
          { genre: 'Action', sold: 120 },
          { genre: 'Shooter', sold: 350 },
          { genre: 'Other', sold: 150 }
        ];
        const { width, height } = rect;
        const { pixelRatio } = tt.getSystemInfoSync();
        const context = F2Context(tt.createCanvasContext(`chart`)); // canvas-id
        const config = { context, width, height, pixelRatio };
        this.chart = new F2.Chart(config);
        this.chart.source(data);
        this.chart.interval()
          .position('genre*sold')
          .color('genre');
        this.chart.render();

      }).exec()
  }
});

这里会有报错,因为小程序里的颜色值可能不支持简写

我们把f2-all.js中对应的值补全,#FFF替换为#FFFFFF,#000替换为#000000,#999替换为#999999 适配后效果:

最后,为图表加上事件监听

<!-- pages/index/index.html -->
<view class="chart-wrapper">
    <canvas canvas-id="chart" id="chart" class="chart" bindtouchstart="touchStart" bindtouchmove="touchMove"></canvas>
</view>
// pages/index/index.js
...
  touchStart(e) {
    if (this.chart) {
      this.chart.get('el').dispatchEvent('touchstart', e);
    }
  },
  touchMove(e) {
    if (this.chart) {
      this.chart.get('el').dispatchEvent('touchmove', e);
    }
  },

至此,F2在小程序也就可以完美使用了