数据可视化原理与实践 - G2原理初探

avatar
前端

有求职需求的小伙伴,可以传送带直达...

本文是数据可视化原理与实践系列的第二篇,将介绍G2的基本用法、源码框架和图表绘制基本原理。 要理解G2的使用和实现,就必须对图形语法有基本认识,对图形语法的介绍可见本系列的上一篇《数据可视化原理与实践 - 浅谈图形语法》

1 背景

1.1背景&简介

G2 是由蚂蚁金服 antV 团队开发的一套基于图形语法理论的可视化底层引擎,以数据驱动,提供图形语法与交互语法,具有高度的易用性和扩展性。 null

1.2 为什么要使用G2,与其他图表库相比的优缺点

在图表库的选择中,主要需要平衡“自由度”和“简便性”。本节对比了主流的开源可视化工具G2、ECharts和D3,对比发现,G2的优势在于它的自由度和简便性都在ECharts和D3之间,比ECharts更灵活,比D3学习成本更低,适用于不满足ECharts的封装,但也没有非用D3不可的复杂需求的场景。各图表具体的优劣势如下表所示。

图表库 优点 缺点
G2:由蚂蚁金服 antV 团队开发,有被称为可视化圣经的《The Grammar of Graphics》作理论支撑,将可视化理论与web技术做了非常好的结合。 抽象成本介于D3和ECharts之间:学习成本不会过高,同时比ECharts更灵活;
自由度较高:由可视化理论做基础,没有类似 Echarts 的 chart 概念,可用“可视化语法”绘制想要的图表,非常灵活。 例如堆积柱形图的坐标系由笛卡尔坐标系改为极坐标系,堆积柱形图就变为了圆环图;
代码复用率较高:因其有一套完善的可视化语法,所以团队或公司可根据自己的 UI 需求,用 g2 做底层库,封装自己的 charts 库。
学习成本稍高:比如理解 g2 的绘图原理至少要知道数据可视化的五种“视觉通道”:数据可以映射到 position, size, color, shape, opacity 五个维度等等。
ECharts:由百度团队开发,因其开源免费又容易手上,所以很快流行起来。 正如其名 Echarts,亦如其它的 xxCharts 类库一样,该库中封装了绝大多数常规 chart,用户通过配置 options 参数,就可很容易绘制指定图表。 抽象程度很高,上手容易:将一个更底层的库封装成各种图表类型,如barChart、lineChart,能通过参数配置很简便地实现这些已封装图表的绘制;
三维效果强大,是唯一支持延迟渲染的图表库。
自由度差:参数配置型语法库的本质是调用现成的绘图函数,只能绘制已封装的图表,几乎没提供视图相关的API,想做出未封装的图表类型非常难;
代码复用率低:封装程度已经很高,很难进一步抽象和封装。
D3:几乎凭 Mike Bostock 一人之力完成,在学术界、专业团队中享有极大声誉。
该库更接近底层,与 g2、echarts 不同,d3 能直接操作 svg,所以拥有极大的自由度,几乎可以实现任何 2d 的设计需求。
正如其名 Data Driven Documents,其本质是将数据与 DOM 绑定,并将数据映射至 DOM 属性上(这时视觉通道、比例尺转换等理论就可发挥作用)。
同时,如下图,d3 长于可视化,而不止于可视化,还提供了数据处理、数据分析、DOM 操作等诸多功能。
功能强大,能满足任何二维可视化需求 过于底层,学习成本过高,对用户的 web 技术、可视化理论、数学逻辑都一定要求。

此外,字节最近开源了自研的可视化解决方案VisActor,其产品结构大致如下图,和AntV的产品结构非常类似。其中对标 G2 的是 VGrammar,是基于图形语法的语法引擎。 null

下面从API和图形语法方面简单对比一下VGrammar和G2:

VGrammar G2
API 同时提供spec形式和函数式的API 主要是函数式API,5.0才提供实验性的spec API
图形语法 - 一切皆图元:G2中几何图形Geometry、图形组件Axis等都变成图元
- 图表是由不同语法元素组成的,包括信号量、数据、映射、坐标系和图元
- 允许语法元素相互引用
- 提供内置的数据处理能力
- 提供实现特殊图表(旭日图、桑基图、词云等)的扩展包
- ...
目前看起来允许语法元素相互引用似乎会比G2更加灵活,但是VGrammar还非常新,它的优劣可能只有在实践中才能深入了解。
基于The Grammar of Graphics。 所构建出的图表是由一系列独立的图形语法元素组合而成的,包括数据、图形属性、几何标记、度量、坐标系、可视化组件、分面等。 即:一张图表就是从数据到几何标记对象的图形属性的一个映射,此外图形中还可能包含数据的统计变换,最后绘制在某个特定的坐标系中。

2 基本使用

下面是ECharts和G2绘制饼图的代码,其中红框框出的部分决定了这个图是个饼图,而不是其他图形(折线图、柱图、点图……)。可以看出,G2要比ECharts难理解得多:

  • 为啥饼图要设置坐标系?

  • interval是啥?

  • adjust('stack')是堆叠吗?为啥饼图要设置堆叠?

  • position是啥配置?

  • 为啥color设置要传入字段名item?

  • ...

    要理解G2的使用,就必须对图形语法有基本认识。对图形语法的介绍可见本系列的上一篇《数据可视化原理与实践 - 浅谈图形语法》file file

3 G2 4.0 源码整体框架和绘制流程

3.1 G2 4.0源码整体框架解读

3.1.1 目录结构

G2项目的入口文件是index.ts,使用各种注册函数注册了主题、渲染引擎、内置几何图形、内置label、内置布局函数、动画执行函数、内置Facet、内置Component、交互动作、交互行为。

file

G2的src文件夹下分了9个文件夹,各文件夹的基本说明如下表。

文件夹 说明
chart 容器(Chart、View)和图形组件(Axis、Legend、Tooltip...)
geometry 几何图形(Interval、Line、Area...)、几何图形中的图形元素Element(用于绘制、更新、管理和销毁Shape)、各shape的定义(包括绘制函数、计算关键点函数等)、几何图形标签管理类(通过label()设置)
component 几何图形标签的渲染器Labels
interaction 交互类Interaction、支持语法的交互类GrammarInteraction、交互的上下文类Context、各交互Action类
animate 执行各种动画的工具函数,涉及对底层渲染引擎g-base的引用
facet 各分面类
engine 渲染引擎的注册和获取类
theme 样式表和主题创建的工具函数
util 其他工具函数

3.1.2 G2核心类的关系和作用

G2的核心类包括Chart、View、Geometry、Controller、Interaction、Action、Facet。类图如下。下面的类图只给出了G2内部有关联的类之间的关系,略去了和别的类无关的类,以及来自第三方的类。如几何图形的图形属性(position、color、size、shape等)都来自第三方库@antv/attr;坐标系Coordinate类来自@antv/coord;度量Scale来自@antv/scale。

file

其中最关键的是继承自Base的这几个类:View、Geometry、Chart。Chart是整个绘图流程的入口类,用于提供创建 canvas、自适应图表大小等能力,是便于开发者使用的类。Chart的提供的能力主要是通过它继承的类View实现的。View 是用来组装数据,Component,Geometry 的容器,一个 View 可以包含有多个子 View,以将一个画布按照不同的布局划分多个不同区域(分面),或叠加多个不同数据源的View来实现多数据源、多图层的图表。每一个 View 拥有自己独立的数据源、坐标系、几何标记、Tooltip 以及图例。Geometry是几何标记基类,主要负责数据到图形属性的映射以及绘制逻辑。

file

Controller是图形组件基类,继承自Controller的各子类用于维护对应图形组件的初始化、布局、渲染、更新、清除和销毁等操作。

file

