!!!###!!!title=React VChart Source Code Analysis——VisActor/VChart Contributing Documents!!!###!!!!!!###!!!description=---title: 14.1.2 react-vchart Source Code Explanation key words: VisActor,VChart,VTable,VStrory,VMind,VGrammar,VRender,Visualization,Chart,Data,Table,Graph,Gis,LLM---!!!###!!!

Implementation of BaseChart

In react-vchart, the encapsulation of all charts is achieved through the higher-order component createChart. Next, we will take the implementation of <VChart /> as an example to explain the implementation principle in detail.

1.1 Explanation of

The encapsulation of <VChart /> is as follows:

export const VChart = createChart<VChartProps>('VChart', {
  vchartConstrouctor: VChartCore
});    

This code is relatively simple and includes the following content:

  • Use the createChart factory function to create a component

  • Specify the component name as VChart

  • Inject the VChartCore constructor

1.2 Basic Chart Implementation (BaseChart.tsx)

1.2.1 State Management

const [updateId, setUpdateId] = useState<number>(0);
const chartContext = useRef<ChartContextType>({});
const [view, setView] = useState<IView>(null);
const isUnmount = useRef<boolean>(false);    

Key States:

  • updateId: Used to control the update of subcomponents.

  • chartContext: Used to store chart instances.

  • view: Used to store view instances.

  • isUnmount: Used to control the unmount state of the component.

1.2.2 Spec Parsing System

const parseSpec = (props: Props) => {
    let spec: ISpec;
    // 1. 处理直接传入的 spec
    if (hasSpec && props.spec) {
        spec = props.spec;
        if (isValid(props.data)) {
            spec = {
              ...props.spec,
                data: props.data
            };
        }
    }
    // 2. 处理从子组件收集的 spec
    else {
        spec = {
          ...prevSpec.current,
          ...specFromChildren.current
        };
    }
    // 3. 处理 tooltip
    const tooltipSpec = initCustomTooltip(setTooltipNode, props, spec.tooltip);
    if (tooltipSpec) {
        spec.tooltip = tooltipSpec;
    }
    return spec;
};    

The Spec parsing system includes three levels:

  • Directly passed spec configuration.

  • Aggregation of subcomponent configurations.

  • Handling of special components (such as tooltip).

1.2.3 Subcomponent Parsing System

const parseSpecFromChildren = (props: Props) => {
    const specFromChildren: Omit<ISpec, 'type' | 'data' | 'width' | 'height'> = {};
    toArray(props.children).map((child, index) => {
        const parseSpec = child?.type?.parseSpec;
        if (parseSpec && child.props) {
            // 处理子组件配置...
            const specResult = parseSpec(childProps);
            // 处理单例和数组配置
            if (specResult.isSingle) {
                specFromChildren[specResult.specName] = specResult.spec;
            } else {
                if (!specFromChildren[specResult.specName]) {
                    specFromChildren[specResult.specName] = [];
                }
                specFromChildren[specResult.specName].push(specResult.spec);
            }
        }
    });
    return specFromChildren;
};    

The responsibilities of the system include:

  • Collecting configurations of all sub-components.

  • Distinguishing between singleton and array types of configurations.

  • Generating the final configuration object.

1.2.4 Update Mechanism

useEffect(() => {
    // 1. 首次渲染
    if (!chartContext.current?.chart) {
        createChart(props);
        renderChart();
        return;
    }
    // 2. spec 更新
    if (hasSpec) {
        if (!isEqual(eventsBinded.current.spec, props.spec)) {
            chartContext.current.chart.updateSpecSync(parseSpec(props));
            handleChartRender(true);
        }
        // 3. 数据更新
        else if (eventsBinded.current.data!== props.data) {
            chartContext.current.chart.updateFullDataSync(props.data);
            handleChartRender(true);
        }
        return;
    }
    // 4. 子组件更新
    const newSpec = pickWithout(props, notSpecKeys);
    if (!isEqual(newSpec, prevSpec.current)) {
        // 更新处理...
    }
}, [props]);    

The update mechanism covers the following four scenarios:

  • Initial rendering

  • Spec configuration update

  • Data update

  • Updates caused by subcomponents

1.2.4 Lifecycle Management

