preface

Topology diagram is a common display type in the field of data visualization. At present, the common visual display schemes in the industry include ECharts, HighCharts, D3, AntV and so on. The current project uses static diagram rendering based on ECharts. In order to expand into dynamic topology rendering in the future, this paper explores the principles of ECharts and G6, and also summarizes the basic implementation methods of a visual library developed by ourselves.

Scheme selection

  • ECharts
    • The diagram
  • AntV
    • G6
      • Graphin

The source code parsing

ECharts source

The entire ECharts core is exported as a large ECharts class, all types are based on new instances of it, and the core is based on the encapsulation of a Canvas like ZRender

ECharts

class ECharts extends Eventful {
    // 公共属性
    group: string;
    // 私有属性
    private _zr: zrender.ZRenderType;
    private _dom: HTMLElement;
    private _model: GlobalModel;
    private _throttledZrFlush: zrender.ZRenderType extends {flush: infer R} ? R : never;
    private _theme: ThemeOption;
    private _locale: LocaleOption;
    private _chartsViews: ChartView[] = [];
    private _chartsMap: {[viewId: string]: ChartView} = {};
    private _componentsViews: ComponentView[] = [];
    private _componentsMap: {[viewId: string]: ComponentView} = {};
    private _coordSysMgr: CoordinateSystemManager;
    private _api: ExtensionAPI;
    private _scheduler: Scheduler;
    private _messageCenter: MessageCenter;
    private _pendingActions: Payload[] = [];
    private _disposed: boolean;
    private _loadingFX: LoadingEffect;
    private _labelManager: LabelManager;
    private [OPTION_UPDATED_KEY]: boolean | {silent: boolean};
    private [IN_MAIN_PROCESS_KEY]: boolean;
    private [CONNECT_STATUS_KEY]: ConnectStatus;
    private [STATUS_NEEDS_UPDATE_KEY]: boolean;
    // 保护属性
    protected _$eventProcessor: never;

    constructor(
        dom: HTMLElement,
        theme?: string | ThemeOption,
        opts?: {
            locale?: string | LocaleOption,
            renderer?: RendererType,
            devicePixelRatio?: number,
            useDirtyRect?: boolean,
            width?: number,
            height?: number
        }
    ) {
        super(new ECEventProcessor());

        opts = opts || {};

        
        if (typeof theme === 'string') {
            theme = themeStorage[theme] as object;
        }

        this._dom = dom;

        let defaultRenderer = 'canvas';

        const zr = this._zr = zrender.init(dom, {
            renderer: opts.renderer || defaultRenderer,
            devicePixelRatio: opts.devicePixelRatio,
            width: opts.width,
            height: opts.height,
            useDirtyRect: opts.useDirtyRect == null ? defaultUseDirtyRect : opts.useDirtyRect
        });

        this._locale = createLocaleObject(opts.locale || SYSTEM_LANG);

        this._coordSysMgr = new CoordinateSystemManager();

        const api = this._api = createExtensionAPI(this);

        this._scheduler = new Scheduler(this, api, dataProcessorFuncs, visualFuncs);

        this._initEvents();

        zr.animation.on('frame', this._onframe, this);

        bindRenderedEvent(zr, this);

        bindMouseEvent(zr, this);

    }

    private _onframe(): void {}

    getDom(): HTMLElement {
        return this._dom;
    }

    getId(): string {
        return this.id;
    }

    getZr(): zrender.ZRenderType {
        return this._zr;
    }

    setOption<Opt extends ECBasicOption>(option: Opt, notMerge?: boolean | SetOptionOpts, lazyUpdate?: boolean): void {
        if (lazyUpdate) {
            this[OPTION_UPDATED_KEY] = {silent: silent};
            this[IN_MAIN_PROCESS_KEY] = false;
            this.getZr().wakeUp();
        }
        else {
            prepare(this);

            updateMethods.update.call(this);
            this._zr.flush();

            this[OPTION_UPDATED_KEY] = false;
            this[IN_MAIN_PROCESS_KEY] = false;

            flushPendingActions.call(this, silent);
            triggerUpdatedEvent.call(this, silent);
        }
    }

