!!!###!!!title=Primitive Interaction and State Handling——VisActor/VChart Contributing Documents!!!###!!!!!!###!!!description=---title: 6.3 Interaction and State Management of Primitives key words: VisActor,VChart,VTable,VStrory,VMind,VGrammar,VRender,Visualization,Chart,Data,Table,Graph,Gis,LLM---!!!###!!!

Introduction

VChart instances provide methods related to event listening, allowing you to meet business needs and interact with charts by listening to events. For all events supported by VChart, refer to the documentation event api. You can listen to a specific event on a primitive in the following two ways:

  • Use markName for filtering, such as:
// 监听 bar 图元 上的 pointerdown 事件
vchart.on('pointerdown', { markName: 'bar' }, (e: EventParams) => {
  console.log('bar pointerdown', e);
});    

  • Use the "level-type" rule with { level: 'mark', type: 'bar' } for filtering, such as:
// 监听 bar 图元 上的 pointerdown 事件
vchart.on('pointerdown', { level: 'mark', type: 'bar' }, (e: EventParams) => {
  console.log('bar pointerdown', e);
});    

States of Primitives

In VChart, primitives can be in several states, and different states can display different styles. The built-in states are:

  • default default state;

  • hover / hover_reverse When the mouse hovers over a primitive, it enters the hover state, and other primitives enter the hover_reverse state;

  • selected / selected_reverse When the mouse clicks on a primitive, it enters the selected state, and other primitives enter the selected_reverse state;

  • dimension_hover / dimension_hover_reverse Dimension hover state, when the mouse pointer hovers over a certain section of the x axis area, the primitives in the area enter the dimension_hover state, and other primitives enter the dimension_hover_reverse state.

State Definition

The state types are defined in packages/vchart/src/compile/mark/interface.ts for convenient use later:

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;    

Notice that there is also a STATE_CUSTOM state, which is a user-defined state. We will introduce the usage of custom states later.

State Style Storage

In order for the graphic elements to display different styles in different states, the structure for storing different state styles is defined in the graphic element interface IMarkRaw:

export type IMarkStateStyle<T extends ICommonSpec> = Record<StateValueType, Partial<IAttrs<T>>>;

export interface IMarkRaw<T extends ICommonSpec> extends ICompilableMark {
  readonly stateStyle: IMarkStateStyle<T>; // 存储状态样式
  ...    

These styles are defined by the user in spec and stored in stateStyle after parsing.

Interaction and State Switching of Primitives

The states and corresponding styles of the primitives have been defined. So, how can we switch the state of the primitives through event interaction and display different styles? The general process is as follows:

Register Event

The entry point for interactive events is the on method of the Event class,

***on***<Evt extends EventType>(
    eType: Evt,
    query: EventQuery | EventCallback<EventParamsDefinition[Evt]>,
    ***callback***?: EventCallback<EventParamsDefinition[Evt]>
  )    

  • eventType is the type of event, such as pointerdown, dimensionHover, etc.

  • query is the event filter, such as element name, event level, component type, etc.

  • callback is the callback function triggered by the event.

This will call the core function register of EventDispatcher:

  // 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;
  }    

  • Parse the event configuration (query) passed by the user and generate the final event filter (filter).

  • Retrieve the corresponding event Bubble object from the internally maintained Map (such as _viewBubbles) based on the source (chart, window, or canvas) in the filter; if not present, create a new one.

  • Add the event handler (handler) to the Bubble; if there is no listener for this event type in the corresponding scenario, register a callback for the underlying syntax layer through the compiler (this._compiler.addEventListener).

**Bubble** is used to manage the collection of handlers for the same event at different bubbling levels (such as Mark, Model, Chart, VChart). It categorizes and stores event handlers according to the bubbling level and provides methods to add, remove, allow, or prohibit handlers, thereby achieving orderly invocation and management of events at each level.
```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 的增删改方法 }



##### Response Event

When an interaction event is triggered, another core function `dispatch` of `EventDispatcher` will be called:

```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;
  }    

  • Retrieve the corresponding Bubble Map based on the event source (source: view, window, canvas), and then extract the Bubble corresponding to the event type from it.

  • If a Bubble is found, obtain the registered handlers (handlers) according to the bubbling hierarchy (MarkModelChartVChart) and call the _invoke method to execute them.

  • The _invoke method checks for matches based on the event filter (filter), and if it passes, it calls the callback function; if the callback returns a truthy value, it indicates preventing subsequent bubbling processing.

State Switching

Switch the state of the graphic elements in the mounted callback function. By default, vchart mounts handlers for hover, selected, dimensionHover/dimensionClick events. The first two are implemented and proxied by the VGrammar syntax layer, while events related to dimension are implemented in VChart. Taking hover as an example, first define and register the dimensionHover event:

// 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) => {
    ...  
  }
}    

In onMouseMove is a callback function, which is the entry point for subsequent changes to the element state, where _callback is as follows:

  // 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;
    }
  };    

In simple terms, it involves adding or removing elements under corresponding events, and the specific change in element state is managed and implemented through the Interaction class. For example, in addEventElement, a new graphic element is added to the specified state and the element is marked for that state.

  ***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);
  }    

Finally, the element changes the style of the internal graphic elements according to the state through the addState function, which calls the interface of the syntax layer VGrammar.

Custom State and Interaction Example

As mentioned above, we can customize some states of the graphic elements, and VChart provides the updateState interface to update states, which allows us to achieve more requirements based on this. For example, we want to highlight the neighboring points in another style when hovering over a point.

First, define a new state as_neighbor for the points in the spec and specify its style:

point: {
    ...
    state: {
        as_neighbor: {
            scaleX: 2,
            scaleY: 2,
            fill:"red",
            fillOpacity: 0.5
        }
    }
    ...
 }    

After that, register the event, when hover over a point, use updateState to set the state of its neighboring points to 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);
            }
        }
    });
});    

In this way, the state of the neighboring point is set to as_neighbor, and through the above process, the specified style is finally displayed (enlarged to 2 times, 0.5 transparency, and turned red):

This document was revised and organized by the following person

玄魂