!!!###!!!title=自定义图元——VisActor/VChart 社区贡献者文档!!!###!!!!!!###!!!description=---title: 6.4 自定义图元 key words: VisActor,VChart,VTable,VStrory,VMind,VGrammar,VRender,Visualization,Chart,Data,Table,Graph,Gis,LLM--- !!!###!!!

简介

当我们有大量数据需要在图表上展示时,一般需要定义一个特定类型的系列(详见3.2节:series),在系列中创建图元来表示数据;然而,如果我们仅仅需要展示一点额外的信息,例如一张图片,一个标题,一条路径,而不想为之单独创建一个系列时,就可以用到自定义图元(customMark)。自定义图元可以让用户在图表上添加自定义的标记,比如添加一些文本、图片、线段等。

类型及示例

目前,所支持的自定义图元有如下几种类型:

  • symbol:点图形

  • rule:线段

  • text:文本

  • rect:矩形

  • path:路径

  • arc:扇区

  • polygon:多边形

  • group:组,可以将其他mark放到组下

如下例,在图表中除了两个数据点外(可以看作是一个scatter series), 在上方添加了三个自定义图元,分别为text, symbol, rule

```xml const spec = { type: 'scatter', xField: 'x', yField: 'y', data: [ { name: 'data', values: [{y: 10,x: 'point1'},{y: 31,x: 'point2'}] } ], customMark: [ { // 自定义text图元 type: 'text', style: { fontSize: 20, fontWeight: 600, text: "Not a data point:", x: 300, y: 25, fill: 'grey', } }, { // 自定义symbol图元 type: 'symbol', style:{ x: 320, y: 18, fill: 'grey', size: 30 } }, { // 自定义rule图元 type: 'rule', style:{ x: 120, y: 35, x1: 310, y1:35, stroke:'grey' } } ] }; const vchart = new VChart(spec, { dom: CONTAINER_ID }); vchart.renderSync();
</div></div>
# 代码实现

自定义图元的代码入口:`packages/vchart/src/component/custom-mark`    

1. 自定义图元的创建和管理    

*  通过 `initMarks `方法,根据用户提供的配置`spec`创建自定义图元。    

*  支持嵌套的图元结构(如`group`类型的图元可以包含子图元)。    

```Typescript
  protected ***initMarks***() {
    if (!this._spec) {
      return;
    }
    const series = this._option && this._option.***getAllSeries***();
    const hasAnimation = this._option.animation !== false;
    const depend: IMark[] = [];

    if (series && series.length) {
      series.***forEach***(s => {
        const marks = s && s.***getMarksWithoutRoot***();

        if (marks && marks.length) {
          marks.***forEach***(mark => {
            depend.***push***(mark);
          });
        }
      });
    }
    // 设置组图元的父图元
    let parentMark: IGroupMark | null = null;
    if (this._spec.parent) {
      const mark = this.***getChart***()
        .***getAllMarks***()
        .***find***(m => m.***getUserId***() === this._spec.parent) as IGroupMark;
      if (mark.type === *'group'*) {
        parentMark = mark;
      }
    }
    // 递归调用的入口,递归地创建每一个图元
    this.***_createExtensionMark***(this._spec, parentMark, *`${PREFIX}_series_${this.id}_extensionMark`*, 0, {
      depend,
      hasAnimation
    });
  }    

  • 图元的样式和数据绑定
  • 使用 _createExtensionMark 方法为每个图元设置样式、动画配置和数据绑定。

  • 支持通过 dataIddataIndex绑定数据视图(DataView)。

  private ***_createExtensionMark***(
    spec: ICustomMarkSpec<Exclude<EnableMarkType, *'group'*>> | ICustomMarkGroupSpec,
    parentMark: null | IGroupMark,
    namePrefix: string,
    index: number = 0,
    options: { hasAnimation?: boolean; depend?: IMark[] }
  ) {
    // 创建图元
    const mark = this.***_createMark***(
      {
        type: spec.type,
        name: ***isValid***(spec.name) ? *`${spec.name}`* : *`${namePrefix}_${index}`*
      },
      {
        // 避免二次dataflow
        skipBeforeLayouted: true,
        attributeContext: this.***_getMarkAttributeContext***(),
        componentType: spec.componentType,
        key: spec.dataKey
      }
    ) as IGroupMark;
    if (!mark) {
      return;
    }
    if (***isValid***(spec.id)) {
      mark.***setUserId***(spec.id);
    }
    // 处理动画
    if (options.hasAnimation && spec.animation) {
      const config = ***animationConfig***({}, ***userAnimationConfig***(spec.type, spec as any, this._markAttributeContext));
      mark.***setAnimationConfig***(config);
    }
    // 构建组图元的层级结构
    if (options.depend && options.depend.length) {
      mark.***setDepend***(...options.depend);
    }
    if (***isNil***(parentMark)) {
      this._marks.***addMark***(mark);
    } else if (parentMark) {
      parentMark.***addMark***(mark);
    }
    // 设置样式
    this.***initMarkStyleWithSpec***(mark, spec);
    // 递归处理组图元
    if (spec.type === *'group'*) {
      namePrefix = *`${namePrefix}_${index}`*;
      spec.children?.***forEach***((s, i) => {
        this.***_createExtensionMark***(s as any, mark, namePrefix, i, options);
      });
    }
    // 处理数据绑定
    if (***isValid***(spec.dataId) || ***isValidNumber***(spec.dataIndex)) {
      const dataview = this.***getChart***().***getSeriesData***(spec.dataId, spec.dataIndex);
      if (dataview) {
        dataview.target.***addListener***(*'change'*, () => {
          mark.***getData***().***updateData***();
        });
        mark.***setDataView***(dataview);
      }
    }
  }    

  • 图元的上下文管理
  • 提供 getMarkAttributeContext_getMarkAttributeContext 方法,定义图元的上下文信息(如全局映射、布局边界等)。
  protected _markAttributeContext: IModelMarkAttributeContext;
  ***getMarkAttributeContext***() {
    return this._markAttributeContext;
  }
  
  private ***_getMarkAttributeContext***() {
    return {
      vchart: this._option.globalInstance,
      chart: this.***getChart***(),
      ***globalScale***: (key: string, value: string | number) => {
        return this._option.globalScale.***getScale***(key)?.***scale***(value);
      },
      ***getLayoutBounds***: () => {
        const { x, y } = this.***getLayoutStartPoint***();
        const { width, height } = this.***getLayoutRect***();
        return new ***Bounds***().***set***(x, y, x + width, y + height);
      }
    };
  }    

  • 布局和边界计算
  • 提供 getBoundsInRect _getLayoutRect 方法,用于计算图元的布局边界和尺寸。
  ***getBoundsInRect***(rect: ILayoutRect) {
    this.***setLayoutRect***(rect);

    const result = this.***_getLayoutRect***();
    const { x, y } = this.***getLayoutStartPoint***();
    return {
      x1: x,
      y1: y,
      x2: x + result.width,
      y2: y + result.height
    };
  }

  private ***_getLayoutRect***() {
    const bounds = new ***Bounds***();

    this.***getMarks***().***forEach***(mark => {
      const product = mark.***getProduct***();

      if (product) {
        bounds.***union***(product.***getBounds***());
      }
    });

    if (bounds.***empty***()) {
      return {
        width: 0,
        height: 0
      };
    }

    return {
      width: bounds.***width***(),
      height: bounds.***height***()
    };
  }    

