!!!###!!!title=Visual Channel Mapping——VisActor/VChart Contributing Documents!!!###!!!!!!###!!!description=---title: 6.2 Visual Channel Mapping key words: VisActor,VChart,VTable,VStrory,VMind,VGrammar,VRender,Visualization,Chart,Data,Table,Graph,Gis,LLM---!!!###!!!

Introduction

Visual mapping is the bridge between data and images, mapping the "data model" to the "image model" and selecting suitable visual variables for different types of data. For example, if we use a bar chart to represent the average scores of boys and girls in a class, we can map the gender attribute in the data to the color attribute in the image, and map the score attribute in the data to the height (Y-axis coordinate) attribute of the bar chart in the image. Below, we analyze how data is mapped to the final image through a simple use case.

Mapping Process of Graphical Elements

Usage Example

const spec = {
  type: 'line',
  data: [
    {
      id: 'lineData',
      values: [
        { date: 'Monday', class: 'class No.1', score: 20 },
        { date: 'Monday', class: 'class No.2', score: 30 },

        { date: 'Tuesday', class: 'class No.1', score: 25 },
        { date: 'Tuesday', class: 'class No.2', score: 28 }
      ]
    }
  ],
  seriesField: 'class',
  xField: 'date',
  yField: 'score',
  point: {
    style: {
      fill: 'blue'
    }
  }
};

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

This example creates a line type glyph series to display 4 data points, where points with the same class attribute will be connected into a line. The date attribute is mapped to the X-axis coordinate, and the score attribute is mapped to the Y-axis coordinate. The effect is as follows:

Creation of Primitives

Below we analyze through code how renderSync() parses the configuration spec and generates various primitives on the graph. Overall, renderSync() includes the following three stages: pre-rendering, rendering, and post-rendering.

  // packages/vchart/src/core/vchart.ts
  protected ***_renderSync*** = (option: IVChartRenderOption = {}) => {
    const self = this as unknown as IVChart;
    if (!this.***_beforeRender***(option)) {
      return self;
    }
    this._compiler?.***render***(option.morphConfig);
    this.***_afterRender***();
    return self;
  };    

The rendering process belongs to the domain of VGrammar, and after rendering, the main focus is on updating the animation state. We mainly focus on the pre-render preparation related to the graphics, including initializing chart configurations, instantiating charts, and compiling rendering instructions.

1. Initialize Chart Configurations
  // packages/vchart/src/core/vchart.ts
  private ***_initChartSpec***(spec: any, actionSource: VChartRenderActionSource) {
    // 如果用户注册了函数,在配置中替换相应函数名为函数内容
    if (VChart.***getFunctionList***() && VChart.***getFunctionList***().length) {
      spec = ***functionTransform***(spec, VChart);
    }
    this._spec = spec;
    // 创建图表配置转换器,并转换为common chart的配置
    if (!this._chartSpecTransformer) {
      this._chartSpecTransformer = Factory.***createChartSpecTransformer***(
        this._spec.type,
        this.***_getChartOption***(this._spec.type)
      );
    }
    this._chartSpecTransformer?.***transformSpec***(this._spec);
    // 转换模型配置
    this._specInfo = this._chartSpecTransformer?.***transformModelSpec***(this._spec);
  }    

First, replace the function name registered by the user with the corresponding function entity. Then, according to the chart type, create the corresponding chart configuration converter to convert the configuration of this type of chart into the configuration of the common type chart. This includes creating a default series based on the chart type and completing the user-defined series configuration:

    // packages/vchart/src/chart/cartesian/cartesian-transformer.ts
    const defaultSeriesSpec = this.***_getDefaultSeriesSpec***(spec);
    if (!spec.series || spec.series.length === 0) { // 没有用户定义的系列 采用默认
      spec.series = [defaultSeriesSpec];
    } else {
      spec.series.***forEach***((s: ISeriesSpec) => {
        if (!this.***_isValidSeries***(s.type)) { // 判断用户定义系列是否有效
          return;
        }
        Object.***keys***(defaultSeriesSpec).***forEach***(k => { // 补全配置
          if (!(k in s)) {
            s[k] = defaultSeriesSpec[k];
          }
        });
      });
    }    

2. Instantiate the Chart

Next, create a chart object of the corresponding type. The chart object here does not refer to the VChart object created below. VChart encapsulates the chart, serving as the entry point for user operations, responsible for global management and external interfaces of the chart;

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

The instantiated chart here is responsible for the specific chart construction (such as creating and managing series, components) and internal logic processing (managing data flow, global mapping, graphic element states, etc.).

  // packages/vchart/src/core/vchart.ts
  private ***_initChart***(spec: any) {
    // 创建真正的图表对象
    const chart = Factory.***createChart***(spec.type, spec, this.***_getChartOption***(spec.type));
    this._chart = chart;
    // 进行图表初始化
    this._chart.***setCanvasRect***(this._currentSize.width, this._currentSize.height);
    this._chart.***created***();
    this._chart.***init***();
    this._event.***emit***(ChartEvent.initialized, {
      chart,
      vchart: this
    });
  }    