    private getModel(): GlobalModel {
        return this._model;
    }

    getRenderedCanvas(opts?: {
        backgroundColor?: ZRColor
        pixelRatio?: number
    }): HTMLCanvasElement {
        if (!env.canvasSupported) {
            return;
        }
        opts = zrUtil.extend({}, opts || {});
        opts.pixelRatio = opts.pixelRatio || this.getDevicePixelRatio();
        opts.backgroundColor = opts.backgroundColor
            || this._model.get('backgroundColor');
        const zr = this._zr;
        return (zr.painter as CanvasPainter).getRenderedCanvas(opts);
    }


    private _initEvents(): void {
        each(MOUSE_EVENT_NAMES, (eveName) => {
            const handler = (e: ElementEvent) => {
                const ecModel = this.getModel();
                const el = e.target;
                let params: ECEvent;
                const isGlobalOut = eveName === 'globalout';
                if (isGlobalOut) {
                    params = {} as ECEvent;
                }
                else {
                    el && findEventDispatcher(el, (parent) => {
                        const ecData = getECData(parent);
                        if (ecData && ecData.dataIndex != null) {
                            const dataModel = ecData.dataModel || ecModel.getSeriesByIndex(ecData.seriesIndex);
                            params = (
                                dataModel && dataModel.getDataParams(ecData.dataIndex, ecData.dataType) || {}
                            ) as ECEvent;
                            return true;
                        }
                        // If element has custom eventData of components
                        else if (ecData.eventData) {
                            params = zrUtil.extend({}, ecData.eventData) as ECEvent;
                            return true;
                        }
                    }, true);
                }


                if (params) {
                    let componentType = params.componentType;
                    let componentIndex = params.componentIndex;
                    if (componentType === 'markLine'
                        || componentType === 'markPoint'
                        || componentType === 'markArea'
                    ) {
                        componentType = 'series';
                        componentIndex = params.seriesIndex;
                    }
                    const model = componentType && componentIndex != null
                        && ecModel.getComponent(componentType, componentIndex);
                    const view = model && this[
                        model.mainType === 'series' ? '_chartsMap' : '_componentsMap'
                    ][model.__viewId];

                    params.event = e;
                    params.type = eveName;

                    (this._$eventProcessor as ECEventProcessor).eventInfo = {
                        targetEl: el,
                        packedEvent: params,
                        model: model,
                        view: view
                    };

                    this.trigger(eveName, params);
                }
            };
            (handler as any).zrEventfulCallAtLast = true;
            this._zr.on(eveName, handler, this);
        });

        each(eventActionMap, (actionType, eventType) => {
            this._messageCenter.on(eventType, function (event) {
                this.trigger(eventType, event);
            }, this);
        });

        // Extra events
        // TODO register?
        each(
            ['selectchanged'],
            (eventType) => {
                this._messageCenter.on(eventType, function (event) {
                    this.trigger(eventType, event);
                }, this);
            }
        );

        handleLegacySelectEvents(this._messageCenter, this, this._api);
    }

    dispatchAction(
        payload: Payload,
        opt?: boolean | {
            silent?: boolean,
            flush?: boolean | undefined
        }
    ): void {
        const silent = opt.silent;
        doDispatchAction.call(this, payload, silent);

        const flush = opt.flush;
        if (flush) {
            this._zr.flush();
        }
        else if (flush !== false && env.browser.weChat) {
            this._throttledZrFlush();
        }

        flushPendingActions.call(this, silent);

        triggerUpdatedEvent.call(this, silent);
    }
}
Copy the code

ZRender

ZRender is a typical MVC architecture, in which M is Storage and mainly CRUD management of data. V is Painter, which manages the life cycle and view of Canvas or SVG. C is the Handler, responsible for the interactive processing of events and realizing the simulated encapsulation of DOM events

