!!!###!!!title=高级指引——VisActor/VGrammar 教程文档!!!###!!!!!!###!!!description=VGrammar 作为图形语法,提供了非常强大的自定义的能力,方便用户去扩展,本教程将会介绍一些自定义拓展的能力!!!###!!!

高级指引

VGrammar 作为图形语法,提供了非常强大的自定义的能力,方便用户去扩展,本教程将会介绍一些自定义拓展的能力

自定义 transform

实现

数据变换 tranform ,其实就是一个纯函数;我们要实现自定义的 transform,就是实现一下类型的纯函数:

export type IFunctionTransform<Options = any, Input = any, Output = any> = (
  options?: Options,
  data?: Input,
  params?: Record<string, any>,
  view?: IView
) => Output | Promise<Output> | IProgressiveTransformResult<Output>;

所有的 transform 都接收四个参数:

  • opionts 配置
  • data 执行变换的数据
  • params 配置数据变换的语法元素,依赖的其他语法元素的结果
  • view 全局的 VGrammar 可视化实例,一般只有少数布局算法,可能需要用到,不建议使用

以内置的filter transform 的实现为例:

export const filter = (
  options: {
    callback: (entry: any, params: any) => boolean;
  },
  data: any[],
  parameters?: any
) => {
  return data.filter(entry => {
    return options.callback(entry, parameters);
  });
};

注册

注册 transform 只需要调用Factory.registerTransform()进行注册

示例:

import { Factory } from '@visactor/vgrammar';

Factory.registerTransform('filter2', {
  transform: filter,
  markPhase: 'beforeJoin'
});

使用

注册完毕,可以和内置的 transform 类似使用,只需要传入需要的配置即可

{
  data: [
    {
      id: 'table',
      values: [{ a: 1 }],
      transform: [
        {
          type: 'filter2',
          callback: datum => {
            return datum.a > 0;
          }
        }
      ]
    }
  ];
}

自定义组合图元

VGrammar 内置了一些常见的组合图元,当基础图元以及内置的组合图元均不能满足需求的时候,用户可以通过自定义组合图元的方式,来进行扩展

注册

以内置的wave组合图元的实现为例,在实现自定义组合图元的时候,首先我们需要注册一个全局唯一的name,以及组成图元的基础图元的类型和name; 例如wave图元是由三条面积填充线组成的,所以我们实现的代码如下:

import { Factory } from '@visactor/vgrammar';

const waveGlyphMeta = Factory.registerGlyph('wave', {
  wave0: 'area',
  wave1: 'area',
  wave2: 'area'
});

接下来,我们可以设置组合图元需要支持的视觉通道,VGrammar 支持三种类型的视觉通道设置:

  • registerDefaultEncoder() 设置子图元的默认图形属性
  • registerChannelEncoder() 设置自定义的图形通道,用户需要实现当组合图元设置了该视觉通道的时候,哪些子图元需要更新对应的图形属性
  • registerFunctionEncoder() 当组合图元的多个图形属性,最终只需要映射到某个子图元的单个属性的时候,可以注册函数类型的视觉通道解析函数;

registerDefaultEncoder()registerChannelEncoder() 的实现示例如下:

waveGlyphMeta
  .registerChannelEncoder('wave', (channel, encodeValue, encodeValues, datum, element) => {
    const originPoints: IPointLike[] = new Array(21).fill(0).map((v, index) => {
      const waveHeight = index % 2 === 0 ? 20 : 0;
      return { x: -500 + 50 * index, y: encodeValues.y + waveHeight, y1: encodeValues.y + encodeValues.height };
    });
    const points0 = originPoints.map(point => {
      return { x: point.x + encodeValue * 100, y: point.y, y1: point.y1 };
    });
    const points1 = originPoints.map(point => {
      return { x: point.x + encodeValue * 200 - 40, y: point.y, y1: point.y1 };
    });
    const points2 = originPoints.map(point => {
      return { x: point.x + encodeValue * 300 - 20, y: point.y, y1: point.y1 };
    });
    return {
      wave0: { points: points0, x: 0, y: 0 },
      wave1: { points: points1, x: 0, y: 0 },
      wave2: { points: points2, x: 0, y: 0 }
    };
  })
  .registerDefaultEncoder(() => {
    return {
      wave0: { curveType: 'monotoneX', fillOpacity: 1 },
      wave1: { curveType: 'monotoneX', fillOpacity: 0.66 },
      wave2: { curveType: 'monotoneX', fillOpacity: 0.33 }
    };
  });