在调用view的interaction函数设置交互时,会创建GrammarInteraction实例,用于控制交互上下文、Action、交互是否允许执行、交互事件监听和具体执行。

3.2 一个饼图的绘制流程

本节将以绘制一个饼图为例,介绍G2的图表绘制流程和图形语法在G2中的实现。

file

下图给出了简化版的G2渲染流程,即核心类 View(视图)的数据处理、映射、渲染的流程。View 包含有几个子信息:

  • 包含子 View

  • 包含几何图形 geometry(折、柱、点等等)

  • 包含图形组件 component(图例、坐标轴、缩略轴等) 所以不难得知,本质是使用 view 作为容器组织的树形结构,其渲染也是直接是一个大的递归逻辑。分成几个阶段:

  • 实例化

  • 初始化

  • 渲染

  • 销毁

    接下来几个小节将分别介绍实例化、初始化和渲染过程。

null

3.2.1 实例化

import { Chart } from '@antv/g2';
const chart = new Chart({
  container: "container",
  autoFit: true,
  height: 500,
});

Chart 是 G2 暴露的最重要的入口 API,用来创建一个图表实例。他继承自 View 容器,所以他特有的逻辑主要有以下3点:

  1. 创建 G.Canvas 画布

  2. 绑定事件,处理 autoFit 参数(图表自动适配 DOM 容器的大小)

  3. 生成三层 G.Group 然后走 View 的构造函数逻辑

    初始化Chart实例的类间交互如图所示,基本是在使用传入的配置项实例化需要的类。 file

3.2.2 初始化

实例化Chart类之后,到调用Chart对象的render方法之间,都是在设置配置项并存储起来。可设置的配置项包括但不限于:

  • 数据
  • 坐标系
  • scale 数据列定义
  • 组件配置
  • 图形配置
  • 图形映射配置
  • 分面
  • ...

从代码中已经可以将G2的API和图形语法大致对应。图形语法强调必须遵守流程图的顺序(变量 -> 代数 -> 度量 -> 统计 -> 图形 -> 坐标系 -> 美学属性),但是G2的API调用完全不要求顺序,只要在render前调用即可。因为G2的API调用只是对内部对象的属性进行设置,真正的计算流程是在render函数中完成的。即使在render流程中,也只是进行数据处理、计算出绘制所需的数据,这些数据会被用于实例化和配置底层渲染引擎G中的对象,真正的绘制是这些底层对象使用算出的数据调用更底层的库(d3)完成的。

const data = [
  { item: "事例一", count: 40, percent: 0.4 },
  { item: "事例二", count: 21, percent: 0.21 },
  { item: "事例三", count: 17, percent: 0.17 },
  { item: "事例四", count: 13, percent: 0.13 },
  { item: "事例五", count: 9, percent: 0.09 },
];

// DATA:设置数据
chart.data(data);

// ELEMENT:设置几何图形,并将数据映射到视觉通道position、color上
chart
  .interval()
  // 将字段percent映射到视觉通道position上
  .position("percent")
  // 将字段item映射到视觉通道color上
  .color("item")
	// GUIDE:设置标记
  .label("percent", {
    layout: [
      {
        type: "limit-in-plot",
        cfg: { action: "ellipsis" /** 或 translate */ },
      },
    ],
    content: (data) => {
      return `${data.item}: ${data.percent * 100}%`;
    },
  })
  // ELEMENT:设置堆叠的数据调整
  .adjust("stack");

// COORD:设置坐标系
chart.coordinate("theta", {
  radius: 0.75,
});

// GUIDE:设置图形组件tooltip
chart.tooltip({
  showTitle: false,
  showMarkers: false,
});

// GUIDE:设置数据中字段在坐标轴的显示格式
chart.scale("percent", {
  formatter: (val) => {
    val = val * 100 + "%";
    return val;
  },
});

// 设置交互
chart.interaction("element-active");

3.2.2.1 创建变量+应用代数+计算统计值

