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

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

背景

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

为方便理解,稍微解释下图形语法的概念,已经了解的小伙伴可以跳过这一段。

图形语法

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

图形语法与一般的图表主要区别在于:图形语法只要修改下语法描述,就能得到完全不同的图形,而一般的图表需要增加图表类型。图形语法可描述的图形是近乎无限的,而图表类型是有限的。

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

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

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

语法中调整坐标度量,并增加不同颜色后,变成了更完善的玫瑰图

继续调整语法参数,最终可得到饼图

在这个例子中,如果用一般图表(如 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. 把东西画出来
  2. 把交互串起来

把东西画出来

核心思想:将 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/339275513

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万条事件量。

点击这里,立即申请

Flutter ChartSpace:通过跨端 Canvas 实现图表库的更多相关文章

  1. 准备开发一个基于canvas的图表库,记录一些东西(一)

    开源的图表库已经有很多了,这里从头写个自己的,主要还是 提高自己js的水平,增加复杂代码组织的经验 首先写一个画图的库,供以后画图表使用.经过2天的开发,算是能拿出点东西了,虽然功能还很弱,但是有了一 ...

  2. 18个基于 HTML5 Canvas 开发的图表库

    如今,HTML5 可谓如众星捧月一般,受到许多业内巨头的青睐.很多Web开发者也尝试着用 HTML 5 来制作各种各样的富 Web 应用.HTML 5 规范引进了很多新特性,其中之一就是 Canvas ...

  3. ECharts-基于Canvas,纯Javascript图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表

    ECharts http://ecomfe.github.com/echarts 基于Canvas,纯Javascript图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表.创新的拖拽重计算 ...

  4. 基于html5 Canvas图表库 : ECharts

    ECharts开源来自百度商业前端数据可视化团队,基于html5 Canvas,是一个纯Javascript图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表.创新的拖拽重计算.数据视图.值 ...

  5. 【译】使用 Flutter 实现跨平台移动端开发

    作者: Mike Bluestein   | 原文地址:[https://www.smashingmagazine.com/2018/06/google-flutter-mobile-developm ...

  6. React / Vue 跨端渲染原理与实现探讨

    跨端渲染是渲染层并不局限在浏览器 DOM 和移动端的原生 UI 控件,连静态文件乃至虚拟现实等环境,都可以是你的渲染层.这并不只是个美好的愿景,在今天,除了 React 社区到 .docx / .pd ...

  7. Chart.js – 效果精美的 HTML5 Canvas 图表库

    Chart.js 是一个令人印象深刻的 JavaScript 图表库,建立在 HTML5 Canvas 基础上.目前,它支持6种图表类型(折线图,条形图,雷达图,饼图,柱状图和极地区域区).而且,这是 ...

  8. 重磅!滴滴跨端框架Chameleon 1.0正式发布

    滴滴在 GitHub 上开源的跨端解决方案 Chameleon(简写 CML)正式发布 1.0 版本,中文名卡梅龙:中文意思变色龙,意味着就像变色龙一样能适应不同环境的企业级跨端整体解决方案,具有易用 ...

  9. 基于canvas实现的高性能、跨平台的股票图表库--clchart

    什么是 ClChart? ClChart是一个基于canvas创建的简单.高性能和跨平台的股票数据可视化开源项目.支持PC.webApp以及React Native和Weex等平台.在React Na ...

随机推荐

  1. 【然天一】随机读写(4k)百盘天梯

    随机读写适用于大量小文件的读写,是最贴近办公和编程的使用场景.现在很多硬盘厂商只宣传它们的连续读写(Seq),但除了游戏和视频剪辑场景之外并没有什么卵用. 总结一下: 傲腾秒杀全部 NAND SLC ...

  2. ApacheCN Kali Linux 译文集 20211020 更新

    Kali Linux 秘籍 中文版 第一章 安装和启动Kali 第二章 定制 Kali Linux 第三章 高级测试环境 第四章 信息收集 第五章 漏洞评估 第六章 漏洞利用 第七章 权限提升 第八章 ...

  3. python--003 数据类型

    运算符 in "heilo" in "adfsdfsdfsdfsdfdsfdshellofsdfdsf" "li"  in ["l ...

  4. JDK8 的 Lambda、Stream、LocalDate

    前言 本篇主要讲述是Java中JDK1.8的一些新语法特性使用,主要是Lambda.Stream和LocalDate日期的一些使用讲解. 作者:虚无境 来源:cnblogs.com/xuwujing/ ...

  5. MySQL--数据表操作--行转列和列转行

    原创:转载需注明原创地址 https://www.cnblogs.com/fanerwei222/p/11782549.html MySQL--数据表操作: 会用到的查询操作. 1. Limit的用法 ...

  6. Windows office2019免费激活,附代码

    office2019地址:链接:https://pan.baidu.com/s/1zPt5U7b0L-bGHl5AOtYs2w提取码:m5ei 新建一个txt,然后把这段代码放进去,然后保存关闭改后缀 ...

  7. HTML笔记整理--下节

    欢迎来到HTML基础笔记下节部分! 内联样式 当特殊的样式需要应用到个别元素时,就可以使用内联样式. 使用内联样式的方法是在相关的标签中使用样式属性.样式属性可以包含任何 CSS 属性.以下实例显示出 ...

  8. RHCSA阶段笔记

    命令终端字段含义介绍 [root@localhost ~]# 解释: root:当前登录系统用户名(root超级管理员) localhost :当前主机名 :当前用户所在目录( 为家目录) ,root ...

  9. Solution -「CF 804F」Fake bullions

    \(\mathcal{Description}\)   Link.   给定 \(n\) 个点的竞赛图,第 \(i\) 个点代表了 \(s_i\) 个人,每个人(0-based)可能有真金条.此后在 ...

  10. 【ybtoj】贪心算法例题

    [基础算法]第二章 贪心算法 例一 奶牛晒衣服 题目描述 有n件衣服,第i件衣服的湿度为h. 在自然条件下,每件衣服每分钟都可以自然晒干A点湿度. 在烘干机作用下,可以选择一件衣服,用一分钟的时间晒干 ...