简介
VChart 实例上提供了事件监听相关的方法,可以通过监听事件来满足业务需求,实现与图表的交互。VChart 支持的所有事件参考文档事件api。其中,可以通过如下两种方式监听图元上的某个事件:
- 使用
markName进行过滤,如:
// 监听 bar 图元 上的 pointerdown 事件 vchart.on('pointerdown', { markName: 'bar' }, (e: EventParams) => { console.log('bar pointerdown', e); });
- 使用
{ level: 'mark', type: 'bar' }的"层级-类型"规则进行过滤,如:
// 监听 bar 图元 上的 pointerdown 事件 vchart.on('pointerdown', { level: 'mark', type: 'bar' }, (e: EventParams) => { console.log('bar pointerdown', e); });
图元的状态
在VChart中,图元可以处于一些状态,不同的状态可以展示不同的样式。内置的状态有:
-
default默认状态; -
hover/hover_reverse鼠标悬浮在图元上时,进入hover状态,其他图元进入hover_reverse状态; -
selected/selected_reverse鼠标点击图元时,进入selected选中状态,其他图元进入selected_reverse状态; -
dimension_hover/dimension_hover_reverse维度悬浮状态,鼠标指针悬浮在某一段x轴区域内时,区域内图元进入dimension_hover状态,其他图元进入dimension_hover_reverse状态。
状态定义
在packages/vchart/src/compile/mark/interface.ts中定义了状态类型,方便后续直接使用:
export enum STATE_VALUE_ENUM { STATE_NORMAL = *'normal'*, STATE_HOVER = *'hover'*, STATE_HOVER_REVERSE = *'hover_reverse'*, STATE_DIMENSION_HOVER = *'dimension_hover'*, STATE_DIMENSION_HOVER_REVERSE = *'dimension_hover_reverse'*, STATE_SELECTED = *'selected'*, STATE_SELECTED_REVERSE = *'selected_reverse'*, } export enum STATE_VALUE_ENUM_REVERSE { STATE_HOVER_REVERSE = *'hover_reverse'*, STATE_DIMENSION_HOVER_REVERSE = *'dimension_hover_reverse'*, STATE_SELECTED_REVERSE = *'selected_reverse'* } export type STATE_NORMAL = typeof STATE_VALUE_ENUM.STATE_NORMAL; export type STATE_HOVER = typeof STATE_VALUE_ENUM.STATE_HOVER; export type STATE_HOVER_REVERSE = typeof STATE_VALUE_ENUM.STATE_HOVER_REVERSE; export type STATE_CUSTOM = string; export type StateValueNot = STATE_HOVER_REVERSE | STATE_CUSTOM; export type StateValue = STATE_NORMAL | STATE_HOVER | STATE_CUSTOM; export type StateValueType = StateValue | StateValueNot;
注意到,其中还有一个STATE_CUSTOM状态,即用户自定义状态,我们后续介绍自定义状态的使用方法。
状态样式存储
为了让图元在不同状态下显示不同的样式,在图元接口IMarkRaw中定义了存储不同状态样式的结构:
export type IMarkStateStyle<T extends ICommonSpec> = Record<StateValueType, Partial<IAttrs<T>>>;
export interface IMarkRaw<T extends ICommonSpec> extends ICompilableMark {
readonly stateStyle: IMarkStateStyle<T>; // 存储状态样式
...
这些样式由用户在spec中定义,解析后存储到stateStyle当中。
图元的交互与状态切换
已经定义了图元的状态和相应的样式,那么,如何通过事件交互使得图元切换状态,并展示出不同的样式?大致的流程如下:

注册事件
交互事件的入口是Event类的on方法,
***on***<Evt extends EventType>(
eType: Evt,
query: EventQuery | EventCallback<EventParamsDefinition[Evt]>,
***callback***?: EventCallback<EventParamsDefinition[Evt]>
)
-
eventType是事件类型,例如pointerdown、dimensionHover等。 -
query是事件筛选,例如图元名称、事件层级、组件类型等。 -
callback是事件触发时的回调函数。
其中会调用EventDispatcher的核心函数register:
// vchart/src/event/event-dispatcher.ts
***register***<Evt extends EventType>(eType: Evt, handler: EventHandler<EventParamsDefinition[Evt]>): this {
// 解析 query 配置并生成最终 handler 内容
this.***_parseQuery***(handler);
// 获取相应的bubble对象
const bubbles = this.***getEventBubble***(handler.filter?.source || Event_Source_Type.chart);
const listeners = this.***getEventListeners***(handler.filter?.source || Event_Source_Type.chart);
if (!bubbles.***get***(eType)) {
bubbles.***set***(eType, new ***Bubble***());
}
// 挂载事件监听
const bubble = bubbles.***get***(eType) as Bubble;
bubble.***addHandler***(handler, handler.filter?.level as EventBubbleLevel);
if (this.***_isValidEvent***(eType) && !listeners.***has***(eType)) {
const ***callback*** = this.***_onDelegate***.***bind***(this);
this._compiler.***addEventListener***(handler.filter?.source as EventSourceType, eType, ***callback***);
listeners.***set***(eType, ***callback***);
} else if (this.***_isInteractionEvent***(eType) && !listeners.***has***(eType)) {
const ***callback*** = this.***_onDelegateInteractionEvent***.***bind***(this);
this._compiler.***addEventListener***(handler.filter?.source as EventSourceType, eType, ***callback***);
listeners.***set***(eType, ***callback***);
}
return this;
}
-
解析用户传入的事件配置(
query)并生成最终的事件过滤器(filter)。 -
根据过滤器中的 source(
chart、window或canvas)从内部维护的 Map(如_viewBubbles)里获取对应的事件Bubble对象;如果没有则新建一个。 -
将事件处理器(
handler)添加到 Bubble 中;若该事件类型在对应场景下尚未有监听器,则通 过编译器 (this._compiler.addEventListener) 为底层语法层注册回调。
export class Bubble {
private _map: Map<EventCallback
constructor() { this._levelNodes.set(Event_Bubble_Level.vchart, []); this._levelNodes.set(Event_Bubble_Level.chart, []); this._levelNodes.set(Event_Bubble_Level.model, []); this._levelNodes.set(Event_Bubble_Level.mark, []); } ...... // 管理 Map 的增删改方法 }
##### 响应事件
当触发交互事件后,会调用`EventDispatcher`的另一个核心函数`dispatch`:
```Typescript
// vchart/src/event/event-dispatcher.ts
***dispatch***<Evt extends EventType>(eType: Evt, params: EventParamsDefinition[Evt], level?: EventBubbleLevel): this {
// 默认事件类别为 view
const bubble = this.***getEventBubble***((params as BaseEventParams).source || Event_Source_Type.chart).***get***(
eType
) as Bubble;
// 没有任何监听事件时,bubble 不存在
if (!bubble) {
return this;
}
// 事件冒泡逻辑:Mark -> Model -> Chart -> VChart
let stopBubble: boolean = false;
if (level) {
// 如果指定了 level,则直接处理,不进行冒泡
const handlers = bubble.***getHandlers***(level);
stopBubble = this.***_invoke***(handlers, eType, params);
} else {
const levels = [
Event_Bubble_Level.mark,
Event_Bubble_Level.model,
Event_Bubble_Level.chart,
Event_Bubble_Level.vchart
];
let i = 0;
// Mark 级别的事件只包含对语法层代理的基础事件
while (!stopBubble && i < levels.length) {
stopBubble = this.***_invoke***(bubble.***getHandlers***(levels[i]), eType, params);
i++;
}
}
return this;
}
-
根据事件来源(source:view,window,canvas)获取对应的
Bubble Map,再从其中取出与事件类型对应的Bubble。 -
若找到
Bubble,则依据冒泡层级(Mark→Model→Chart→VChart)依次获取已注册的处理器(handlers),调用_invoke方法执行。 -
_invoke方法会根据事件过滤器(filter)检查是否匹配,若通过则调用回调函数;如果回调返回真值,表示阻止后续的冒泡处理。
状态切换
在挂载的回调函数中进行图元状态的切换,默认情况下,vchart挂载了hover,selected,dimensionHover/dimensionClick事件的处理函数,前两者由VGrammar语法层实现和代理,dimension有关的事件在VChart中实现。以hover为例,首先定义并注册dimensionHover事件:
// packages/vchart/src/event/events/dimension/dimension-hover.ts
export class DimensionHoverEvent extends DimensionEvent {
private _cacheDimensionInfo: IDimensionInfo[] | null = null;
***register***<Evt extends EventType>(eType: Evt, handler: EventHandler<EventParamsDefinition[Evt]>) {
this.***_callback*** = handler.***callback***;
this._eventDispatcher.***register***<*'pointermove'*>(*'pointermove'*, {
query: { ...handler.query, source: Event_Source_Type.chart },
***callback***: this.***onMouseMove***
});
...
}
private ***onMouseMove*** = (params: BaseEventParams) => {
if (!params) {
return;
}
const x = (params.event as any).viewX;
const y = (params.event as any).viewY;
const targetDimensionInfo = this.***getTargetDimensionInfo***(x, y);
if (targetDimensionInfo === null && this._cacheDimensionInfo !== null) {
// 鼠标移出某维度
this.***_callback***.***call***(null, {
...params,
action: *'leave'*,
dimensionInfo: this._cacheDimensionInfo.***slice***()
});
this._cacheDimensionInfo = targetDimensionInfo;
} else if (
targetDimensionInfo !== null &&
(this._cacheDimensionInfo === null ||
targetDimensionInfo.length !== this._cacheDimensionInfo.length ||
targetDimensionInfo.***some***((info, i) => !***isSameDimensionInfo***(info, this._cacheDimensionInfo![i])))
) {
// 鼠标移入某维度
this.***_callback***.***call***(null, {
...params,
action: *'enter'*,
dimensionInfo: targetDimensionInfo.***slice***()
});
this._cacheDimensionInfo = targetDimensionInfo;
} else if (targetDimensionInfo !== null) {
// 鼠标在某维度上滑动
this.***_callback***.***call***(null, {
...params,
action: *'move'*,
dimensionInfo: targetDimensionInfo.***slice***()
});
}
};
private ***onMouseOut*** = (params: BaseEventParams) => {
...
}
}
在onMouseMove是一个回调函数,是后续改变图元状态的入口,其中的_callback如下:
// packages/vchart/src/interaction/dimension-trigger.ts private ***onHover*** = (params: DimensionEventParams) => { switch (params.action) { case *'enter'*: // 清理之前的hover元素 const lastHover = this.interaction.***getEventElement***(STATE_VALUE_ENUM.STATE_DIMENSION_HOVER); lastHover.***forEach***(e => this.interaction.***addEventElement***(STATE_VALUE_ENUM.STATE_DIMENSION_HOVER_REVERSE, e)); this.interaction.***clearEventElement***(STATE_VALUE_ENUM.STATE_DIMENSION_HOVER, false); // 添加新的hover元素 const elements = this.***getEventElement***(params); elements.***forEach***(el => this.interaction.***addEventElement***(STATE_VALUE_ENUM.STATE_DIMENSION_HOVER, el)); this.interaction.***reverseEventElement***(STATE_VALUE_ENUM.STATE_DIMENSION_HOVER); break; case *'leave'*: // 清空所有元素 this.interaction.***clearEventElement***(STATE_VALUE_ENUM.STATE_DIMENSION_HOVER, true); params = null; break; case *'click'*: case *'move'*: default: break; } };
简单来说就是增删相应事件下的元素,而具体元素状态的改变是通过Interaction类来管理和实现的。例如在addEventElement中,添加了新的图元到指定状态,并将元素标记为该状态。
***addEventElement***(stateValue: StateValue, element: IElement) { if (this._disableTriggerEvent) { return; } if (!element.***getStates***().***includes***(stateValue)) { element.***addState***(stateValue); // 改变元素内部图元样式 } const list = this._stateElements.***get***(stateValue) ?? []; list.***push***(element); this._stateElements.***set***(stateValue, list); }
最终,元素通过addState函数,根据状态改变内部图元的样式,这一部分调用了语法层VGrammar的接口。
自定义状态和交互示例
上面提到,我们可以自定义一些图元的状态,并且VChart提供了updateState更新状态接口,我们可以基于此实现更多的需求。例如,我们想要在hover一个点时,同时以另一种样式高亮它的邻居点。
首先,在spec中的点系列中定义一种新的点的状态as_neighbor,并指定它的样式:
point: { ... state: { as_neighbor: { scaleX: 2, scaleY: 2, fill:"red", fillOpacity: 0.5 } } ... }
之后,注册事件,当hover某个点时,使用updateState来设置其邻居点的状态为as_neighbor:
vchart.***on***(*'pointerover'*, { id: *'point-series'* }, e => { // 找到邻居点 const selectedNeighbors: number[] = findNeighbors(); // 更新邻居点的状态 使用filter vchart.***updateState***({ as_neighbor: { ***filter***: datum => { return selectedNeighbors.***includes***(datum.id); } } }); });
这样,邻居点的状态被设置成as_neighbor,通过上述流程最终展现出指定的样式(放大到2倍,0.5透明度,同时变成红色):
