Transform 是数据预处理,对 dataencoding 进行转换,可以是异步的,也可以是同步的。在数据预处理之后,就可以提取每个 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; // 原始数据的索引数组 I: number[]; // 根据指定的 encode 从当前数据提取出一列数据 columnOf: (data: any, encode: Encode) => Primitive[]; };

type TransformProps = { // 用于区分 Transform 的种类 type: ‘connector’ | ‘inference’ | ‘layout’ | ‘statsitic’; // 是否是异步的,主要是为了判断给他们添加缓存的种类 async: boolean; // 是否需要缓存,默认都是 true memo: boolean; };

type TransformComponent = { (options?: O): Transform; props: TransformProps; };

type Transform = (context: TransformContext) => TransformContext | Promise;

  1. 在之前的设计中 Layout & Connector 被称为 TransformTransformInference Statistic 这三部分是分开的,合并的理由或者说优点如下:
  2. - 用户理解成本降低,不需要去区别 Transform Statistic 的区别是什么,两者的本质都是数据预处理。
  3. - Layout Transform 可以直接修改 encoding 的信息,去掉了生成新字段和指定为 encoding 操作,减少了用户的使用成本。
  4. - Statistic 可以感知到原始数据 data,这对涉及到聚合的变换非常有用,因为这样的 reducer 可以拿到完整的数据,而不只是 encoding 里面声明的 column
  5. - 简化了渲染流程的理解和实现,之前为:Transform -> Inference -> Encoding -> Statisitc -> ...,现在为:Transform -> Encoding -> ...
  6. - 更强的灵活性,同时之前能做到的事情,现在也可以做到,并且因为获得了更多的信息(参数),拥有更强的能力。
  7. 同时不会默认给 transform 函数添加缓存了,而是在 runtime 运行前给所有 library 中的 transform 中添加缓存即可。这样用户不需要关注缓存相关的功能。
  8. ```javascript
  9. // Before
  10. function transform() {}
  11. export Sort = useMemo(transform);
  12. // After
  13. export Sort() {}
  14. function memoTransform(library) {
  15. return mapObject(library, (value, key) => {
  16. if(key.startsWith('transform') && value.memeo) {
  17. return value.async ? useAsyncMemo(value): useMemo(value);
  18. }
  19. });
  20. }

下面是接下来案例中的一些工具函数。

  1. // 赋给 source 默认值
  2. function applyDefaults(source, defaults) {
  3. const target = {...source};
  4. for (const [key, value] of Object.entries(defaults)) {
  5. target[key] = target[key] ?? value;
  6. }
  7. return target;
  8. };
  9. // 返回一个函数,该函数将合并它的参数和返回值
  10. function merge(transform) {
  11. return (options) => {
  12. const newOptions = transform(options);
  13. return {...options, ...newOptions};
  14. }
  15. }
  16. // 返回一列数据
  17. function column(value) {
  18. return {type: 'column', value};
  19. }

Connector

  • Fetch ```typescript // Fetch.ts import { TransformComponent as TC } from ‘runtime’; import { useAsyncMemoTransform } from ‘utils’;

export type FetchOptions = { url?: string; };

const transform: TC = (options) => { const { url } = options; return merge(async () => { const response = await fetch(url); const data = await response.json(); return { data }; }); }

export const Fetch = useAsyncMemoTransform(transform);

Fetch.props = { type: ‘connector’, }

  1. <a name="lM1NS"></a>
  2. ## Layout
  3. - Treemap
  4. - Force Graph
  5. - Sankey
  6. - Voronoi
  7. - Tree
  8. ```javascript
  9. const options = {
  10. type: 'polygon',
  11. transform: [
  12. { type: 'fetch', url: 'xxx' },
  13. { type: 'treemap' },
  14. ],
  15. encode: { color: 'name' },
  16. };
  1. import { TransformComponent as TC } from 'runtime';
  2. import { useMemoTransform } from 'utils';
  3. export type TreemapOptions = {};
  4. export const Treemap: TC<TreemapOptions> = () => {
  5. return merge((context) => {
  6. const { data: treeData, encode } = context;
  7. // 生成新的数据数据索引
  8. const data = d3.treemap()(treeData);
  9. const I = range(data);
  10. // 生成新的通道
  11. const count = data.length;
  12. const X1 = new Array(count);
  13. const X2 = new Array(count);
  14. const X3 = new Array(count);
  15. const X4 = new Array(count);
  16. const Y1 = new Array(count);
  17. const Y2 = new Array(count);
  18. const Y3 = new Array(count);
  19. const Y4 = new Array(count);
  20. for (let i = 0; i < data.length; i++) {
  21. const { x, y, width, height } = data[i];
  22. X1[i] = x;
  23. X2[i] = x + width;
  24. X3[i] = x + width;
  25. X4[i] = x;
  26. Y1[i] = y;
  27. Y2[i] = y + height;
  28. Y3[i] = y + height;
  29. Y4[i] = y;
  30. }
  31. // 更新索引,数据和编码
  32. return {
  33. I,
  34. data: rects,
  35. encode: {
  36. ...encode,
  37. x1: column(X1),
  38. x2: column(X2),
  39. x3: column(X3),
  40. x4: column(X4),
  41. y1: column(Y1),
  42. y2: column(Y2),
  43. y3: column(Y3),
  44. y4: column(Y4),
  45. },
  46. };
  47. });
  48. }
  49. Treemap.props = {
  50. type: 'layout';
  51. }

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 = () => { return merge(({ encode }) => ({ encode: applyDefualts(encode, { x1: zero() }), })); }

MaybeZeroX1.props = { type: ‘inference’, }

  1. <a name="AxoRe"></a>
  2. ## Statistic
  3. 统计变换是最复杂一种变换,它涉及到数据的聚合,过滤等。同时它也是 G2 5.0 中最重要的一种变换,因为它直接决定了 G2 5.0 的统计分析能力。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/418707/1652672626585-94b2f6bf-0f36-4d45-a083-bfd71cd4c296.png#clientId=ue9bcffb7-53d5-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=231&id=gsMy9&margin=%5Bobject%20Object%5D&name=image.png&originHeight=818&originWidth=1326&originalType=binary&ratio=1&rotation=0&showTitle=true&size=473311&status=done&style=stroke&taskId=u4c064cf3-37a0-48be-9d28-34794b07c82&title=%E8%81%9A%E5%90%88%E5%89%8D&width=374 "聚合前")![image.png](https://cdn.nlark.com/yuque/0/2022/png/418707/1652678295228-dae5f83e-f622-4161-8642-cf9982574c88.png#clientId=u0a139caf-0be1-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=231&id=sUVKo&margin=%5Bobject%20Object%5D&name=image.png&originHeight=822&originWidth=1254&originalType=binary&ratio=1&rotation=0&showTitle=true&size=226322&status=done&style=stroke&taskId=u10798c56-7c14-4743-ba95-35d8a990170&title=%E8%81%9A%E5%90%88%E5%90%8E&width=352 "聚合后")
  4. <a name="h8Bdo"></a>
  5. ### 相关工作
  6. 一般的的图表库都是在 encoding 之前去预处理数据,生成新的字段,然后将生成的字段参与 encoding。但是 Vega Lite 和 Plot 的声明方式却与众不同,是很接近图形语法的声明方式。
  7. **Vega Lite**
  8. - 特点:Encoding 和 Statistic 其实是在一起声明的。
  9. - 优点:简洁。
  10. - 缺点:Encoding 的配置较为复杂。
  11. ```javascript
  12. // Vega Lite
  13. const vegaLite = {
  14. data: { value: athletes },
  15. mark: 'bar',
  16. encoding: {
  17. x: { field: 'sex', group: true },
  18. y: { aggregate: 'mean', field: 'weight' },
  19. },
  20. };

通过编译之后等 Vega 配置可以看出 Vega Lite 实现统计变换的方法是通过 transform 生成新的字段,然后用新的字段参与 encoding 的声明。这样的实现的优缺点如下:

  • 优点:实现简单,因为把 transform 和 encoding 的逻辑解耦开来,所以 transform 模块只需要关心数据本身的变换。
  • 缺点:
    • 字段冲突:生成的字段可能和已有的字段冲突(可能性很小)。
    • 需要额外的机制把 statistic 映射为 transform 和 encoding。
      1. const vega = {
      2. data: {
      3. value: athletes,
      4. transform: [{
      5. type: 'aggregate',
      6. groupby: ['sex'],
      7. fields: ['weight'],
      8. ops: ['mean'],
      9. // 生成中间字段
      10. as: ['__y']
      11. }],
      12. },
      13. marks: [{
      14. type: 'bar',
      15. encoding: {
      16. x: { field: 'sex' },
      17. // 使用中间字段,这个字段是不会被用户感知的
      18. y: { field: '__y' }
      19. }
      20. }],
      21. };

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();

  1. Plot 的实现 statistic 的思路和 Vega-Lite 是不一样的,它不存在一个中间的过渡字段,而是直接对 encoding 进行修改。这样实现的优缺点如下:
  2. - 优点:不存在字段冲突。
  3. - 缺点:实现起来比较复杂,这里的复杂度一方面主要来自于一下几个方面:
  4. - 同时对 data encoding 进行转换。
  5. - Plot API 设计让它在转换的时候不能获取到 data,需要通过 lazy channel 的方式延迟获得 data 的值。
  6. - 不同于常规的数据处理,这里是直接操作一列数据,而不是一行数据。
  7. ```javascript
  8. // Stack 的例子
  9. function stack(x, y = one, ky, {offset, order, reverse}, options) {
  10. const z = maybeZ(options);
  11. const [X, setX] = maybeColumn(x);
  12. const [Y1, setY1] = column(y);
  13. const [Y2, setY2] = column(y);
  14. offset = maybeOffset(offset);
  15. order = maybeOrder(order, offset, ky);
  16. return [
  17. basic(options, (data, facets) => {
  18. const X = x == null ? undefined : setX(valueof(data, x));
  19. const Y = valueof(data, y, Float64Array);
  20. const Z = valueof(data, z);
  21. const O = order && order(data, X, Y, Z);
  22. const n = data.length;
  23. const Y1 = setY1(new Float64Array(n));
  24. const Y2 = setY2(new Float64Array(n));
  25. const facetstacks = [];
  26. for (const facet of facets) {
  27. const stacks = X ? Array.from(group(facet, i => X[i]).values()) : [facet];
  28. if (O) applyOrder(stacks, O);
  29. for (const stack of stacks) {
  30. let yn = 0, yp = 0;
  31. if (reverse) stack.reverse();
  32. for (const i of stack) {
  33. const y = Y[i];
  34. if (y < 0) yn = Y2[i] = (Y1[i] = yn) + y;
  35. else if (y > 0) yp = Y2[i] = (Y1[i] = yp) + y;
  36. else Y2[i] = Y1[i] = yp; // NaN or zero
  37. }
  38. }
  39. facetstacks.push(stacks);
  40. }
  41. if (offset) offset(facetstacks, Y1, Y2, Z);
  42. return {data, facets};
  43. }),
  44. X,
  45. Y1,
  46. Y2
  47. ];
  48. }

设计

  • 特点:encode 和 statsitic 分开声明,同时由数组代替了嵌套的形式。

    1. // G2 5.0
    2. const g2 = {
    3. data: athletes,
    4. type: 'interval',
    5. transform: [
    6. { type: 'groupX', y: 'sum' }
    7. ],
    8. encode: { x: 'sex', y: 'weight', fill: 'sex' },
    9. };
    1. function StackY() {
    2. return merge((context) => {
    3. const { data, I, columnOf, encode } = context;
    4. const { x, y } = encode;
    5. // 从原始数据中提取出下面两列数据
    6. const X = columnOf(data, x);
    7. const Y = columnOf(data, y);
    8. // 按照 X 通道分组
    9. const groups = Array.from(group(I, (i) => X[i]));
    10. // 堆叠每一组内的 mark 的 y 通道
    11. const newY = new Array(I.length);
    12. const newY1 = new Array(I.length);
    13. for (const G of groups) {
    14. for (let py = 0, i = 0; i < G.length; i += 1) {
    15. const index = I[i];
    16. newY1[index] = py;
    17. newY[index] = py + Y[index];
    18. py = newY[index];
    19. }
    20. }
    21. return {
    22. // 更新 encode
    23. encode: {
    24. ...encode,
    25. x: column(X),
    26. y: column(Y),
    27. y1: column(Y1)
    28. },
    29. };
    30. });
    31. }

    种类

  • dodgeX

  • stackY
  • groupX
  • binX
  • binY
  • bin
  • selectFirst
  • selectLast
  • selectMax
  • selectMin
  • summary

    案例验证

    矩阵树图

    image.png ```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’ } };

  1. <a name="ZjgfU"></a>
  2. ### Voronoi 图
  3. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/418707/1654502978747-d91240a3-ef96-492f-9cb5-e8daa2fb6334.png#clientId=uba5cc825-7196-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=356&id=u828b06a7&margin=%5Bobject%20Object%5D&name=image.png&originHeight=712&originWidth=1284&originalType=binary&ratio=1&rotation=0&showTitle=false&size=727074&status=done&style=stroke&taskId=ub2b6f2e2-afb1-4ac6-9d07-915b78c20f1&title=&width=642)
  4. ```javascript
  5. // https://g2.antv.vision/zh/examples/relation/relation#voronoi
  6. const config = {
  7. type: 'polygon',
  8. data,
  9. transform: [{type: 'voronoi'}],
  10. encode: {
  11. color: 'value',
  12. label: 'value'
  13. },
  14. };

桑基图

image.png

  1. const config = {
  2. type: 'view',
  3. data,
  4. children: [
  5. {
  6. type: 'polygon',
  7. transform: [{type: 'sankey.nodes'}],
  8. encode: {
  9. color: 'name',
  10. },
  11. },
  12. {
  13. type: 'polygon',
  14. transform: [{type: 'sankey.links'}],
  15. encode: {
  16. shape: 'arc',
  17. color: 'name'
  18. }
  19. }
  20. ],
  21. }

力导向图

image.png

  1. const config = {
  2. type: 'view',
  3. data,
  4. children: [
  5. {
  6. type: 'point',
  7. transform: [{type: 'force.nodes'}],
  8. encode: {
  9. color: 'name',
  10. },
  11. },
  12. {
  13. type: 'edge',
  14. transform: [{type: 'force.links'}],
  15. }
  16. ],
  17. }