乐趣区

关于echarts:动态渲染拓扑图方案探究

前言

拓扑图是数据可视化畛域一种比拟常见的展现类型,目前业界常见的可视化展示的计划有 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 图可视化剖析解决方案》,数据可视化畛域既业余又穿插,对于深挖此道的同学还是须要下一番功夫的。

参考

  • ECharts 关系图官网
  • ECharts 官网源码
  • ECharts 3.0 源码简要剖析 1 - 总体架构
  • ZRender 官网源码
  • ZRender 源码剖析 1:总体构造
  • ZRender 源码剖析 2:Storage(Model 层)
  • ZRender 源码剖析 3:Painter(View 层)- 上
  • ZRender 源码剖析 4:Painter(View 层)- 中
  • ZRender 源码剖析 5:Shape 绘图详解
  • ZRender 源码剖析 6:Shape 对象详解之门路
  • G6 官网
  • G6 官网源码
  • G6 源码浏览 -part1- 运行主流程
  • G6 源码浏览 -Part2-Item 与 Shape
  • G6 源码浏览 -Part3- 绘制 Paint
  • Graphin 官网源码
  • Graphin 官网
退出移动版