useEffect(() => {
    return () => {
        if (chartContext.current?.chart) {
            chartContext.current.chart.release();
            chartContext.current.chart = null;
        }
        eventsBinded.current = null;
        isUnmount.current = true;
    };
}, []);    

When the component is destroyed, ensure that resources can be released, including:

  • Release chart instances

  • Clean up event bindings

  • Update component status

Implementation of BaseComponent

2.1 Core Implementation Mechanism

In the react-vchart framework, the creation of all components relies on the createComponent factory function. The definition of this function is as follows:

const createComponent = <T extends ComponentProps>(
    componentName: string,    // 组件名
    specName: string,        // 规格名称
    supportedEvents?: Record<string, string>,  // 支持的事件
    isSingle?: boolean,      // 是否单例
    registers?: (() => void)[]  // 注册器
): any => {
    // ...组件创建逻辑
};    

Here, the generic T extends ComponentProps is used to constrain the type of component properties passed in. The function receives multiple parameters:

  • componentName: Used to identify the name of the component, which is unique throughout the application, making it easy for developers to recognize and manage components.

  • specName: Represents the specification name corresponding to the component, which is very important for configuration collection and management. Different components are distinguished by different specification names for their respective configurations.

  • supportedEvents: An optional object used to define the events supported by the component. The key-value pair form of the object represents the event type and the corresponding event handling logic. For example, a component may support the click event and define the corresponding handler function.

  • isSingle: A boolean value used to indicate whether the component is in singleton mode. If true, it means that there will only be one instance of the component in the entire application; if false, multiple instances can be created.

  • registers: An array of functions, each used to perform specific registration operations. These registration operations may include registering specific functions or plugins of the component into the framework.

To understand more intuitively, the following shows the encapsulation code using the axis component and legend component as examples:

  • Axis
export const Axis = createComponent<AxisProps>('Axis', 'axes');    

Here, a coordinate axis component named Axis is created, the component name is Axis, and the corresponding specification name is axes. In this way, the framework can accurately identify and handle the relevant configurations and operations of the coordinate axis component.

  • Legend
export const Legend = createComponent<LegendProps>(
    'Legend',
    'legends',
    LEGEND_CUSTOMIZED_EVENTS,
    false,
    [registerDiscreteLegend]
);    

This code creates a Legend component, with the component name being Legend and the specification name being legends. It also specifies the custom events supported by this component LEGEND_CUSTOMIZED_EVENTS, and the component is not in singleton mode (false). Finally, a registrar function registerDiscreteLegend is passed in to perform specific registration operations, which may be to register discrete data-related functions for the legend component.

2.2 Component Communication Mechanism

2.2.1 Context Communication

const Comp: React.FC<T> = (props: T) => {
    const context = useContext(RootChartContext);
    // ...
};    

In a React application, communication between components is an important issue. Here, the useContext hook function is used to achieve communication between components. RootChartContext is a context object that contains information related to charts, such as chart instances, global configurations, etc. By using useContext(RootChartContext), components can access this global information to achieve data sharing and interaction between components. For example, a child component may need to obtain the global configuration information of the chart to adjust its display mode, and it can obtain context information in this way.

2.2.2 Event System

// 事件绑定
if (supportedEvents) {
    bindEventsToChart(
        context.chart,
        props,
        eventsBinded.current,
        supportedEvents
    );
}    

This part of the code implements the event binding functionality of the component. When the component defines supportedEvents (i.e., supported events), the bindEventsToChart function is called to bind events. This function receives four parameters:

  • context.chart: Represents the chart instance, obtained through the context. Events are bound to this chart instance so that the handling logic can be triggered when the chart experiences corresponding events.

  • props: The properties of the component, which may include configuration information related to events, such as event handler functions.

  • eventsBinded.current: Possibly an object or variable that stores the events that have already been bound, used to record the currently bound events to avoid duplicate bindings.

  • supportedEvents: The aforementioned object of events supported by the component, containing the mapping relationship between event types and handler functions. In this way, the component can interact with the chart instance to achieve the association between user operations and component behavior.

2.3 Configuration Collection Mechanism

Each component implements the parseSpec method to parse the configuration corresponding to the component, ultimately assembling it into the complete spec required by vchart:

