G6-Mobile 的前世今生

除了最常见的图表,还有一类关系图可视化,关系图相对于通用图表,数据之间关系复杂或难以确定,所以可视化时要考虑:

  • 各种布局,它决定了如何展示图形的关系
  • 复杂的关系节点
  • 多样化的交互

之前写过一篇图可视化的方案总结,是 Antv 的系列文章。

其实社区里除了 antv 系列,还有 echarts、d3 等不少图可视化引擎,但支持小程序甚至移动端的图可视化引擎几乎还没见过。通常可以通过嵌套 webview 的方式做实现,显然再性能、体验都存在问题,开发方式也不够优雅。

因此图可视化引擎 G6 也做了移动端适配,让关系图在 H5 和小程序中的操作和表现都更顺滑,完全贴合原生小程序开发方式,最终以 G6-mobile 包的形式发布。这次适配的两大 pr 如下:

下面我会梳理一遍 G6-mobile 诞生的脉络,讲述在 G6-mobile 实现中遇到的问题和方案选择。

G6

G6 是个什么样的引擎

g6 是 antv 图可视化体系下的一个关系图可视化引擎, 如果了解复杂网络图论的同学应该能 get 到 6 (六度分离)的含义,而 g 指的是图形语法(The Grammar of Graphic)。

图形语法

图形语法简单总结就是:把数据字段映射到图形属性,再把图形属性和图形正交,就可以组成描述能力丰富的可视化图。

  • 几何图形:点、线、面、多边形等
  • 图形属性:位置、颜色、形状、大小
  • 数据如何映射到属性:就是 g2/g6这类引擎要做的事,提供语法 api。

再讲个题外话,事实上 antv 体系下各类图或图表库都是以图形语法为基础拓展的,包括 g2、g6,这是不同于其他配置形式的图表库 echarts、highcharts的地方。配置形式图表库的好处是比较适合视觉描述(产品描述)思路,上手开发比较容易,但不易拓展。

  • 比如想画一张柱状图,我们会先找出基础柱状图组件,再看这个组件提供了哪些 api 、能配置出哪些视觉效果,但如果 api 不支持就无法配置,数据到组件属性的映射关系在我们选定基础柱状图的时候就已经固定死了。
TypeScript
myChart.setOption({
    xAxis: {
        type: 'category',
        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
    },
    yAxis: {
        type: 'value'
    },
    series: [{
        data: [150, 230, 224, 218, 135, 147, 260],
        type: 'line'
    }]
});

而走图形语法的思路,我们会先思考需要显示什么图形,再考虑数据映射到图形属性的逻辑即可,不用担心图形是否支持这么映射,因此可以更灵活拆解式的开发。

TypeScript
chart
  .line()
  .color('#f3964a')
  .position(`reportDate*${ratioKey}`);

配置形式的开发方式缺失了灵活性,但方便业务使用,@antv/g2-plot 就是封装了 g2 的链式语法,提供配置形式的接口,这两种方式怎么平衡是个值得思考的问题。

G6 的架构

之前提到关系图可视化需要重点考虑布局、节点、和交互,所以 g6 的设计远比图形语法描述的复杂,这次我们主要讲 g6  现在 4.x 的架构,这关系到我们如何产出 g6-mobile,感兴趣还可以参考 g6 的历史架构

g6-4.0-antv-portal

上面是来自官方文档的架构图,不过跟实际代码比起来有些出入和模糊,我们逐个来讲。

从下到上来看:

  • 最底层是基础渲染层,这一层被抽到另一个包 @antv/g 中, 对应我们上文提到的浏览可视化渲染方式 canvas2d、svg、webgl,其中对移动端的支持其实是 f2 在实现过程中 fork 的 g 3.x 的版本改造的,这个 g-mobile 3.x 目前是整合到 f2 中的并没有单独抽包 。

  • 上一层是一个拓展中间层,这一层的目的是为了把 g2 和 g6 的架构统一,提到的名词都是概念,有些 g6 已经实现、有些还没有,有的在 @antv/g6 里实现、有的在 @antv/g 里实现。

  • 约束布局:解决的问题是布局过程中图形之间互相有依赖关系互相影响,约束布局把这些依赖关系在约束计算中内聚,外部布局并不感知,比如可以让多图形在同一方向的时候,均分区域大小,这是衍生自 g2 的方法,g6还没实现。

  • 状态管理:@antv/g6 中实现,这是处理关系图可视化交互的解法之一,图中的节点或边是有状态的,g6 中默认处理交互状态,也支持自定义的业务状态。

  • 事件模型:@antv/g 中实现,配合状态管理处理交互,g6 内置了事件、也支持自定义事件,g6 上的所有基础图形、Item(节点/边)都能通过事件进行操作。注意这里是构建了事件模型、注册和代理事件,我们之前说到 canvas 一大缺点是难以局部控制、事件操作,根本原因是canvas 绘制的是一张图片,用户在图片上点击时时不能获取对应的图形信息,所以还有个重点是定位。

  • 定位:已经在 @antv/g 里实现。canvas 中定位获取图形元素的方法主要有这几种:

    • 缓存一个隐藏的 canvas 通过颜色获取(渲染开销翻倍)
    • canvas 内置 api( isPointInPath 可以判断获取对应的点是否在绘制的图形内部,所以可以遍历每个图形,isPointInPath 方法判断点是否在图形中,缺点就是遍历会导致性能差)
    • 几何运算,canvas 的画布是有坐标的,根据图形的边缘坐标就可以建立一个最小覆盖图形的矩形(g6 里只计算了起点 xy 和 宽高),把它作为图形的包围盒 bbox;理论上只要我们的包围盒划分的足够细,我们就可以拾取定位任意位置元素。这样我们检测所有的图形,判断点是否在图形的包围盒内,如果图形绘制线,判断是否在线上,如果图形被填充,则判断是否被包围。
    • g6 综合了几何计算 + isPointInPath 方法: 简单的图形使用几何算法,复杂的很多填充的图形使用包围盒 + isPointInPath 来检测。
  • 再往上是组件层,这一层是 g6 无关 g2 架构的能力

    • 拓展 shape:这里提到了 shape,这是 @antv/g 给出的『图形』概念,其中做的事就是我们前面提到通过 canvas2d api 绘制基础图形。

      • shape 的基类 element (event-emit 模式)实现了一个绘图元素,实现可见性属性、矩阵变换、移动等绘制中用到的基本方法。
      • shape 继承 element 实现一个基础图形的基类,这一层包装之后对外暴露统一的绘制和定位方法(获取包围盒)
      • 通过拓展 shape 类,g6 可以创建 circle、rect 等不同的基础『图形』方便开发者调用,因此在 @antv/g6 中也实现一个 shape 类(即拓展 shape 类)。这个 shape 类是工厂模式,circle、rect 等具体图形在这个工厂中注册,代理 @antv/g 中 shape 的可见属性、绘制接口实现具体图形的绘制。
    • 分组管理:g6 中 group 的概念,是对基础图形的合并处理,比如同一个 group 的多个绘图元素可以合并包围盒、统一创建销毁、设置状态。group 同样继承自 element 实现。

