!!!###!!!title=图元交互与状态处理——VisActor/VChart 社区贡献者文档!!!###!!!!!!###!!!description=---title: 6.3 图元的交互和状态处理 key words: VisActor,VChart,VTable,VStrory,VMind,VGrammar,VRender,Visualization,Chart,Data,Table,Graph,Gis,LLM--- !!!###!!!

简介

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 是事件类型,例如 pointerdowndimensionHover等。

  • 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(chartwindow canvas)从内部维护的 Map(如 _viewBubbles)里获取对应的事件 Bubble对象;如果没有则新建一个。

  • 将事件处理器(handler)添加到 Bubble 中;若该事件类型在对应场景下尚未有监听器,则通过编译器 (this._compiler.addEventListener) 为底层语法层注册回调。

**Bubble** 用来管理同一事件在不同冒泡层级(如 Mark、Model、Chart、VChart)上的处理器集合。它会将事件处理器按照冒泡层级分类存储,并提供添加、移除、允许或禁止处理器的方法,从而实现事件在各层级的有序调用与管理。
```Typescript export type BubbleNode = { handler: EventHandler; level: EventBubbleLevel; };

export class Bubble { private _map: Map<EventCallback, BubbleNode> = new Map(); private _levelNodes: Map<EventBubbleLevel, BubbleNode[]> = new Map();

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,则依据冒泡层级(MarkModelChartVChart)依次获取已注册的处理器(handlers),调用 _invoke方法执行。

  • _invoke方法会根据事件过滤器(filter)检查是否匹配,若通过则调用回调函数;如果回调返回真值,表示阻止后续的冒泡处理。

状态切换

在挂载的回调函数中进行图元状态的切换,默认情况下,vchart挂载了hoverselecteddimensionHover/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透明度,同时变成红色):

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

玄魂