代码中给出的数据data已经是图形语法中计算统计值之后的结果了,因此在实现中跳过这些数据处理步骤(G2也是支持这些数据处理逻辑的,详见DataSet),直接调用chart.data()存储已处理好的图表数据。

const data = [
  { item: "事例一", count: 40, percent: 0.4 },
  { item: "事例二", count: 21, percent: 0.21 },
  { item: "事例三", count: 17, percent: 0.17 },
  { item: "事例四", count: 13, percent: 0.13 },
  { item: "事例五", count: 9, percent: 0.09 },
];

// DATA:设置数据
chart.data(data);

3.2.2.2 构造几何图形+计算美学属性

调用chart.interval(),设置几何图形为区间图。继续链式调用position('percent')将数据中的percent字段映射到图形属性position的x轴上,链式调用color('item')将数据中的item字段映射到图形属性color上。这样设置后,区间图的位置属性将由percent的值决定,颜色将由item的值决定。

// ELEMENT:设置几何图形,并将数据映射到视觉通道position、color上
chart
  .interval()
  // 将字段percent映射到视觉通道position上
  .position("percent") // 1*percent
  // 将字段item映射到视觉通道color上
  .color("item")
	// GUIDE:设置标记
  .label("percent", {
    layout: [
      {
        type: "limit-in-plot",
        cfg: { action: "ellipsis" /** 或 translate */ },
      },
    ],
    content: (data) => {
      return `${data.item}: ${data.percent * 100}%`;
    },
  })
  // COORD:设置堆叠的数据调整
  .adjust("stack");

设置几何图形和视觉通道映射的时序图如下图所示。这一过程也是一些实例化和属性设置。 file

3.2.2.3 应用坐标

调用chart.coordinate('theta')设置坐标系为theta。theta是一种特殊的极坐标系,半径长度固定,仅仅将数据映射到角度。仅仅设置坐标系为theta的话,饼图的每一份会重叠,值最大的占整个饼,其他值占的角度根据占最大值的比例决定。如下图所示,事例一的percent值最大(0.4),所以蓝色会占整个饼,事例二的percent为0.21,占0.4的52.5%,所以绿色扇形的角度为189度,且会叠在蓝色上,遮住一部分蓝色饼,其他事例以此类推。

null

为了实现饼图的效果,还需要对图形应用堆叠效果。G2的堆叠效果是调用几何图形的数据调整函数adjust实现的。

chart
  .interval()
  // COORD:设置堆叠的数据调整
  .adjust("stack");

// COORD:设置坐标系
chart.coordinate("theta", {
  radius: 0.75,
});

设置坐标系的时序图如下图所示,也是实例化+设置属性。

file

3.2.2.4 设置图形组件+设置交互

这一系列设置没有在图形语法中详细定义,在G2的实现也较为分散,本次不详细介绍。

3.2.3 渲染

// 渲染
chart.render();

在设置了各项配置之后,最后在 render 的时候,会消费这些配置,然后去生成衍生数据、数据映射、绘制图形组件、G 渲染。图形语法仅仅是将数据处理成图表,并不关注渲染的细节,渲染是通过将图表输入到渲染器实现的。G2也同理,将数据处理成图表后,会调用底层渲染引擎G进行绘制。 View 包含有子 View,然后无限嵌套的树形结构,所以渲染逻辑是一个递归的渲染过程,这其中的逻辑主要分成为几个阶段:

  1. 数据处理阶段
  2. 计算auto padding
  3. 布局
  4. 渲染绘制

本示例中绘制饼图的输入数据如下表,后续将以这个数据为例展示对数据的处理。

file

3.2.3.1 数据处理(应用度量+构造几何图形-堆叠)

数据处理,主要是将初始化过程中的配置项,进行处理,产生一些衍生数据,用于做渲染。主要包含:

  • 过滤数据(如过滤出选中legend的数据)