Mark的比较

CustomMark 与 BaseMark 的区别

1. 定义的层次
  • BaseMark

  • 是一个基础类,直接定义了图元(Mark)的行为和样式。

  • 主要用于处理单个图元的样式、状态、属性计算等逻辑。

  • 继承自 CompilableMark,提供_computeAttribute _computeStateAttribute 方法,将高层配置转换为底层渲染指令。与底层渲染引擎(如 VGrammar)直接交互。

  • CustomMark

  • 是一个组件类,负责管理多个图元的创建和行为。

  • 继承自 BaseComponent,用于定义更高层次的自定义图元逻辑。

  • 通过调用 BaseMark或其他图元类的方法,间接与底层渲染引擎交互。

2. 主要职责
  • BaseMark

  • 负责单个图元的样式、状态和属性计算。

  • 提供方法如 setStylegetStylesetAttribute 等,用于操作单个图元的样式和属性。

  • 直接处理图元的渐变色、边框、角度转换等细节。

  • CustomMark

  • 负责管理多个图元的创建、样式设置和数据绑定。

  • 提供方法如 initMarks_createExtensionMark,用于根据配置动态创建图元。

  • 处理图元的上下文信息(如全局映射、布局边界)和与其他组件的依赖关系。

3. 数据绑定
  • BaseMark

  • 通过 setAttribute_computeAttribute方法,直接绑定数据到单个图元的属性上。

  • 支持通过映射(scale)和字段(field)动态计算属性值。

  • CustomMark

  • 通过 _createExtensionMark方法,为每个图元绑定数据视图(DataView)。

  • 支持通过 dataIddataIndex指定数据源。

4. 样式和动画
  • BaseMark

  • 提供 _initStyle_initSpecStyle方法,用于初始化单个图元的样式。

  • 支持渐变色、边框、角度转换等样式的动态计算。

  • CustomMark

  • _createExtensionMark方法中,为每个图元设置样式和动画配置。

  • 默认为自定义图元添加动画,但支持用户通过配置启用动画。

CustomMark 与 ExtensionMark 的区别

1. 从定义上看

CustomMark配置项的接口如下:

export interface ICustomMarkSpec<T extends EnableMarkType>
  extends IModelSpec,
    IMarkSpec<IBuildinMarkSpec[T]>,
    IAnimationSpec<string, string> {
  type: T;
*  // mark对应的名称,主要用于事件过滤如: { markName: 'yourName' }*
  name?: string;
*  // 关联的数据索引*
  dataIndex?: number;
*  // dataKey用于绑定数据与Mark的关系,如果数据和系列数据一致,可以不配置,默认会读取系列中的配置*
  dataKey?: string | ((datum: any) => string);
*  // 关联的数据id*
  dataId?: StringOrNumber;
*  // 指定组件类型*
  componentType?: string;
*  // 是否开启动画*
  animation?: boolean;
*  // 指定 parent Id*
  parent?: string;
}    