registerFunctionEncoder() 的实现可以参考linkPath组合图元的实现:

linkPathGlyphMeta.registerFunctionEncoder(
  (encodeValues: LinkPathEncodeValues, datum: any, element: IElement, config: LinkPathConfig) => {
    const direction = encodeValues.direction ?? config?.direction;
    const parsePath = ['vertical', 'TB', 'BT'].includes(direction) ? getVerticalPath : getHorizontalPath;
    const isRatioShow = typeof encodeValues.ratio === 'number' && encodeValues.ratio >= 0 && encodeValues.ratio <= 1;

    const encodeChannels = Object.keys(encodeValues);
    // parse path when all required channels are included
    if (['x0', 'y0', 'x1', 'y1'].every(channel => encodeChannels.includes(channel))) {
      return {
        back: {
          path: isRatioShow ? parsePath(encodeValues, 1) : ''
        },
        front: {
          path: parsePath(encodeValues, isRatioShow ? encodeValues.ratio : 1)
        }
      };
    }

    return {};
  }
);

使用

组合图元注册完成后,就可以和内置的组合图元一样使用了:

{
  marks: [
    {
      type: 'glyph',
      glyphType: 'wave',
      encode: {
        update: {
          y: 100,
          height: 100,
          fill: 'DarkOrange',
          wave: 0
        }
      }
    }
  ];
}

自定义语法元素

VGrammar 所有的语法元素,都是按照依赖关系进行运行的,当现有的语法元素不能满足要求的时候,用户可以试着实现自定义的语法元素;接下来以vgrammar-projection中实现的语法元素Projection为例,讲述如何自定义语法元素,并在 VGrammar 可视化图表中使用;

实现语法类

我们实现自定义的语法元素的时候,需要继承语法元素的基类GrammarBase,实现一下三个主要的方法:

  • parse(spec: CustomizedSpec) 解析配置的方法,配置的类型需要定义
  • evaluate(upstream: any, parameters: any) 执行逻辑,在语法元素被执行的时候,会传入上游依赖的数据,和其他依赖参数
  • output() 返回语法元素的输出对象,会被下游节点获取到并执行后续的逻辑

示例:

import { GrammarBase } from '@visactor/vgrammar';

export class Projection extends GrammarBase implements IProjection {
  readonly grammarType: GrammarType = 'projection';

  private projection: any;

  constructor(view: IView) {
    super(view);
  }

  parse(spec: ProjectionSpec) {
    super.parse(spec);
    this.spec = mergeConfig(this.spec, spec);
    this.attach(parseProjection(spec, this.view));

    return this;
  }

  evaluate(upstream: any, parameters: any) {
    if (!this.projection || this.projection.type !== this.spec.type) {
      this.projection = create(this.spec.type);
      this.projection.type = this.spec.type;
    }
    projectionProperties.forEach(prop => {
      if (!isNil(this.spec[prop])) {
        set(this.projection, prop, invokeFunctionType(this.spec[prop], parameters, projection));
      }
    });

    if (!isNil(this.spec.pointRadius)) {
      this.projection.path.pointRadius(invokeFunctionType(this.spec.pointRadius, parameters, projection));
    }
    if (!isNil(this.spec.fit)) {
      const fit = invokeFunctionType(this.spec.fit, parameters, projection);
      const data = collectGeoJSON(fit);

      if (this.spec.extent) {
        this.projection.fitExtent(invokeFunctionType(this.spec.extent, parameters, projection), data);
      } else if (this.spec.size) {
        this.projection.fitSize(invokeFunctionType(this.spec.size, parameters, projection), data);
      }
    }

    return this.projection;
  }

  output() {
    return this.projection;
  }
}

注册

接下来只需要调用注册方法对自定义的语法元素进行注册:

import { registerGrammar } from '@visactor/vgrammar';

registerGrammar('projection', Projection, 'projections');

registerGrammar 接收三个参数:

  • type: 唯一标志符,对应了语法元素实例中的grammarType
  • grammarClass: 自定义语法类
  • specKey: 可选参数;在 spec 模式下,申明语法元素的key,注意,不能和内置的语法元素重复,如果不传则保持和type一致

使用

在 Spec 模式下,可以通过注册语法元素时候,申明的specKey进行设置

vGrammarView.parseSpec({
  projections: [
    {
      id: 'firstProjection',
      size: [10, 10]
    }
  ]
});

在 API 模式下,可以通过customized() API 创建自定义的语法元素

vGrammarView
  .customized('projection', {
    size: [10, 10]
  })
  .id('firstProjection');