Transform 是数据预处理,对 data 和 encoding 进行转换,可以是异步的,也可以是同步的。在数据预处理之后,就可以提取每个 channel 需要的属性,从而进行数据映射。
在 G2 5.0 里面的 transform 按照使用场景主要分为以下几类:
- Connector:获得数据,比如通过 url 获得数据。这类 transform 主要是获得 data 的值。
 - Layout:布局算法,比如 treemap 等。这类 transform 主要生成新的 data,并且更新 encoding。
 - Inference:推断 encoding 和 statistic,比如条形图的补全 y 方向的 0 和推断 stack 等。
 - Statistic:统计变换,对数据进行过滤、聚合等操作,主要用于改变图形的位置通道。
接口设计
```typescript type Primitive = number | string | Date | boolean; 
type TransformContext = {
  // 原始数据,可以不传
  data?: any;
  // 编码信息
  encode: Record
type TransformProps = { // 用于区分 Transform 的种类 type: ‘connector’ | ‘inference’ | ‘layout’ | ‘statsitic’; // 是否是异步的,主要是为了判断给他们添加缓存的种类 async: boolean; // 是否需要缓存,默认都是 true memo: boolean; };
type TransformComponent
type Transform = (context: TransformContext) => TransformContext | Promise
在之前的设计中 Layout & Connector 被称为 Transform,Transform、Inference和 Statistic 这三部分是分开的,合并的理由或者说优点如下:- 用户理解成本降低,不需要去区别 Transform 和 Statistic 的区别是什么,两者的本质都是数据预处理。- Layout Transform 可以直接修改 encoding 的信息,去掉了生成新字段和指定为 encoding 操作,减少了用户的使用成本。- Statistic 可以感知到原始数据 data,这对涉及到聚合的变换非常有用,因为这样的 reducer 可以拿到完整的数据,而不只是 encoding 里面声明的 column。- 简化了渲染流程的理解和实现,之前为:Transform -> Inference -> Encoding -> Statisitc -> ...,现在为:Transform -> Encoding -> ...- 更强的灵活性,同时之前能做到的事情,现在也可以做到,并且因为获得了更多的信息(参数),拥有更强的能力。同时不会默认给 transform 函数添加缓存了,而是在 runtime 运行前给所有 library 中的 transform 中添加缓存即可。这样用户不需要关注缓存相关的功能。```javascript// Beforefunction transform() {}export Sort = useMemo(transform);// Afterexport Sort() {}function memoTransform(library) {return mapObject(library, (value, key) => {if(key.startsWith('transform') && value.memeo) {return value.async ? useAsyncMemo(value): useMemo(value);}});}
下面是接下来案例中的一些工具函数。
// 赋给 source 默认值function applyDefaults(source, defaults) {const target = {...source};for (const [key, value] of Object.entries(defaults)) {target[key] = target[key] ?? value;}return target;};// 返回一个函数,该函数将合并它的参数和返回值function merge(transform) {return (options) => {const newOptions = transform(options);return {...options, ...newOptions};}}// 返回一列数据function column(value) {return {type: 'column', value};}
Connector
- Fetch ```typescript // Fetch.ts import { TransformComponent as TC } from ‘runtime’; import { useAsyncMemoTransform } from ‘utils’;
 
export type FetchOptions = { url?: string; };
const transform: TC
export const Fetch = useAsyncMemoTransform(transform);
Fetch.props = { type: ‘connector’, }
<a name="lM1NS"></a>## Layout- Treemap- Force Graph- Sankey- Voronoi- Tree```javascriptconst options = {type: 'polygon',transform: [{ type: 'fetch', url: 'xxx' },{ type: 'treemap' },],encode: { color: 'name' },};
import { TransformComponent as TC } from 'runtime';import { useMemoTransform } from 'utils';export type TreemapOptions = {};export const Treemap: TC<TreemapOptions> = () => {return merge((context) => {const { data: treeData, encode } = context;// 生成新的数据数据索引const data = d3.treemap()(treeData);const I = range(data);// 生成新的通道const count = data.length;const X1 = new Array(count);const X2 = new Array(count);const X3 = new Array(count);const X4 = new Array(count);const Y1 = new Array(count);const Y2 = new Array(count);const Y3 = new Array(count);const Y4 = new Array(count);for (let i = 0; i < data.length; i++) {const { x, y, width, height } = data[i];X1[i] = x;X2[i] = x + width;X3[i] = x + width;X4[i] = x;Y1[i] = y;Y2[i] = y + height;Y3[i] = y + height;Y4[i] = y;}// 更新索引,数据和编码return {I,data: rects,encode: {...encode,x1: column(X1),x2: column(X2),x3: column(X3),x4: column(X4),y1: column(Y1),y2: column(Y2),y3: column(Y3),y4: column(Y4),},};});}Treemap.props = {type: 'layout';}
Inference
- MaybeType:推断 encoding 的种类。
 - MaybeArray:将 array encoding 拆封成多个通道,这些通道公用一个比例尺。
 - MaybeZeroX1:推断 x1 = 0
 - MaybeZeroY1:推断 y1 = 0
 - MaybeZeroY2:推断 y2 = 0
 - MaybeSeries:将 color 字段推断为 series 字段
 - MaybeTooltip:推断 tooltip 的值,目前是使用 y1 和 position(这个地方需要优化)
 - MaybeTitle:推断 title 的值,目前使用的是 x 的值(这个也需要优化)
 - MaybeKey:生成默认的 key
 - MaybeStackY:推断 stackY ```typescript import { TransformComponent as TC } from ‘runtime’; import { useMemoTransform } from ‘utils’;
 
// Inference function zero() { return { type: ‘constant’, value: 0 }; }
export type MaybeZeroX1Options = {};
export const MaybeZeroX1: TC
MaybeZeroX1.props = { type: ‘inference’, }
<a name="AxoRe"></a>## Statistic统计变换是最复杂一种变换,它涉及到数据的聚合,过滤等。同时它也是 G2 5.0 中最重要的一种变换,因为它直接决定了 G2 5.0 的统计分析能力。<br /><a name="h8Bdo"></a>### 相关工作一般的的图表库都是在 encoding 之前去预处理数据,生成新的字段,然后将生成的字段参与 encoding。但是 Vega Lite 和 Plot 的声明方式却与众不同,是很接近图形语法的声明方式。**Vega Lite**- 特点:Encoding 和 Statistic 其实是在一起声明的。- 优点:简洁。- 缺点:Encoding 的配置较为复杂。```javascript// Vega Liteconst vegaLite = {data: { value: athletes },mark: 'bar',encoding: {x: { field: 'sex', group: true },y: { aggregate: 'mean', field: 'weight' },},};
通过编译之后等 Vega 配置可以看出 Vega Lite 实现统计变换的方法是通过 transform 生成新的字段,然后用新的字段参与 encoding 的声明。这样的实现的优缺点如下:
- 优点:实现简单,因为把 transform 和 encoding 的逻辑解耦开来,所以 transform 模块只需要关心数据本身的变换。
 - 缺点:
- 字段冲突:生成的字段可能和已有的字段冲突(可能性很小)。
 - 需要额外的机制把 statistic 映射为 transform 和 encoding。
const vega = {data: {value: athletes,transform: [{type: 'aggregate',groupby: ['sex'],fields: ['weight'],ops: ['mean'],// 生成中间字段as: ['__y']}],},marks: [{type: 'bar',encoding: {x: { field: 'sex' },// 使用中间字段,这个字段是不会被用户感知的y: { field: '__y' }}}],};
 
 
Plot
- 特点:嵌套的 statistic,encoding 作为初始值,会经过一系列的 statistics 转换成最终的需要的值。
 - 优点:encoding 和 statistic 分开声明,理解更容易一点。
 - 缺点:多个 statistic 的时候会嵌套,可读性和易修改性会变弱。 ```javascript // Plot Plot.barY( athletes, Plot.groupX({ y: ‘mean’ }, { x: ‘sex’, y: ‘weight’, fill: ‘sex’ }), ).plot();
 
// 嵌套的 Statistic // normalizeY + groupX Plot.barY( athletes, Plot.normalizeY( Plot.groupX({ y: ‘mean’ }, { x: ‘sex’, y: ‘weight’, fill: ‘sex’ }), ), ).plot();
Plot 的实现 statistic 的思路和 Vega-Lite 是不一样的,它不存在一个中间的过渡字段,而是直接对 encoding 进行修改。这样实现的优缺点如下:- 优点:不存在字段冲突。- 缺点:实现起来比较复杂,这里的复杂度一方面主要来自于一下几个方面:- 同时对 data 和 encoding 进行转换。- Plot API 设计让它在转换的时候不能获取到 data,需要通过 lazy channel 的方式延迟获得 data 的值。- 不同于常规的数据处理,这里是直接操作一列数据,而不是一行数据。```javascript// Stack 的例子function stack(x, y = one, ky, {offset, order, reverse}, options) {const z = maybeZ(options);const [X, setX] = maybeColumn(x);const [Y1, setY1] = column(y);const [Y2, setY2] = column(y);offset = maybeOffset(offset);order = maybeOrder(order, offset, ky);return [basic(options, (data, facets) => {const X = x == null ? undefined : setX(valueof(data, x));const Y = valueof(data, y, Float64Array);const Z = valueof(data, z);const O = order && order(data, X, Y, Z);const n = data.length;const Y1 = setY1(new Float64Array(n));const Y2 = setY2(new Float64Array(n));const facetstacks = [];for (const facet of facets) {const stacks = X ? Array.from(group(facet, i => X[i]).values()) : [facet];if (O) applyOrder(stacks, O);for (const stack of stacks) {let yn = 0, yp = 0;if (reverse) stack.reverse();for (const i of stack) {const y = Y[i];if (y < 0) yn = Y2[i] = (Y1[i] = yn) + y;else if (y > 0) yp = Y2[i] = (Y1[i] = yp) + y;else Y2[i] = Y1[i] = yp; // NaN or zero}}facetstacks.push(stacks);}if (offset) offset(facetstacks, Y1, Y2, Z);return {data, facets};}),X,Y1,Y2];}
设计
特点:encode 和 statsitic 分开声明,同时由数组代替了嵌套的形式。
// G2 5.0const g2 = {data: athletes,type: 'interval',transform: [{ type: 'groupX', y: 'sum' }],encode: { x: 'sex', y: 'weight', fill: 'sex' },};
function StackY() {return merge((context) => {const { data, I, columnOf, encode } = context;const { x, y } = encode;// 从原始数据中提取出下面两列数据const X = columnOf(data, x);const Y = columnOf(data, y);// 按照 X 通道分组const groups = Array.from(group(I, (i) => X[i]));// 堆叠每一组内的 mark 的 y 通道const newY = new Array(I.length);const newY1 = new Array(I.length);for (const G of groups) {for (let py = 0, i = 0; i < G.length; i += 1) {const index = I[i];newY1[index] = py;newY[index] = py + Y[index];py = newY[index];}}return {// 更新 encodeencode: {...encode,x: column(X),y: column(Y),y1: column(Y1)},};});}
种类
dodgeX
- stackY
 - groupX
 - binX
 - binY
 - bin
 - selectFirst
 - selectLast
 - selectMax
 - selectMin
 - summary
案例验证
矩阵树图
```javascript
// https://g2.antv.vision/zh/examples/relation/relation#treemap 
// 默认隐藏 axisX 和 axisY // 默认将 x 和 y scale 设为 identity const config = { type: ‘polygon’, data, transform: [{type:’treemap’}], encode: { color: ‘name’, label: ‘name’ } };
<a name="ZjgfU"></a>### Voronoi 图```javascript// https://g2.antv.vision/zh/examples/relation/relation#voronoiconst config = {type: 'polygon',data,transform: [{type: 'voronoi'}],encode: {color: 'value',label: 'value'},};
桑基图

const config = {type: 'view',data,children: [{type: 'polygon',transform: [{type: 'sankey.nodes'}],encode: {color: 'name',},},{type: 'polygon',transform: [{type: 'sankey.links'}],encode: {shape: 'arc',color: 'name'}}],}
力导向图

const config = {type: 'view',data,children: [{type: 'point',transform: [{type: 'force.nodes'}],encode: {color: 'name',},},{type: 'edge',transform: [{type: 'force.links'}],}],}