The core steps are created and init, the former creates various elements according to spec, such as region, series, components, and the latter initializes each element. We focus on the graphic elements in the creation of series.

  // packages/vchart/src/series/base/base-series.ts
  ***created***(): void {
    ...
    this.***initMark***();
    ...
  }    

Since the type of our chart is line, there is a default line series. We look at the implementation of initMark in the line-series:

  // packages/vchart/src/series/line/line.ts
  ***initMark***(): void {
    ...
    const seriesMark = this._spec.seriesMark ?? *'line'*;
    this.***initLineMark***(progressive, seriesMark === *'line'*);
    this.***initSymbolMark***(progressive, seriesMark === *'point'*);
  }    

It was found that indeed the line primitives and symbol primitives continued to be created:

After a series of function calls (LineLikeSeriesMixin.initLineMark -> BaseSeries._createMark -> BaseModel._createMark -> Factory.createMark), it finally reaches the constructor of the corresponding graphic element, which is the "implementation of specific graphic elements" mentioned earlier.

3. Compile Rendering Instructions

Compile various VChart models (region, series, component) into renderable VGrammar syntax elements, involving the content of the VGrammar syntax layer, which will not be analyzed in detail.

Mapping of Graphic Element Visual Configuration

In the BaseMark class, graphic elements achieve the mapping from data to visual channels through a series of methods and logic. This can be roughly divided into two processes: the storage of attributes and the calculation of attribute values. The former simply parses the user-defined spec and stores it in the style sheets of various states of the graphic element, during which some simple conversions are made; the latter is where the user of the graphic element truly obtains and calculates specific attribute values when laying out the graphic element.

Step1 Store Style

1. Initialize Style

Initialize the default style of the graphic element, call the setStyle method to set the default value for the normal state:

private ***_initStyle***(): void {
  const defaultStyle = this.***_getDefaultStyle***();
  this.***setStyle***(defaultStyle, *'normal'*, 0);
}    

  • Default styles include visible: true, x: 0, y: 0, etc.

  • These default values ensure that the elements render correctly even without user-defined styles.

The initStyleWithSpec method initializes styles based on the user-provided spec:

initStyleWithSpec(spec: IMarkSpec<T>, key?: string) {
  if (!spec) return;

  if (isValid(spec.id)) this._userId = spec.id;
  if (isBoolean(spec.interactive)) this._markConfig.interactive = spec.interactive;
  if (isValid(spec.zIndex)) this._markConfig.zIndex = spec.zIndex;
  if (isBoolean(spec.visible)) this.setVisible(spec.visible);

  this._initSpecStyle(spec, this.stateStyle, key);
}    

  • Parse user-defined attributes such as interactive, zIndex, visible, etc.

  • Call the _initSpecStyle method to handle style and state. This part mainly involves calling setStyle to set the corresponding style for each state (including the initial normal state) and storing the state information in the state manager. We explain the state in detail.

The above methods all call the core function setStyle, which is used to set styles for specified states:

  ***setStyle***<U extends keyof T>(
    style: Partial<IMarkStyle<T>>, // 样式
    state: StateValueType = *'normal'*, // 状态
    level: number = 0, // 状态层级 当处于不同状态产生冲突时 根据层级设置样式
    stateStyle = this.stateStyle // 存储状态样式
  ): void {
    if (***isNil***(style)) {
      return;
    }
    if (stateStyle[state] === undefined) {
      stateStyle[state] = {};
    }
    const isUserLevel = this.***isUserLevel***(level);
    Object.***keys***(style).***forEach***((attr: string) => {
      let attrStyle = style[attr] as MarkInputStyle<T[U]>;
      if (***isNil***(attrStyle)) {
        return;
      }
      // 过滤和转化样式
      attrStyle = this.***_filterAttribute***(attr as any, attrStyle, state, level, isUserLevel, stateStyle);
      // 设置样式
      this.***setAttribute***(attr as any, attrStyle, state, level, stateStyle);
      /*  在setAttribute中设置属性计算方式/样式
          stateStyle[state][attr] = {
            level,
            style,
            referer: undefined
          };
      */
    });
  }    

2. Filter and Transform Styles

The _filterAttribute called in setStyle is used to filter and transform individual style attributes, ensuring that the style attributes conform to internal usage standards. These transformations are relatively simple, as noted in the comments.

  protected ***_filterAttribute***<U extends keyof T>(
    attr: U,
    style: MarkInputStyle<T[U]>,
    state: StateValueType,
    level: number,
    isUserLevel: boolean,
    stateStyle = this.stateStyle
  ): StyleConvert<T[U]> {
    // *** **将visual spec转换为 scale 类型的 mark style** ***
    // 用于后续计算属性值
    let newStyle = this.***_styleConvert***(style);
    
    if (isUserLevel) {
      switch (attr) {
        case *'angle'*:
          // 角度值转弧度值
          newStyle = this.***convertAngleToRadian***(newStyle);
          break;
        case *'innerPadding'*:
        case *'outerPadding'*:
          // VRender 的 padding 定义基于 centent-box 盒模型,默认正方向是向外扩,与 VChart 不一致。这里将 padding 符号取反
          newStyle = this.***_transformStyleValue***(newStyle, (value: number) => -value);
          break;
        case *'curveType'*:
          // 根据direction返回'*monotoneY*'(*Direction.horizontal*)或'*monotoneX*'
          newStyle = this.***_transformStyleValue***(newStyle, (value: string) =>
            ***curveTypeTransform***(value, (this._option.model as any).direction)
          );
          break;
      }
    }
    return newStyle;
  }    