ExtensionMark配置项的接口如下:

export interface IExtensionMarkSpec<T extends Exclude<EnableMarkType, *'group'*>> extends ICustomMarkSpec<T> {
*  // 关联的数据索引*
  dataIndex?: number;
*  // dataKey用于绑定数据与Mark的关系,如果数据和系列数据一致,可以不配置,默认会读取系列中的配置*
  dataKey?: string | ((datum: any) => string);
*  // 关联的数据id*
  dataId?: StringOrNumber;
*  // 指定组件类型*
  componentType?: string;
}    

可以看出:

  • ICustomMarkSpec是一个通用的接口,用于定义自定义的 Mark(标记)。它继承了多个接口,包括 IModelSpecIMarkSpecIAnimationSpec,并且支持所有的 EnableMarkType类型。
EnableMarkType一览: `group`, `symbol`, `rule`, `line`, `text`, `rect`, `rect3d`, `image`, `path`, `area`, `arc`, `arc3d`, `polygon`, `pyramid3d`, `boxPlot`, `linkPath`, `ripple`
* `IExtensionMarkSpec`是 `ICustomMarkSpec`的扩展接口,但它限制了图元的类型,排除了 `group` 类型。
export interface IExtensionMarkSpec<T extends Exclude<EnableMarkType, *'group'*>> extends ICustomMarkSpec<T> {...}    

2. 从使用上看
  • extensionMark是图表支持用户在图表系列上补充绘制任意内容的自定义接口。

  • customMark可以让用户在图表上添加自定义的标记,比如添加一些文本、图片、线段等。

更具体的,当图表中包含多个series时,extensionMark的配置应当是放在series的配置当中的,属于对某个series的补充;而customMark是针对整个图表的,对图表信息的补充。二者所针对的对象和所在的层次不同。

如下例,我们定义了两个series,并分别为它们添加了一个文本类型的extensionMark(紫色部分),这些extensionMark的配置是属于某个series配置中的;与此同时,我们在series配置之外,添加了一个文本类型的customMark(蓝色部分),它的配置是属于整个图标配置的。

const spec = {
  type: 'common',
  data: [
    {
      id: 'id0',
      values: [
        { x: '周一', type: '早餐', y: 15 },
        { x: '周一', type: '午餐', y: 25 },
        { x: '周二', type: '早餐', y: 12 },
        { x: '周二', type: '午餐', y: 30 },
        { x: '周三', type: '早餐', y: 15 },
        { x: '周三', type: '午餐', y: 24 }
      ]
    },
    {
      id: 'id1',
      values: [
        { x: '周一', type: '饮料', y: 22 },
        { x: '周二', type: '饮料', y: 43 },
        { x: '周三', type: '饮料', y: 33 }
      ]
    }
  ],
  series: [
    {
      type: 'bar',
      dataIndex: 0,
      label: { visible: true },
      seriesField: 'type',
      xField: ['x', 'type'],
      yField: 'y',
      extensionMark:[{
        type: 'text',
        style: {
          fontSize: 20,
          fontWeight: 600,
          text: "extension-mark of series1",
          x: 450,
          y: 200,
          fill: 'blue',
        }
      }]
    },
    {
      type: 'line',
      dataIndex: 1,
      label: { visible: true },
      seriesField: 'type',
      xField: 'x',
      yField: 'y',
      stack: false,
      extensionMark:[{
        type: 'text',
        style: {
          fontSize: 20,
          fontWeight: 600,
          text: "extension-mark of series2",
          x: 300,
          y: 25,
          fill: 'orange',
        }
      }]
    }
  ],
  customMark:[{
    type: 'text',
    style: {
      fontSize: 20,
      fontWeight: 600,
      text: "custom-mark",
      x: 800,
      y: 200,
      fill: 'grey',
    }
  }],
  axes: [{ orient: 'left' }, { orient: 'bottom', label: { visible: true }, type: 'band' }],
};

const vchart = new VChart(spec, { dom: CONTAINER_ID });
vchart.renderSync();    

而如果把extensionMarkcustomMark的位置调换,都是不能被正确解析的。

如果图表的类型不是`common`,而是指定了类型(如`scatter`),则有一个**默认的**散点`series`,此时`extensionMark`放到图表的配置中也可以生效,默认属于散点`series`.

比较小结

VChart中出现了多种和Mark有关的概念:

  • Mark:基础图元,不仅仅可以对数据进行可视化(系列中的图元), 而且还构成了图表中的各个组件;

  • customMark:自定义图元,作为图表的组件,对图表信息进行补充说明;

  • extensionMark:扩展图元,在系列当中,不仅有表示数据的各种主要图元,还可以添加补充说明该系列信息的扩展图元;

  • markLinemarkAreamarkPoint:图表组件,同样对图表信息进行补充。

本文档由以下人员修正整理

玄魂