简介
视觉映射是数据与图像之间的桥梁,它将“数据模型”映射到“图像模型”,为不同类型的数据选择适合它的视觉变量。例如,我们使用柱状图表示一个班级中男生和女生的平均成绩,那么可以将数据中的性别属性映射到图像中的颜色属性,将数据中的成绩属性映射到图像中柱状图的高度(Y轴坐标)属性。下面我们通过一个简单的用例,分析数据是如何被映射到最终所看到的图像的。
图元的映射流程
使用示例
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();
该示例创建了一个line类型的图元系列来展示4条数据,其中class属性相同的点将被连成一条折线,date属性被映射到X轴坐标,score属性被映射到Y轴坐标,效果如下:

图元的创建
下面我们通过代码,分析renderSync()中是如何解析配置spec,并生成图上的各种图元的。总体来说,renderSync()中包含以下三个阶段,渲染前、渲染、渲染后。
// 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;
};
其中渲染过程属于VGrammar的范畴,而渲染后主要进行动画状态的更新,我们主要关注与图元有关的渲染前准备,包括初始化图表配置、实例化图表、编译渲染指令。
1. 初始化图表配置

// 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);
}
首先第一步,将用户注册的函数名替换为相应的函数实体。之后,根据图表类型,创建相应的图表配置转换器,将该类型图表的配置转换为common类型图表的配置。这其中包括,根据图表类型创建默认series,补全用户定义的series配置:
// 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. 实例化图表

接下来就是创建一个相应类型的图表对象。这里的图表对象并非指如下创建的VChart对象。VChart对图表进行了一次封装,是用户操作的入口,负责图表的全局管理和对外接口;
const vchart = new VChart(spec, { dom: CONTAINER_ID });
而这里实例化的chart图表负责具体的图表构建(如创建和管理系列、组件)和内部逻辑处理(管理数据流、全局映射、图元状态等)。
// 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
});
}
其中最核心的步骤是created和init,前者根据spec创建各项元素,如区域region, 系列series, 组件components,后者对各个元素进行初始化。我们重点关注创建series当中的图元部分。
// packages/vchart/src/series/base/base-series.ts ***created***(): void { ... this.***initMark***(); ... }
由于我们图表的类型是line,默认有一个line系列,我们到line-series中查看其initMark的实现:
// 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'*); }
发现其中确实继续创建了line图元和symbol图元:

经过一系列的函数调用(LineLikeSeriesMixin.initLineMark -> BaseSeries._createMark -> BaseModel._createMark -> Factory.createMark),最终到了相应图元的构造函数,即我们在中提到的“具体图元的实现”。
3. 编译渲染指令
将各种VChart模型(region, series, component)编译为可渲染的VGrammar语法元素,涉及到VGrammar语法层的内容,不做详细分析。
图元视觉配置的映射
在 BaseMark类中,图元通过一系列方法和逻辑实现了数据到视觉通道的映射。这大致可分为两个过程,属性的存储和属性值的计算。前者只是将用户定义的spec解析并存储到图元各个状态的样式表中,这期间会做一些简单的转换;后者是图元的使用者真正布局图元时获取和计算具体属性值。

Step1 存储样式
1. 初始化样式
初始化图元的默认样式,调用 setStyle方法为 normal状态设置默认值:
private ***_initStyle***(): void { const defaultStyle = this.***_getDefaultStyle***(); this.***setStyle***(defaultStyle, *'normal'*, 0); }
-
默认样式包括
visible: true、x: 0、y: 0 等。 -
这些默认值确保图元在没有用户定义样式时仍能正常渲染。
initStyleWithSpec方法根据用户传入的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);
}
-
解析用户定义的
interactive、zIndex、visible等属性。 -
调用
_initSpecStyle方法处理style和state。这一部分主要是通过调用setStyle,为每种状态(包括最开始的normal状态)设置对应的样式,并构成状态信息存储到状态管理器当中。关于状态,我们在中详细说明。
以上方法都调用了setStyle这个核心函数,该函数用于给指定的状态设置样式:
***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. 过滤和转换样式
在setStyle中调用的_filterAttribute是对单个样式属性进行过滤和转换,确保样式属性符合内部的使用规范。这些转换都较为简单,见注释。
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; }
需要特别注意的是_styleConvert中将一些需要转化成scale类型的样式进行转化,用于后续属性值的计算,例如,将yField: 'score'转化为:
{
scale, // 映射对象,用于数据到视觉通道的映射,可以理解为一个函数,输入数据对应的值,输出视觉通道的值
field: 'score', // 数据字段名,表示映射的输入字段。
changeDomain: true // 布尔值,表示是否允许动态更新比例尺的定义域(domain)
};
这即是一个scale类型的样式,其中第一个域scale将数据对应字段的值datum['score']计算为图元的y坐标。
Step2 计算属性 值
BaseMark向外提供了接口getAttribute,供其使用者根据实际的数据计算和获取属性值。
***getAttribute***<U extends keyof T>(key: U, datum: Datum, state: StateValueType = *'normal'*, opt?: IAttributeOpt) { return this.***_computeAttribute***(key, state)(datum, opt); }
这里的_compteAttribute(key, state)返回的是一个属性计算 的函数,key是属性名,state是所处的状态;(datum, opt)作为这个函数的参数,返回计算结果,与我们上面**“存储属性的计算方式”**的描述一致。
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; }
继续深入_computeStateAttribute,会发现在这里构造了属性计算函数,这个函数的输入是(datum, opt), 输出是计算的得到属性值。如果是属性值是常量(与数据无关的,固定在spec上的),则这个构造的函数直接返回style;而真正需要计算的是一些复杂的样式和数据到视觉的映射~~(回收主题)~~。
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; }; }
重点说明一下scale样式,也就是包含数据到视觉映射的部分。继续上面的例子,我们已经构造了一个scale的样式:
style: {
scale,
field: 'score',
changeDomain: true,
}
如果我们需要计算图元的y坐标,首先获取到图元绑定的数据(见第五章 VChart数据处理),然后通过scale映射对象,输入data['score']获取对应的y值。更多关于scale的说明见第七章 VChart Scale。