It is particularly important to note that in styleConvert, some styles that need to be converted to scale type are transformed for subsequent attribute value calculations. For example, convert yField: 'score' to:

{
  scale, // 映射对象,用于数据到视觉通道的映射,可以理解为一个函数,输入数据对应的值,输出视觉通道的值
  field: 'score', // 数据字段名,表示映射的输入字段。
  changeDomain: true // 布尔值,表示是否允许动态更新比例尺的定义域(domain)
};    

This is a scale type style, where the first field scale calculates the value of the data corresponding field datum['score'] as the y coordinate of the graphic element.

Step2 Calculate Attribute Values

BaseMark provides an interface getAttribute for its users to calculate and obtain attribute values based on actual data.

  ***getAttribute***<U extends keyof T>(key: U, datum: Datum, state: StateValueType = *'normal'*, opt?: IAttributeOpt) {
    return this.***_computeAttribute***(key, state)(datum, opt);
  }    

Here, the compteAttribute(key, state) returns a function for calculating an attribute, where key is the attribute name and state is the current state; (datum, opt) is used as the parameter for this function, returning the calculation result, consistent with our above description of “the method of storing attribute calculations”.

  protected ***_computeAttribute***<U extends keyof T>(key: U, state: StateValueType) {
    let stateStyle = this.stateStyle[state]?.[key];
    if (!stateStyle) {
      stateStyle = this.stateStyle.normal[key];
    }
    const baseValueFunctor = this.***_computeStateAttribute***(stateStyle, key, state);
    const hasPostProcess = ***isFunction***(stateStyle?.***postProcess***);
    const hasExCompute = key in this._computeExChannel;
    // ... 
    // 叠加后处理函数和额外计算函数
    // ...
    return baseValueFunctor;
  }    

Continuing to delve into computeStateAttribute, you will find that an attribute calculation function is constructed here. The input of this function is (datum, opt), and the output is the calculated attribute value. If the attribute value is a constant (unrelated to the data, fixed on spec), then this constructed function directly returns style; what really needs to be calculated are some complex styles and mappings from data to visuals~~(recycling theme)~~.

  protected ***_computeStateAttribute***<U extends keyof T>(stateStyle: any, key: U, state: StateValueType) {
    if (!stateStyle) { // 处理空样式
      return (datum: Datum, opt: IAttributeOpt) => undefined as any;
    }
    if (stateStyle.referer) { // 处理引用样式
      return stateStyle.referer.***_computeAttribute***(key, state);
    }
    if (!stateStyle.style) { // 处理空样式
      return (datum: Datum, opt: IAttributeOpt) => stateStyle.style;
    }
    // =====================================================================
    // **处理函数样式**:如果 stateStyle.style 是函数,调用该函数计算属性值。
    if (typeof stateStyle.style === *'function'*) {
      return (datum: Datum, opt: IAttributeOpt) =>
        stateStyle.***style***(datum, this._attributeContext, opt, this.***getDataView***());
    }
    // **渐变色处理**,支持各个属性回调
    if (GradientType.***includes***(stateStyle.style.gradient)) {
      return this.***_computeGradientAttr***(stateStyle.style);
    }
    // **内外描边处理**,支持各个属性回调
    if ([*'outerBorder'*, *'innerBorder'*].***includes***(key as string)) {
      return this.***_computeBorderAttr***(stateStyle.style);
    }
    // **处理映射样式**:如果 stateStyle.style 包含映射关系(scale),根据数据字段映射值。
    if (***isValidScaleType***(stateStyle.style.scale?.type)) {
      return (datum: Datum, opt: IAttributeOpt) => {
        let data = datum;
        if (this.model.modelType === *'series'* && (this.model as ISeries).***getMarkData***) {
          data = (this.model as ISeries).***getMarkData***(datum);
        }
        return stateStyle.style.scale.***scale***(data[stateStyle.style.field]);
      };
    }
    // =====================================================================
    // **处理常量样式**:如果 stateStyle.style 是常量值,直接返回该值。
    return (datum: Datum, opt: IAttributeOpt) => {
      return stateStyle.style;
    };
  }    

Emphasize the scale style, which includes the data-to-visual mapping part. Continuing with the above example, we have already constructed a scale style:

style: {
  scale, 
  field: 'score',
  changeDomain: true,
}    

If we need to calculate the y coordinate of the graphic element, first obtain the data bound to the graphic element (see Chapter 5 VChart Data Processing), and then use the scale mapping object to input data['score'] to get the corresponding y value. For more information about scale, see Chapter 7 VChart Scale.

This document was revised and organized by the following personnel

玄魂