前言

拓扑图是数据可视化畛域一种比拟常见的展现类型,目前业界常见的可视化展示的计划有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官网