file

  • 创建Coordinate实例:不同的坐标系会将一个相对坐标映射到不同的像素点上,从而实现图形的变化。比如从图形语法中我们可以了解到,柱图和饼图其实就是一个图形经过坐标系变化而来的。后续所有的数据转换,渲染,都会用到 coordinate,所以需要先创建好实例。
// 0 ~ 1 的相对坐标点位置
const point = { x: 0.1, y: 0.8 };
// 实际像素坐标
const pointPixel = coordinate.convent(point);

file

  • 初始化Geometry Geometry是G2中非常重要的类,承载几乎所有数据处理过程,是图形语法理论的实现方。初始化主要包括:
  1. 根据字段配置,生成 scale 信息

  2. 数据加工

    a. 分组:根据分组字段(color、shape、size)将数据分组,每个分组的数据组成一簇。一组可能对应多条数据,但是本示例中的每条数据item都不一样,所以一组只有一条数据。分组数据会被转换为二维数组,第一维是组,第二维是组里的数据。分组后会保存原始数据,方便后期 tooltip 取对应的数据进行展示。分组+保存原始数据后的数据结构如图所示。 file b. 数字化:使用 scale 将位置相关的分类数据数字化,便于后续经过 coordinate 获取数据的实际画布坐标。数字化逻辑很简单,就是使用 index 作为分类信息的数据。比如柱状图中分类字段 item (值为 ['事例一', '事例二', '事例三', '事例四', '事例五'])被映射到 x 轴,与位置相关,因此在这一步,item 的值就会被数字化为 [0, 1, 2, 3, 4](将每个值替换为其索引)。但是饼图中分类字段 item 被没有被映射到位置 position 上,就不会被数字化。

    c. 调整:调整堆积图的数据。堆叠处理后的数据如下。映射到位置的 percent 会被转换为堆叠的区间。 file d. scale 调整:对于堆积图,调整 scale 的 min max;对于线图、柱图修改 range 范围。如本示例中,y 轴原数据(0.4, 0.21, 0.17, 0.13, 0.09)的最小值为 0.09,最大值是 0.4;堆叠之后 y 轴数据的最小值为 0,最大值为 1。

  3. facet 分面处理:这是高级功能。根据 facet 配置 + 数据,生成子 view,以及他们的位置。本示例不涉及分面,这里就不具体说明。

3.2.3.2 计算 auto padding

padding 的含义是:图形区域上右下左的间距,这个间距是保留给图形组件使用的。

file

在 G2 中,padding 可以是一个用户指定的固定值,也可以是自适应(auto)的。对于 auto,auto padding 的逻辑就是将 auto 转换成确定的数值。具体来说,就是根据 axis、legend、slider 等组件的方位,以及他们的 width height,最终计算出实际的 padding 值。算出 padding 之后,就可以使用 画布的宽高 - padding 更新坐标系的宽高了。

// auto padding --------->  [16, 16, 16, 16]

3.2.3.3 布局

算出 padding,更新了坐标系的宽高后,需要根据组件(axis、legend、slider)的 position(上右下左),来计算组件具体的 x y。这一步后,会得到组件的 x y width height,就可以进行绘制了。

3.2.3.4 渲染绘制

渲染分为图形的渲染和组件的渲染。

  • Geometry 渲染 主要渲染逻辑在 shapeFactory 中。不同的图形有不同的 shapeFactory,每个 shapeFactory 都有自己的渲染方法(转化成 G 的 shape),是在index.ts中引入内置shape的时候注册进来的。

file

Geometry的渲染主要分成为5步:

  1. 分组数据变成单向链表,这样数据之间就能互相索引了。在数据结构中,每条数据以 nextPoints 记录下一条数据的关键点,来实现链接。
  2. 计算关键点,由 shapeFactory 的 getPoints 实现。链接且计算关键点后的数据如下。

file

file

  1. 映射到图形空间:将图形属性转换为映射后的数据。如 x y 就映射为 0 ~ 1 的位置,color 就映射为具体的颜色。

file

  1. 调用Coordinate.convert将归一化坐标转换为画布坐标:

file

  1. 每个分组数据,创建一个 Element 实例,这个实例会管理自己的数据 model,然后会根据 shape 获取对应的 shapeFactory 去绘制。绘图使用的是绘图数据,就是在上一步数据上加上填充色、透明度、形状、大小等样式信息。
  • Component 渲染
    • Axis:根据字段对应 scale + 设置的 axisCfg,去生成 G 的结构;
    • Legend:根据 legendCfg 去绘制 marker + text
    • Tooltip:使用 HTML 绘制
    • ...

4 注册机制(值得借鉴的实现)

G2中存在大量注册函数,来实现独立、可插拔的逻辑。如G2中的几何图形设置函数(如chart.interval()),仅在G2初始化时才会动态注册到容器View上。比起将几何图形设置写死在View上,这种结构的好处是将几何图形的管理与容器逻辑解耦,增加或删除某个几何图形都不必修改View代码。具体实现的关键代码如下:

// 1. G2内部:调用registerGeometry注册内置的几何图形,将对应设置方法挂在View上
// index.ts
import { registerGeometry } from './core';
registerGeometry('Polygon', Polygon);
registerGeometry('Interval', Interval);
registerGeometry('Schema', Schema);
registerGeometry('Path', Path);
registerGeometry('Point', Point);
registerGeometry('Line', Line);
registerGeometry('Area', Area);
registerGeometry('Edge', Edge);
registerGeometry('Heatmap', Heatmap);

// view.ts
/**
 * 注册 geometry 组件
 * @param name
 * @param Ctor
 * @returns Geometry
 */
export function registerGeometry(name: string, Ctor: any) {
  // 语法糖,在 view API 上增加原型方法
  View.prototype[name.toLowerCase()] = function (cfg: any = {}) {
    const props = {
      /** 图形容器 */
      container: this.middleGroup.addGroup(),
      labelsContainer: this.foregroundGroup.addGroup(),
      ...cfg,
    };

    const geometry = new Ctor(props);
    this.geometries.push(geometry);

    return geometry;
  };
}

// 2. G2使用者:调用几何图形设置方法时,调用上述过程中挂在View上的对应方法,实例化对应的几何图形
// App.vue
const chart = new Chart({
  container: "container",
  autoFit: true,
  height: 500,
});
chart.interval()

团队招聘

    我们是快手数平前端团队,主要负责快手大数据平台中从采集,加工,消费整条链路相关产品的前端建设工作,涉及到生产、分析、流量、AB、专题等多个领域,目标是建设更自助,更快速,更先进的数据产品。

来到这里你能接触到:

  1. 快手数据平台是如何通过先进技术支持起 EB 级别数据量的;
  2. 日均百亿级别的日志上报的埋点基础设施的是如何设计与迭代的;
  3. 超过百万代码量的独立项目带来的工程化挑战;
  4. 2D/3D可视化、在线编辑器、电子表格等细分领域的探索;
  5. 自研工具产品来提升团队效率,用数据来说话;
  6. ......

我们希望你能:

  1. 有坚实的计算机/前端基础,不只是 MVVM 工程师;
  2. 有一定的 B 端项目/可视化相关的经验更佳,或非常热爱;
  3. 能发现问题,分析问题,并解决问题;
  4. 敢于创新,善于沟通,时刻关注前沿技术;
  5. 【加分项】有一定产品/交互/设计 sense;
  6. 【加分项】开源项目/社区;

团队氛围:

  1. 来这里你能接触到大数据中各个领域的专家,大家都非常 nice,没有架子,随时坦诚沟通,互相学习;
  2. 技术很重要,业务更重要,定期的培训分享都少不了;
  3. 团队氛围融洽,就事论事,风清气正,拒绝勾心斗角;
  4. 团队年轻有活力,既要工作好,也要玩的好; 加入我们,一起做点好玩的!

感兴趣的同学,欢迎投寄简历: luominxianglmx@outlook.com

参考