Comp.parseSpec = (props: T) => {
    return {
        spec: pickWithout(props, notSpecKeys),
        specName,
        isSingle
    };
};    

parseSpec method plays a key role in component configuration management. It receives the component's props as a parameter and returns an object containing three attributes:

  • spec: The component configuration obtained through the pickWithout(props, notSpecKeys) method. The pickWithout function might be a custom function used to filter out the required configuration information from props, excluding unnecessary keys (notSpecKeys). This configuration information will be used as the actual configuration for the component in vchart.

  • specName: The aforementioned component specification name, used to identify the type of component configuration, facilitating differentiation and management in the overall configuration.

  • isSingle: A boolean value indicating whether the component is in singleton mode. This information is also crucial in the process of configuration assembly and management, for example, when handling multiple component configurations, it is necessary to decide how to merge configurations based on the value of isSingle. By implementing the parseSpec method for each component, the framework can collect the configuration information of each component and ultimately assemble it into a complete configuration spec that meets vchart requirements.

2.4 Component Registration Mechanism

if (registers && registers.length) {
    VChart.useRegisters(registers);
}    

This part of the code implements the component registration mechanism. When a component defines registers (i.e., an array of registrars) and the array is not empty, the VChart.useRegisters(registers) method is called. VChart may be a global chart object or a framework core object, and the useRegisters method is used to register functions from the registrar array into the framework. These registrar functions may be used to register specific features, plugins, or integrations with other modules for the component. In this way, the component can register some of its special features or configurations into the framework to function throughout the application.

2.5 Configuration Filtering

const notSpecKeys = supportedEvents 
   ? Object.keys(supportedEvents).concat(ignoreKeys) 
    : ignoreKeys;    

This code implements the configuration filtering function. The notSpecKeys variable is used to store unwanted configuration keys. If the component defines supportedEvents (i.e., supported events), then notSpecKeys is formed by merging all keys of supportedEvents with ignoreKeys; otherwise, notSpecKeys is directly equal to ignoreKeys. ignoreKeys may be a predefined array containing some keys that need to be ignored during the configuration parsing process. In this way, during the configuration collection and parsing process, unwanted configuration information can be excluded, ensuring that the final configuration spec only contains useful information, thereby improving the accuracy and effectiveness of the configuration.

2.6 Update Control

if (props.updateId!== updateId.current) {
    updateId.current = props.updateId;
    // 处理更新逻辑...
}    

