# 动态渲染拓扑图方案探究

# 前言

图片

拓扑图是数据可视化领域一种比较常见的展示类型,目前业界常见的可视化展现的方案有ECharts、HighCharts、D3、AntV等。当前的项目使用的是基于ECharts的静态关系图渲染,为了后续可能扩展成动态的拓扑图渲染,本文探索了ECharts的原理以及G6的原理,也算是对自研一个可视化库的基本实现方法做了一个梳理。

# 方案选择

  • ECharts
    • 关系图
  • AntV
    • G6
      • Graphin

# 源码解析

# ECharts源码

图片

整个ECharts核心对外输出是一个大的ECharts类,所有的类型都是基于其进行new出来的实例,而其核心是基于对ZRender这样一个Canvas的封装

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

# ZRender

图片

图片

ZRender是典型的MVC架构,其中M为Storage,主要对数据进行CRUD管理;V为Painter,对Canvas或SVG的生命周期及视图进行管理;C为Handler,负责事件的交互处理,实现dom事件的模拟封装

class ZRender {
    // 公共属性
    dom: HTMLElement
    id: number
    storage: Storage
    painter: PainterBase
    handler: Handler
    animation: Animation
    // 私有属性
    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(el: 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() {
        
    }
}

# G6源码

图片

G6是AntV专门针对图开源的一个库,其底层通过对边和点的定义,以及对位置的确定,来进行图的绘制,其主要包括五大内容:1、图的元素:点、边、分组等;2、图的算法:DFS、BFS、图检测、最短路径、中心度等;3、图布局:force、circle、grid等;4、图渲染:Canvas及SVG等;5、图交互:框选、点选、拖拽等;而Graphin是基于G6的使用React封装的落地方案

# G6

图片

和ECharts的核心思路是一致的,都是基于MVC的模型,但是G6针对图的特点对元素进行了细化,用御术的话说就是“G6是面粉,ECharts是面条”,果然同一个作者开发的思路都是极其的相似

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();

      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 之前清空 redo 和 undo 栈
      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);
    }
    // 将在 onLayoutEnd 中被调用
    function success() {
      // fitView 与 fitCenter 共存时,fitView 优先,fitCenter 不再执行
      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 {
        // 为提升性能,选择数量少的进行操作
        if (data.nodes && data.edges && data.nodes.length < data.edges.length) {
          const nodesArr = this.getNodes();

          // 遍历节点实例,将所有节点提前。
          nodesArr.forEach((node) => {
            node.toFront();
          });
        } else {
          const edgesArr = this.getEdges();

          // 遍历节点实例,将所有节点提前。
          edgesArr.forEach((edge) => {
            edge.toBack();
          });
        }
      }
    }

    if (this.get('enabledStack')) {
      this.pushStack('render');
    }
  }
}

# Graphin

图片

Graphin是基于G6封装的React组件,可以直接进行使用

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

# 总结

数据可视化通常是基于Canvas进行渲染的,对于简单的图形渲染,我们常常一个实例一个实例去写,缺少系统性的统筹规划的概念,对于需要解决一类问题的可视化方案,可以借鉴ECharts及G6引擎的做法,基于MVC模型,将展示、行为及数据进行分离,对于特定方案细粒度的把控可以参考G6的方案。本质上,大数据可视化展示是一个兼具大数据、视觉传达、前端等多方交叉的领域,对于怎么进行数据粒度的优美展示,可以借鉴data-ink ratio以及利用力导布局的算法(ps:引入库伦斥力及胡克弹力阻尼衰减进行动效展示,同时配合边线权重进行节点聚合),对于这方面感兴趣的同学,可以参考今年SEE Conf的《图解万物——AntV图可视化分析解决方案》,数据可视化领域既专业又交叉,对于深挖此道的同学还是需要下一番功夫的。

# 参考