elementshapegroup 就是 g 架构图中的三层渲染模型。

  • 状态管理:上一层提过。

  • plugin:提供一些拓展分析工具组件,比如网格、tooltip之类的,这块被单独拆成了一个 @antv/g6-plugin 包。

  • animation:配置是否展示动画、以什么函数显示动画。动画的实现在 @antv/g 中,做法就是创建一个 timeline 定时器去执行动画函数,改变 shape 属性、执行 shape 的移动变换绘制。

  • layout: 前面提到布局是关系图可视化的一个核心,这里被抽到 @antv/layout 中,内置多种布局算法。

  • 最上方是用户接口层,提供了 graph、treegraph(由于数据结构和布局自成一派,单独抽出,想具体了解参考g6 文档)、item (g6 中的 item 就是 node 和 edge,这是关系图中最基本的两种元素,他们也是通过拓展 shape 类注册具体图形实现的,g6 内置了多种具体图形作为默认 node 和 edge 供选择)。

g6 体系中还有一部分是数据处理,涉及到不少模块:

  • 数据转换(@antv/data-set 包中专门实现)
  • 并行计算(我们前面提到为了提供布局渲染效率可以利用 gpu 并行计算,@antv/webgpu 包中独立封装了 webgpu 的接口,涉及到许多依赖,后面会讲)
  • 图算法(@antv/algorithim 包中单独实现,调用 dfs 之类的算法处理 node 和 edge)

尽管这架构图看起来还算清晰,但每一块的实现分散在 g6 相关的多个包里,所以最后需要梳理一下 g6、 g 各个包之间的关系:

G6 mobile

G6 mobile 面临的问题

了解完 g6,可以开始实现 g6-mobile 了,先看下面临的问题:

  • 移动端包括 h5 和小程序,h5 中的事件及部分 api 在小程序中不能使用,需要做一定适配来同时支持 h5 和小程序。

  • 小程序封装的 canvas api 和 浏览器上有差异,导致 g-canvas 绘制需要调整。

  • g-base 里包含 documentwindow 的引用,在小程序上不能用,需要兼容。

  • 别忘了之前我们提到 f2 在是实现是 fork g 3.x 内部做了一套 g-mobile。所以我们也就面临多个方案选择:

    • 把 f2 内置的 g-moblie 独立并兼容 g 4.x
    • g 4.0 产出 g-mobileg-mobileg-canvas 的渲染和 api 保持一致,只是针对事件特殊处理。

考虑工作量和未来拓展性,选择了第二种。

G6 mobile 的整体思路

  • 渲染:抹平小程序 canvas api 差异

    • 通过 proxycanvascontext 的调用转成了 小程序 canvas context 的接口。
  • 交互:增加移动端手势事件、交互 behavor 对接移动端事件

    • 事件我们想到了 hammer.js,这个专门处理移动端手势交互(尤其是多指触控)的库,但它同样依赖浏览器环境,所以我们精简了一份,事件由外部触发,hammer 只做事件解析,独立成 g6-hammer 包。
    • 由于 g6-hammer 只做事件解析,我们还需要在 g-mobile 中增加 mini-event 模型移除 `document 和 window` 同时触发事件。
    • 另外在 g6-mobile 中也需要将 behavior 改造适配移动端事件。
    • g6-mobile 中重写 event controller 去除 documentwindow
  • 布局

    • g6-mobile 中重写 layout controller 去除 webworkerwebgpu

根据这些任务我们同样需要拆分到 g6、g 的多包中分别进行,调整之后 g6 的包变成了下面这样:

G6 mobile 拆包优化

到此,我们看似完成了 g6-mobile 的开发,但打包构建出来之后我们发现了新的问题,g6-mobile 初始版本打包之后发现体积很大,parse 之后 1.3 M,gzip 之后 330k,这个尺寸移动端难以接受。

这就是我之前的一篇文章做的事: 👉 G6-Mobile 包体积优化

G6 mobile beta 版

经过上述操作,我们最终完成了 g6-mobile 的开发调整。发布了两个包 https://www.npmjs.com/package/@antv/g6-mobile https://www.npmjs.com/package/@antv/g-mobile

G6-mobile beta 版也跟随 G6 4.2.0 一起面世

如果你也想在小程序或移动端中绘制关系图,欢迎使用 g6-mobile,已经独立成 F6 啦!