Introduction
When we have a large amount of data to display on a chart, we usually need to define a specific type of series (see section 3.2: series) and create marks within the series to represent the data. However, if we only need to display a little extra information, such as an image, a title, or a path, and do not want to create a separate series for it, we can use custom marks (customMark). Custom marks allow users to add custom annotations to the chart, such as adding some text, images, line segments, etc.
Types and Examples
Currently, the supported types of custom marks are as follows:
-
symbol: Point graphic -
rule: Line segment -
text: Text -
rect: Rectangle -
path: Path -
arc: Sector -
polygon: Polygon -
group: Group, which can contain other marks
In the following example, in addition to the two data points in the chart (which can be considered a scatter series), three custom marks are added above, namely text, symbol, and rule.
xml
const spec = {
type: 'scatter',
xField: 'x',
yField: 'y',
data: [
{
name: 'data',
values: [{y: 10,x: 'point1'},{y: 31,x: 'point2'}]
}
],
customMark: [
{ // Custom text mark
type: 'text',
style: {
fontSize: 20,
fontWeight: 600,
text: "Not a data point:",
x: 300,
y: 25,
fill: 'grey',
}
},
{ // Custom symbol mark
type: 'symbol',
style:{
x: 320,
y: 18,
fill: 'grey',
size: 30
}
},
{ // Custom rule mark
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);
});
}
});
}
// Set the parent mark of the group 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;
}
}
// Entry point for recursive calls, recursively create each mark
this.***_createExtensionMark***(this._spec, parentMark, *`${PREFIX}_series_${this.id}_extensionMark`*, 0, {
depend,
hasAnimation
});
}
图元的样式和数据绑定
使用 _createExtensionMark 方法为每个图元设置样式、动画配置和数据绑定。
支持通过 dataId或 dataIndex绑定数据视图(DataView)。
private ***_createExtensionMark***(
spec: ICustomMarkSpec<Exclude<EnableMarkType, *'group'*> | ICustomMarkGroupSpec,
parentMark: null | IGroupMark,
namePrefix: string,
index: number = 0,
options: { hasAnimation?: boolean; depend?: IMark[] }
) {
// Create the mark
const mark = this.***_createMark***(
{
type: spec.type,
name: ***isValid***(spec.name) ? *`${spec.name}`* : *`${namePrefix}_${index}`*
},
{
// Avoid double 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);
}
// Handle animation
if (options.hasAnimation && spec.animation) {
const config = ***animationConfig***({}, ***userAnimationConfig***(spec.type, spec as any, this._markAttributeContext));
mark.***setAnimationConfig***(config);
}
// Build the hierarchical structure of group marks
if (options.depend && options.depend.length) {
mark.***setDepend***(...options.depend);
}
if (***isNil***(parentMark)) {
this._marks.***addMark***(mark);
} else if (parentMark) {
parentMark.***addMark***(mark);
}
// Set style
this.***initMarkStyleWithSpec***(mark, spec);
// Recursively handle group marks
if (spec.type === *'group'*) {
namePrefix = *`${namePrefix}_${index}`*;
spec.children?.***forEach***((s, i) => {
this.***_createExtensionMark***(s as any, mark, namePrefix, i, options);
});
}
// Handle data binding
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:
负责单个图元的样式、状态和属性计算。
提供方法如 setStyle、getStyle、setAttribute 等,用于操作单个图元的样式和属性。
直接处理图元的渐变色、边框、角度转换等细节。
CustomMark:
负责管理多个图元的创建、样式设置和数据绑定。
提供方法如 initMarks和 _createExtensionMark,用于根据配置动态创建图元。
处理图元的上下文信息(如全局映射、布局边界)和与其他组件的依赖关系。
3. 数据绑定
BaseMark:
通过 setAttribute和 _computeAttribute方法,直接绑定数据到单个图元的属性上。
支持通过映射(scale)和字段(field)动态计算属性值。
CustomMark:
通过 _createExtensionMark方法,为每个图元绑定数据视图(DataView)。
支持通过 dataId或 dataIndex指定数据源。
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;
* // The name corresponding to the mark, mainly used for event filtering such as: { markName: 'yourName' }*
name?: string;
* // Associated data index*
dataIndex?: number;
* // dataKey is used to bind the relationship between data and Mark. If the data is consistent with the series data, it can be left unconfigured, and the configuration in the series will be read by default*
dataKey?: string | ((datum: any) => string);
* // Associated data id*
dataId?: StringOrNumber;
* // Specify component type*
componentType?: string;
* // Whether to enable animation*
animation?: boolean;
* // Specify 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(标记)。它继承了多个接口,包括 IModelSpec、IMarkSpec和 IAnimationSpec,并且支持所有的 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: 'Monday', type: 'Breakfast', y: 15 },
{ x: 'Monday', type: 'Lunch', y: 25 },
{ x: 'Tuesday', type: 'Breakfast', y: 12 },
{ x: 'Tuesday', type: 'Lunch', y: 30 },
{ x: 'Wednesday', type: 'Breakfast', y: 15 },
{ x: 'Wednesday', type: 'Lunch', y: 24 }
]
},
{
id: 'id1',
values: [
{ x: 'Monday', type: 'Drink', y: 22 },
{ x: 'Tuesday', type: 'Drink', y: 43 },
{ x: 'Wednesday', type: 'Drink', 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();