关于flutter:Flutter-ChartSpace通过跨端-Canvas-实现图表库

37次阅读

共计 5779 个字符,预计需要花费 15 分钟才能阅读完成。

基于 Flutter 的图形语法库,通过跨端 Canvas,将基于 Javascript 的图形语法库 ChartSpace 扩大至 Flutter 端

作者:字节跳动终端技术——胡珀

背景

数据平台有个基于图形语法的图表库 ChartSpace,反对 web/h5/mini program,当初收到业务诉求,要反对到 Flutter 端。

为不便了解,略微解释下图形语法的概念,曾经理解的小伙伴能够跳过这一段。

图形语法

图形语法(grammar of graphics) 是通过一套语法来形容任意图形,次要来自 Wilkinson 的《The Grammar of Graphics》,可参考文章:https://zhuanlan.zhihu.com/p/…

图形语法与个别的图表次要区别在于:图形语法只有批改下语法形容,就能失去齐全不同的图形,而个别的图表须要减少图表类型。图形语法可形容的图形是近乎有限的,而图表类型是无限的。

举个例子(截取自:https://segmentfault.com/a/11…):

如果咱们基于图形语法绘制了柱状图

将语法中的坐标系换成极坐标后,会变成玫瑰图

语法中调整坐标度量,并减少不同色彩后,变成了更欠缺的玫瑰图

持续调整语法参数,最终可失去饼图

在这个例子中,如果用个别图表(如 ECharts),须要至多 4 个图表类型,图表数据的格局也可能存在区别。但应用图形语法形容,只须要调整不同的语法参数,就能失去不同的图形。

图形语法通过调整语法参数,失去不同的图形,给数据的表白提供了更大的空间,属于更业余的图表引擎,但同样也带来了较为简单的语法规定。

基于图形语法,前端(JS 语法)罕用的图表库:

  • G2:蚂蚁金服基于图形语法的图表库,图形语法通过 js 语法应用
const ds = new DataSet();

const chart = new Chart({...});

...

const dv2 = ds.createView().source(dv1.rows);
dv2.transform({
  type: 'regression',
  method: 'polynomial',
  fields: ['year', 'death'],
  bandwidth: 0.1,
  as: ['year', 'death']
});

const view2 = chart.createView();
view2.axis(false);
view2.data(dv2.rows);

...

chart.render();
  • Vega:开源的图形语法框架,图形语法通过 json 配置应用
{
  width : 500,
  height : 200,
  config : {
    axis : {
      grid : true,
      gridColor :  #dedede 
    }
  },
  ...
}
  • ChartSpace:字节跳动基于图形语法的图表库,图形语法通过 json 配置应用,语法与 Vega 相近
{
     type :  line ,
     data : [],
     labels : {visible : false},
     axes : [
        {orient :  left},
        {orient :  bottom}
    ],
     xField :  x ,
     yField :  y 
}

在跨端,跨语言的状况下,json 配置的语法领有更好的多端一致性。后端保留一套雷同的 json 配置,能够在多端绘制出雷同的图形。

图表库 ChartSpace

ChartSpace 是字节数据平台基于图形语法的图表库,已反对 web/h5/mini program,当初要反对到 flutter 端。

业务上冀望多端协同,同一份数据在不同端上有一致性的体现,以折线图为例:

计划

惯例的计划是实现一套 flutter 版的图形语义,解析 chartspace 的语义配置,绘制成雷同规格的图形。但这种计划带来的开发成本比拟高,所以咱们抉择了另一套计划:跨端 canvas。

原理就是将 chartspace (js) 所应用的 web canvas 上绘制的内容,通过跨端技术给出现到 flutter canvas 上来。

临时无奈在飞书文档外展现此内容

实现这个计划,要解决两个问题:

  1. 把货色画进去

<!—->

  1. 把交互串起来

把货色画进去

核心思想:将 chartspace(js) 的 canvas 绘制指令执行从 js 转移到 flutter 执行,指标是对齐 Flutter Canvas 和 Web Canvas。

实现形式是:在 JS 中通过结构 Mock Canvas 对象,录制 canvas 指令,而后发送到 Flutter 侧,通过 Flutter Canvas 来实现这些指令。

次要工作量在于用 Flutter Canvas 实现一套 Web Canvas 的 API。

临时无奈在飞书文档外展现此内容

把交互串起来

用户交互的输出是 touch 事件,只须要将 Flutter PointerEvent 转换为 Web TouchEvent,输出到 chartspace 即可。

之后 chartspace 会产生新的 canvas 指令,在 Flutter Canvas 中绘制出新的内容,流程和首次渲染一样,至此交互就残缺了。

临时无奈在飞书文档外展现此内容

成果

实现后成果如下,tooltip 的成果是手指点击后产生的。

获得的收益是:低成本实现,低成本保护,跨端一致性。

渲染性能比照:

开发期间做过很多优化,graph 渲染工夫从 80ms 优化到 50ms,咱们还在继续优化,争取做到靠近原生的体验。后续咱们其余小伙伴会分享优化的思路和实际。

跨端 Canvas 纯 Flutter
graph 渲染 52ms 20ms
tooltip 渲染 9ms 0ms

跨端 Canvas 的数据是从用户输出开始,到渲染图形完结,蕴含了 bridge 传输,chartspace (js) 生成 canvas 指令的工夫。

纯 Flutter 是将雷同的 canvas 指令变成 Flutter 代码后的执行工夫。

能够看到渲染性能与纯 Flutter 模式有肯定差距,但也在可承受范畴内,失常图表交互时,用户很难感知到区别。

咱们置信,雷同的图表如果本人绘制,应该能有更好的性能,在 canvas 的指令优化 和 Web Canvas API 的实现上,还有肯定的优化空间。

踩坑 & 解决方案

实际过程中,遇到了很多问题,这里选取几类有代表性的分享一下

Canvas 生命周期不同

生命周期区别如下:

Flutter Canvas Web Canvas
渲染不会保留画布 渲染会保留画布
每次都是从新绘制 在上一次的根底上持续绘制

咱们的解决方案是,保留渲染后的后果,在上一次的渲染后果上持续绘制

@override
  void paint(Canvas canvas, Size size) {final paintList = _repaint.consume();
    ui.Picture picture = canvasRecorder.record(canvasId, size, _repaint.reverse, paintList);
    if (picture != null) {canvas.drawPicture(picture);
    }
  }

Canvas Context 不同

Context 区别如下:

Flutter Canvas Web Canvas
canvas.save 保留的内容:Saves a copy of the current transform and clip on the save stack. ctx.save 保留的内容:
每次 paint 都是全新的 canvas 实例 canvas 创立后,实例不变

针对第一个问题,save / restore 的内容不统一,咱们创立了 WebCanvas 对象以模仿 Web 上的 Canvas,手动治理 save / restore 的内容

class WebCanvas {
  ...
  
  SaveStack saveStack = SaveStack();
  SaveInfo get current => saveStack.current;
  
  ...
}

针对第二个问题,咱们创立了 CanvasRecorder 对象,并在该对象中持有 WebCanvas 实例,与 Web 上的 Canvas 实例的生命周期保持一致

class CanvasRecorder {
  ...

  CanvasHistory getCanvasHistory(String canvasId) {if (!hisMap.containsKey(canvasId)) {hisMap.putIfAbsent(canvasId, () => CanvasHistory(canvasId));
    }
    return hisMap[canvasId];
  }
  
  ...
}
class CanvasHistory {
  ...
  
  final ChartSpaceCanvas chartSpaceCanvas = ChartSpaceCanvas();
  
  ...
}
class ChartSpaceCanvas {
  ...

  final WebCanvas webcanvas = WebCanvas();
  
  ...
}

Canvas 默认值不同

Canvas 默认值不同的中央较多,咱们间接按 Web Canvas 的规范设置了默认值,没有认真统计过差别,粗略来说有以下属性有区别:

  • transform

<!—->

  • fillStyle

<!—->

  • strokeStyle

<!—->

  • strokeMiterLimit

<!—->

  • font

以 transform 为例,transform 理论保护的是一个 4 * 4 的变换矩阵(DOMMatrix 对象),web 上 setTransform 办法设置的是变换矩阵不同地位的值

Flutter 上是间接操作这个变换矩阵

然而 Web Canvas 和 Flutter Canvas 的变换矩阵默认值不统一

Flutter Canvas Web Canvas
0 0 0 00 0 0 00 0 0 00 0 0 0 1 0 0 00 1 0 00 0 1 00 0 0 1

所以解决方案如下:

class Matrix4Builder {static Matrix4 webDefault() {final matrix4 = Matrix4.zero();
    matrix4.setEntry(0, 0, 1.0);
    matrix4.setEntry(1, 1, 1.0);
    matrix4.setEntry(2, 2, 1.0);
    matrix4.setEntry(3, 3, 1.0);
    return matrix4;
  }
}

Bridge 须要同步 API

咱们通过 Mock CanvasRenderdingContext 对象,来达到录制 canvas 指令的目标,然而 CanvasRenderdingContext 对象上有很多办法须要同步 API,比拟高频的比方 measureText。

然而惯例的 Bridge 通信是

临时无奈在飞书文档外展现此内容

其中 Flutter 与 iOS/Android 的通信是异步的,所以这里应用 FFI 间接与 JS Runtime 通信能力保障同步

临时无奈在飞书文档外展现此内容

截取局部代码实现:

Pointer<Utf8> funcMeasureTextCString = Utf8.toUtf8('measureText');
var measureTextFunctionObject = jSObjectMakeFunctionWithCallback(
    _globalContext,
    jSStringCreateWithUTF8CString(funcMeasureTextCString),
    Pointer.fromFunction(measureTextFunction));
jSObjectSetProperty(
    _globalContext,
    _globalObject,
    jSStringCreateWithUTF8CString(funcMeasureTextCString),
    measureTextFunctionObject,
    jsObject.JSPropertyAttributes.kJSPropertyAttributeNone,
    nullptr);
free(funcMeasureTextCString);

总结 & 瞻望

总结一下,咱们通过跨端 Canvas 的形式,低成本实现了 Flutter ChartSpace,实际下来获得了不错的性能体现。

这也得益于 ChartSpace 自身正当的架构设计,通过 json 配置来定义图形语义,能无效屏蔽不同平台,语言的差别。

因为 ChartSpace 是基于图形语义的实现,相比定制化的图表类型,须要更大的计算量,会影响渲染性能。但当初也反对了分步渲染,在大数据和简单的图形下,能以渐进式的成果逐渐出现残缺图形,对用户体验并没有侵害。

Flutter ChartSpace 临时还没反对分步渲染,以后的计划还有很大的优化空间,咱们会持续摸索。

将来思考在两个方向上持续拓展:

设计易用性更高的 API

图形语法尽管很弱小,也带来了应用上的复杂度,咱们能够在图形语法上包装一层 API,将罕用的图形给剥离进去,升高应用老本。

比方蚂蚁团体的 g2plot 就是在 g2 根底上的封装,提供了更简洁的语法,援用 g2plot 的一段形容

相干形容来自:https://zhuanlan.zhihu.com/p/…

const line = new Line('container', {
  data,
  xField: 'year',
  yField: 'value',
});

line.render();

大家能够比照下 g2plot 的语法示例和 g2 的语法示例,g2 的语法在文章的图形语法一节。

拓展更多的端 / 技术栈

实际下来后,咱们发现,雷同的技术能够拓展至更多的技术栈,比方:iOS/Android/RN

临时无奈在飞书文档外展现此内容

开源

ChartSpace 和 Flutter ChartSpace 都是字节外部的产品。ChartSpace 在大量的数据产品,和其余业务中一直打磨,禁受了不同场景的考验,包含抖音的数据分析,当初曾经有开源打算了。Flutter ChartSpace 也须要在外部场景打磨后,再思考开源。

Flutter ChartSpace 会在 ChartSpace 之后开源,预期是今年年底。


🔥 火山引擎 APMPlus 利用性能监控是火山引擎利用开发套件 MARS 下的性能监控产品。咱们通过先进的数据采集与监控技术,为企业提供全链路的利用性能监控服务,助力企业晋升异样问题排查与解决的效率。目前咱们面向中小企业特地推出 「APMPlus 利用性能监控企业助力口头」,为中小企业提供利用性能监控免费资源包。当初申请,有机会取得60 天 收费性能监控服务,最高可享 6000 万 条事件量。
👉 点击这里,立刻申请

正文完
 0