This part of the code implements the update control of the component. updateId is an identifier used to control component updates. When the props.updateId received by the component is not equal to the currently stored updateId.current, it indicates that an update has occurred. At this point, updateId.current is updated to props.updateId, and then the subsequent update logic is executed (indicated in the code by the comment // Handle update logic...). This update control mechanism ensures that when the component receives a new update identifier, it can correctly handle update operations, such as re-rendering the component, updating data, or performing specific update tasks, thereby ensuring that the component's state remains consistent with the latest requirements.

Implementation of BaseSeries

The series components of React-VChart are also mainly implemented using higher-order components. The following will provide a more detailed analysis of its core implementation.

3.1 Series Component Creator

export const createSeries = <T extends BaseSeriesProps>(
  componentName: string,   // 组件名称
  markNames: string[],     // 图形标记名称
  type?: string,          // 图表类型
  registers?: (() => void)[]  // 注册函数
) => {
  //...
}    

This factory function plays a core role in the creation process of the entire series of components. It strictly constrains the type of component properties passed in through the generic \u003CT extends BaseSeriesProps\u003E, ensuring type safety.

The parameters received by the function have their own important responsibilities:

  • componentName: Serves as the unique identifier of the component, having uniqueness throughout the application. This makes it more convenient for developers to manage and identify components, just like giving each component a unique "name tag".

  • markNames: An array of series element names used to determine the elements used by the component.

  • type: The chart type, although an optional parameter, clarifies the chart type corresponding to the component.

  • registers: Declares the resources that the series needs to register, used for on-demand loading through tree-shaking to achieve package size optimization.

3.2 Area Component Implementation

export type AreaProps = BaseSeriesProps & Omit<IAreaSeriesSpec, 'type'>;
export const Area = createSeries<AreaProps>(
  'Area',              // 组件名
  ['area'],           // 图形标记
  'area',             // 类型
  [registerAreaSeries] // 注册器
);    

Area 组件的定义首先通过 export type AreaProps = BaseSeriesProps & Omit<IAreaSeriesSpec, 'type'> 来定义其属性类型。它结合了 BaseSeriesPropsIAreaSeriesSpec,并通过 Omit 操作排除了 'type' 属性,这是因为在创建组件时,类型已经通过 createSeries 函数的参数进行了指定。

然后,通过 createSeries 函数创建 Area 组件。传入的参数分别为组件名 'Area'、图形标记 ['area']、图表类型 'area' 以及注册器 [registerAreaSeries]。注册器 registerAreaSeries 用于执行与面积图相关的特定注册操作,可能包括注册面积图的样式、动画效果等。

3.3 核心功能实现

  • 标记 ID 管理
const addMarkId = (spec: any, seriesId: string | number) => {
  markNames.forEach(markName => {
    const defaultMarkId = `${seriesId}-${markName}`;
    if (isNil(spec[markName])) {
      spec[markName] = { id: defaultMarkId };
    } else if (isNil(spec[markName].id)) {
      spec[markName].id = defaultMarkId;
    }
  });
};    

In the process of chart rendering, each graphic mark needs to have a unique identifier, which is the role of mark ID management. The addMarkId function receives spec (configuration object) and seriesId (series ID) as parameters.

The function generates a default markId for each graphic mark by traversing the markNames array. The generation rule is to concatenate seriesId and markName with -, for example, 'series1-area'.

If a property corresponding to a markName does not exist in spec, an object containing the default markId is created; if the markName property exists but id does not, the default markId is set for it. This ensures that each graphic mark has a unique identifier, facilitating subsequent event handling and style setting operations.

  • Event Handling System
const handleEvent = (e: any) => {
  const markIds = markNames.map(markName =>
    `${id}-${markName}`
  );
  if (e?.mark && markIds.includes(e.mark.getUserId())) {
    props[VCHART_TO_REACT_EVENTS[e.event.type]](e);
  }
};    

The event handling system is responsible for processing user interactions with the chart. The handleEvent function receives an event object e.

First, generate all possible markId arrays markIds through the markNames array. Then check whether the mark in the event object e exists and whether the user ID of mark is in the markIds array.

If the conditions are met, it indicates that the event is triggered by the graphic mark of the current component. Then, find the corresponding event handler through props[VCHART_TO_REACT_EVENTS[e.event.type]] and pass the event object e into it to perform the corresponding operation. VCHART_TO_REACT_EVENTS is a mapping table used to map VChart's event types to React component's event handlers.

  • Configuration Parsing
Comp.parseSpec = (compProps: T) => {
  const newSeriesSpec = pickWithout<T>(compProps, notSpecKeys);
  // 添加标记 ID
  addMarkId(newSeriesSpec, compProps.id?? compProps.componentId);
  // 设置类型
  if (!isNil(type)) {
    newSeriesSpec.type = type;
  }
  return {
    spec: newSeriesSpec,
    specName:'series'
  };
};    

The configuration parsing function Comp.parseSpec is responsible for parsing the component's properties into a configuration object that meets the requirements of VChart.

First, the function pickWithout<T>(compProps, notSpecKeys) is used to filter out the required configuration information from compProps, excluding unnecessary keys notSpecKeys. notSpecKeys may contain some properties unrelated to event handling or other aspects, ensuring the purity of the configuration object in this way.

Then, the addMarkId function is called to add a mark ID to the new series configuration newSeriesSpec, ensuring that each graphic mark has a unique identifier.

Next, if the type parameter is not empty, the chart type is set in newSeriesSpec.

Finally, an object containing spec (the parsed configuration object) and specName (the configuration type name, here as 'series') is returned. This object will be passed to VChart for rendering as the final configuration.

Through the detailed analysis above, we have gained a deeper understanding of the implementation principles of the React-VChart series components, including component creation, property definition, and the implementation of core functions. These technical principles provide a solid foundation for developers when using and extending React-VChart.

This document was revised and organized by the following personnel

玄魂