Class ZRender {// public attribute DOM: HTMLElement ID: number storage: storage Painter: PainterBase handler: Handler animation: Animation // Private attributes private _sleepAfterStill = 10; private _stillFrameAccum = 0; private _needsRefresh = true private _needsRefreshHover = true private _darkMode = false; private _backgroundColor: string | GradientObject | PatternObject; constructor(id: number, dom: HTMLElement, opts? : ZRenderInitOpt) { opts = opts || {}; /** * @type {HTMLDomElement} */ this.dom = dom; this.id = id; const storage = new Storage(); let rendererType = opts.renderer || 'canvas'; // TODO WebGL if (useVML) {throw new Error('IE8 support has been dropped since 5.0'); } if (! painterCtors[rendererType]) { // Use the first registered renderer. rendererType = zrUtil.keys(painterCtors)[0]; } if (! painterCtors[rendererType]) { throw new Error(`Renderer '${rendererType}' is not imported. Please import it first.`); } opts.useDirtyRect = opts.useDirtyRect == null ? false : opts.useDirtyRect; const painter = new painterCtors[rendererType](dom, storage, opts, id); this.storage = storage; this.painter = painter; const handerProxy = (! env.node && ! env.worker) ? new HandlerProxy(painter.getViewportRoot(), painter.root) : null; this.handler = new Handler(storage, painter, handerProxy, painter.root); this.animation = new Animation({ stage: { update: () => this._flush(true) } }); this.animation.start(); } /** * add Element */ add(el: Element) {} /** * remove Element */ remove(el: Element) {} refresh() {this._needsrefresh = true; // Active the animation again. this.animation.start(); } private _flush(fromInside? : boolean) { let triggerRendered; const start = new Date().getTime(); if (this._needsRefresh) { triggerRendered = true; this.refreshImmediately(fromInside); } if (this._needsRefreshHover) { triggerRendered = true; this.refreshHoverImmediately(); } const end = new Date().getTime(); if (triggerRendered) { this._stillFrameAccum = 0; this.trigger('rendered', { elapsedTime: end - start }); } else if (this._sleepAfterStill > 0) { this._stillFrameAccum++; // Stop the animiation after still for 10 frames. if (this._stillFrameAccum > this._sleepAfterStill) { this.animation.stop(); } } } on<Ctx>(eventName: string, eventHandler: EventCallback<Ctx, unknown> | EventCallback<Ctx, unknown, ElementEvent>, context? : Ctx): this { this.handler.on(eventName, eventHandler, context); return this; } off(eventName? : string, eventHandler? : EventCallback<unknown, unknown> | EventCallback<unknown, unknown, ElementEvent>) { this.handler.off(eventName, eventHandler); } trigger(eventName: string, event? : unknown) { this.handler.trigger(eventName, event); } clear() { } dispose() { } }Copy the code

G6 source

G6 is AntV dedicated to an open source library for graphs. Its bottom layer is used to draw graphs through the definition of edges and points, as well as the determination of positions. It mainly includes five contents: 1. 2. Graph algorithm: DFS, BFS, graph detection, shortest path, centrality, etc. 3. Diagram layout: Force, Circle, Grid, etc. 4. Graph rendering: Canvas and SVG, etc. 5. Diagram interaction: box selection, click selection, drag and drop, etc. Graphin is a landing solution based on G6 using React encapsulation

G6

The core idea is consistent with ECharts, which is based on MVC model, but G6 has refined the elements according to the characteristics of the figure. In the words of bending, “G6 is flour, ECharts is noodles”. Indeed, the ideas developed by the same author are extremely similar

export default abstract class AbstractGraph extends EventEmitter implements IAbstractGraph { protected animating: boolean; protected cfg: GraphOptions & { [key: string]: any }; protected undoStack: Stack; protected redoStack: Stack; public destroyed: boolean; constructor(cfg: GraphOptions) { super(); this.cfg = deepMix(this.getDefaultCfg(), cfg); this.init(); this.animating = false; this.destroyed = false; if (this.cfg.enabledStack) { this.undoStack = new Stack(this.cfg.maxStep); this.redoStack = new Stack(this.cfg.maxStep); } } protected init() { this.initCanvas(); const viewController = new ViewController(this); const modeController = new ModeController(this); const itemController = new ItemController(this); const stateController = new StateController(this); this.set({ viewController, modeController, itemController, stateController, }); this.initLayoutController(); this.initEventController(); this.initGroups(); this.initPlugins(); } protected abstract initLayoutController(): void; protected abstract initEventController(): void; protected abstract initCanvas(): void; protected abstract initPlugins(): void; protected initGroups(): void { const canvas: ICanvas = this.get('canvas'); const el: HTMLElement = this.get('canvas').get('el'); const { id } = el; const group: IGroup = canvas.addGroup({ id: `${id}-root`, className: Global.rootContainerClassName, }); if (this.get('groupByTypes')) { const edgeGroup: IGroup = group.addGroup({ id: `${id}-edge`, className: Global.edgeContainerClassName, }); const nodeGroup: IGroup = group.addGroup({ id: `${id}-node`, className: Global.nodeContainerClassName, }); const comboGroup: IGroup = group.addGroup({ id: `${id}-combo`, className: Global.comboContainerClassName, }); Combogroup.toback (); combogroup.toback (); this.set({ nodeGroup, edgeGroup, comboGroup }); } const delegateGroup: IGroup = group.addGroup({ id: `${id}-delegate`, className: Global.delegateContainerClassName, }); this.set({ delegateGroup }); this.set('group', group); } public node(nodeFn: (config: NodeConfig) => Partial<NodeConfig>): void { if (typeof nodeFn === 'function') { this.set('nodeMapper', nodeFn); } } public edge(edgeFn: (config: EdgeConfig) => Partial<EdgeConfig>): void { if (typeof edgeFn === 'function') { this.set('edgeMapper', edgeFn); } } public combo(comboFn: (config: ComboConfig) => Partial<ComboConfig>): void { if (typeof comboFn === 'function') { this.set('comboMapper', comboFn); } } public addBehaviors( behaviors: string | ModeOption | ModeType[], modes: string | string[], ): AbstractGraph { const modeController: ModeController = this.get('modeController'); modeController.manipulateBehaviors(behaviors, modes, true); return this; } public removeBehaviors( behaviors: string | ModeOption | ModeType[], modes: string | string[], ): AbstractGraph { const modeController: ModeController = this.get('modeController'); modeController.manipulateBehaviors(behaviors, modes, false); return this; } public paint(): void { this.emit('beforepaint'); this.get('canvas').draw(); this.emit('afterpaint'); } public render(): void { const self = this; this.set('comboSorted', false); const data: GraphData = this.get('data'); If (this.get('enabledStack')) {// render before clearing redo and undo stack this.clearstack (); } if (! data) { throw new Error('data must be defined first'); } const { nodes = [], edges = [], combos = [] } = data; this.clear(); this.emit('beforerender'); each(nodes, (node: NodeConfig) => { self.add('node', node, false, false); }); // process the data to tree structure if (combos && combos.length ! == 0) { const comboTrees = plainCombosToTrees(combos, nodes); this.set('comboTrees', comboTrees); // add combos self.addCombos(combos); } each(edges, (edge: EdgeConfig) => { self.add('edge', edge, false, false); }); const animate = self.get('animate'); if (self.get('fitView') || self.get('fitCenter')) { self.set('animate', false); } // layout const layoutController = self.get('layoutController'); if (layoutController) { layoutController.layout(success); if (this.destroyed) return; } else { if (self.get('fitView')) { self.fitView(); } if (self.get('fitCenter')) { self.fitCenter(); } self.emit('afterrender'); self.set('animate', animate); } function success() {fitView takes priority over fitCenter, FitCenter no longer executes if (self.get('fitView')) {self.fitView(); } else if (self.get('fitCenter')) { self.fitCenter(); } self.autoPaint(); self.emit('afterrender'); if (self.get('fitView') || self.get('fitCenter')) { self.set('animate', animate); } } if (! this.get('groupByTypes')) { if (combos && combos.length ! == 0) { this.sortCombos(); } else {// To improve performance, If (data.nodes && data.edges && data.nodes.length < data.edges. Length) {const nodesArr = this.getNodes(); // Iterate over node instances, bringing all nodes forward. nodesArr.forEach((node) => { node.toFront(); }); } else { const edgesArr = this.getEdges(); // Iterate over node instances, bringing all nodes forward. edgesArr.forEach((edge) => { edge.toBack(); }); } } } if (this.get('enabledStack')) { this.pushStack('render'); }}}Copy the code

Graphin

Graphin is a G6 packaged React component that can be used directly

import React, { ErrorInfo } from 'react';
import G6, { Graph as IGraph, GraphOptions, GraphData, TreeGraphData } from '@antv/g6';

class Graphin extends React.PureComponent<GraphinProps, GraphinState> {
  static registerNode: RegisterFunction = (nodeName, options, extendedNodeName) => {
    G6.registerNode(nodeName, options, extendedNodeName);
  };

  static registerEdge: RegisterFunction = (edgeName, options, extendedEdgeName) => {
    G6.registerEdge(edgeName, options, extendedEdgeName);
  };

  static registerCombo: RegisterFunction = (comboName, options, extendedComboName) => {
    G6.registerCombo(comboName, options, extendedComboName);
  };

  static registerBehavior(behaviorName: string, behavior: any) {
    G6.registerBehavior(behaviorName, behavior);
  }

  static registerFontFamily(iconLoader: IconLoader): { [icon: string]: any } {
    /**  注册 font icon */
    const iconFont = iconLoader();
    const { glyphs, fontFamily } = iconFont;
    const icons = glyphs.map((item) => {
      return {
        name: item.name,
        unicode: String.fromCodePoint(item.unicode_decimal),
      };
    });

    return new Proxy(icons, {
      get: (target, propKey: string) => {
        const matchIcon = target.find((icon) => {
          return icon.name === propKey;
        });
        if (!matchIcon) {
          console.error(`%c fontFamily:${fontFamily},does not found ${propKey} icon`);
          return '';
        }
        return matchIcon?.unicode;
      },
    });
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  static registerLayout(layoutName: string, layout: any) {
    G6.registerLayout(layoutName, layout);
  }

  graphDOM: HTMLDivElement | null = null;
  graph: IGraph;
  layout: LayoutController;
  width: number;
  height: number;
  isTree: boolean;
  data: GraphinTreeData | GraphinData | undefined;
  options: GraphOptions;
  apis: ApisType;
  theme: ThemeData;

  constructor(props: GraphinProps) {
    super(props);

    const {
      data,
      layout,
      width,
      height,

      ...otherOptions
    } = props;

    this.data = data;
    this.isTree =
      Boolean(props.data && props.data.children) || TREE_LAYOUTS.indexOf(String(layout && layout.type)) !== -1;
    this.graph = {} as IGraph;
    this.height = Number(height);
    this.width = Number(width);

    this.theme = {} as ThemeData;
    this.apis = {} as ApisType;

    this.state = {
      isReady: false,
      context: {
        graph: this.graph,
        apis: this.apis,
        theme: this.theme,
      },
    };

    this.options = { ...otherOptions } as GraphOptions;
    this.layout = {} as LayoutController;
  }

  initData = (data: GraphinProps['data']) => {
    if (data.children) {
      this.isTree = true;
    }
    console.time('clone data');
    this.data = cloneDeep(data);
    console.timeEnd('clone data');
  };

  initGraphInstance = () => {
    const {
      theme,
      data,
      layout,
      width,
      height,
      defaultCombo,
      defaultEdge,
      defaultNode,
      nodeStateStyles,
      edgeStateStyles,
      comboStateStyles,
      modes = { default: [] },
      animate,
      ...otherOptions
    } = this.props;
    const { clientWidth, clientHeight } = this.graphDOM as HTMLDivElement;
    this.initData(data);

    this.width = Number(width) || clientWidth || 500;
    this.height = Number(height) || clientHeight || 500;

    const themeResult = getDefaultStyleByTheme(theme);

    const {
      defaultNodeStyle,
      defaultEdgeStyle,
      defaultComboStyle,
      defaultNodeStatusStyle,
      defaultEdgeStatusStyle,
      defaultComboStatusStyle,
    } = themeResult;
    this.theme = themeResult as ThemeData;
    this.isTree = Boolean(data.children) || TREE_LAYOUTS.indexOf(String(layout && layout.type)) !== -1;
    const isGraphinNodeType = defaultNode?.type === undefined || defaultNode?.type === defaultNodeStyle.type;
    const isGraphinEdgeType = defaultEdge?.type === undefined || defaultEdge?.type === defaultEdgeStyle.type;

    this.options = {
      container: this.graphDOM,
      renderer: 'canvas',
      width: this.width,
      height: this.height,
      animate: animate !== false,
      /** 默认样式 */
      defaultNode: isGraphinNodeType ? deepMix({}, defaultNodeStyle, defaultNode) : defaultNode,
      defaultEdge: isGraphinEdgeType ? deepMix({}, defaultEdgeStyle, defaultEdge) : defaultEdge,
      defaultCombo: deepMix({}, defaultComboStyle, defaultCombo),
      /** status 样式 */
      nodeStateStyles: deepMix({}, defaultNodeStatusStyle, nodeStateStyles),
      edgeStateStyles: deepMix({}, defaultEdgeStatusStyle, edgeStateStyles),
      comboStateStyles: deepMix({}, defaultComboStatusStyle, comboStateStyles),

      modes,
      ...otherOptions,
    } as GraphOptions;

    if (this.isTree) {
      this.options.layout = { ...layout };

      this.graph = new G6.TreeGraph(this.options);
    } else {
      this.graph = new G6.Graph(this.options);
    }

    this.graph.data(this.data as GraphData | TreeGraphData);
    /** 初始化布局 */
    if (!this.isTree) {
      this.layout = new LayoutController(this);
      this.layout.start();
    }
    this.graph.get('canvas').set('localRefresh', false);
    this.graph.render();
    this.initStatus();
    this.apis = ApiController(this.graph);
  };

  updateLayout = () => {
    this.layout.changeLayout();
  };

  componentDidMount() {
    console.log('did mount...');

    this.initGraphInstance();
    this.setState({
      isReady: true,
      context: {
        graph: this.graph,
        apis: this.apis,
        theme: this.theme,
      },
    });
  }

  updateOptions = () => {
    const { layout, data, ...options } = this.props;
    return options;
  };

  initStatus = () => {
    if (!this.isTree) {
      const { data } = this.props;
      const { nodes = [], edges = [] } = data as GraphinData;
      nodes.forEach((node) => {
        const { status } = node;
        if (status) {
          Object.keys(status).forEach((k) => {
            this.graph.setItemState(node.id, k, Boolean(status[k]));
          });
        }
      });
      edges.forEach((edge) => {
        const { status } = edge;
        if (status) {
          Object.keys(status).forEach((k) => {
            this.graph.setItemState(edge.id, k, Boolean(status[k]));
          });
        }
      });
    }
  };

  componentDidUpdate(prevProps: GraphinProps) {
    console.time('did-update');
    const isDataChange = this.shouldUpdate(prevProps, 'data');
    const isLayoutChange = this.shouldUpdate(prevProps, 'layout');
    const isOptionsChange = this.shouldUpdate(prevProps, 'options');
    const isThemeChange = this.shouldUpdate(prevProps, 'theme');
    console.timeEnd('did-update');
    const { data } = this.props;
    const isGraphTypeChange = prevProps.data.children !== data.children;

    /** 图类型变化 */
    if (isGraphTypeChange) {
      this.initGraphInstance();
      console.log('%c isGraphTypeChange', 'color:grey');
    }
    /** 配置变化 */
    if (isOptionsChange) {
      this.updateOptions();
      console.log('isOptionsChange');
    }
    /** 数据变化 */
    if (isDataChange) {
      this.initData(data);
      this.layout.changeLayout();
      this.graph.data(this.data as GraphData | TreeGraphData);
      this.graph.changeData(this.data as GraphData | TreeGraphData);
      this.initStatus();
      this.apis = ApiController(this.graph);
      console.log('%c isDataChange', 'color:grey');
      this.setState((preState) => {
        return {
          ...preState,
          context: {
            graph: this.graph,
            apis: this.apis,
            theme: this.theme,
          },
        };
      });
      return;
    }
    /** 布局变化 */
    if (isLayoutChange) {
      /**
       * TODO
       * 1. preset 前置布局判断问题
       * 2. enablework 问题
       * 3. G6 LayoutController 里的逻辑
       */
      this.layout.changeLayout();
      this.layout.refreshPosition();

      /** 走G6的layoutController */
      // this.graph.updateLayout();
      console.log('%c isLayoutChange', 'color:grey');
    }
  }

  /**
   * 组件移除的时候
   */
  componentWillUnmount() {
    this.clear();
  }

  /**
   * 组件崩溃的时候
   * @param error
   * @param info
   */
  componentDidCatch(error: Error, info: ErrorInfo) {
    console.error('Catch component error: ', error, info);
  }

  clear = () => {
    if (this.layout && this.layout.destroyed) {
      this.layout.destroy(); // tree graph
    }
    this.layout = {} as LayoutController;
    this.graph!.clear();
    this.data = { nodes: [], edges: [], combos: [] };
    this.graph!.destroy();
  };

  shouldUpdate(prevProps: GraphinProps, key: string) {
    /* eslint-disable react/destructuring-assignment */
    const prevVal = prevProps[key];
    const currentVal = this.props[key] as DiffValue;
    const isEqual = deepEqual(prevVal, currentVal);
    return !isEqual;
  }

  render() {
    const { isReady } = this.state;
    const { modes, style } = this.props;
    return (
      <GraphinContext.Provider value={this.state.context}>
        <div id="graphin-container">
          <div
            data-testid="custom-element"
            className="graphin-core"
            ref={(node) => {
              this.graphDOM = node;
            }}
            style={{ background: this.theme?.background, ...style }}
          />
          <div className="graphin-components">
            {isReady && (
              <>
                {
                  /** modes 不存在的时候,才启动默认的behaviros,否则会覆盖用户自己传入的 */
                  !modes && (
                    <React.Fragment>
                      {/* 拖拽画布 */}
                      <DragCanvas />
                      {/* 缩放画布 */}
                      <ZoomCanvas />
                      {/* 拖拽节点 */}
                      <DragNode />
                      {/* 点击节点 */}
                      <DragCombo />
                      {/* 点击节点 */}
                      <ClickSelect />
                      {/* 圈选节点 */}
                      <BrushSelect />
                    </React.Fragment>
                  )
                }

                {/** resize 画布 */}
                <ResizeCanvas graphDOM={this.graphDOM as HTMLDivElement} />
                <Hoverable bindType="node" />
                {/* <Hoverable bindType="edge" /> */}
                {this.props.children}
              </>
            )}
          </div>
        </div>
      </GraphinContext.Provider>
    );
  }
}
Copy the code

conclusion

Data visualization is usually based on Canvas rendering, for simple graphics rendering, we often go to write an instance of an instance, the lack of systematic, overall planning, the concept of visualization scheme of for a class of problems need to be solved, we can draw lessons from practice of ECharts and G6 engine, based on the MVC model, will show to separation, behavior and data, For fine-grained control of specific schemes, please refer to G6 schemes. In essence, big data visual display is a multi-cross field with big data, visual communication and front-end. For how to gracefully display data granularity, data-ink ratio and force-guided layout algorithm can be used for reference (PS: Introducing Coulomb repulsive force and Hooke elastic damping attenuation into the action effect display, and combining with the edge weights for node aggregation). For students interested in this aspect, please refer to the “Diagram everything — AntV Diagram Visualization Analysis Solution” in this year’s SEE Conf. The field of data visualization is both professional and interdisciplinary. For those of you who dig this deep, it takes a lot of work.

reference

  • ECharts diagram official website
  • ECharts official source code
  • ECharts 3.0 source code brief analysis 1- overall architecture
  • ZRender official source code
  • ZRender source code analysis 1: the overall structure
  • ZRender source code analysis 2: Storage(Model layer)
  • ZRender source code analysis 3: Painter(View layer)- on
  • ZRender source code analysis 4: Painter(View layer)- in
  • ZRender source code analysis 5: Shape drawing details
  • ZRender source code analysis 6: Shape object detailed path
  • G6 website
  • G6 official source code
  • G6 source code reading -part1- running the main process
  • G6 下 载 -part2-item and Shape
  • G6 source code read -Part3- draw Paint
  • Graphin official source code
  • Graphin website