乐趣区

关于前端:AntV-G6新版源码浅析

前言

AntV 是蚂蚁金服全新一代数据可视化解决方案,其中 G6 次要用于解决图可视畛域相干的前端可视化问题,其是一个简略、易用、齐备的图可视化引擎。本文旨在通过简要剖析 G6 5.x 版本源码来对图可视畛域的一些底层引擎进行一个大抵理解,同时也为 G6 引擎的社区共建共享提供一些力量,能够更好的提供插件化性能的编写。

架构

新版 G6 整体是基于“插件化”的架构进行设计的,对外整体裸露 Graph 类及 StdLib 规范库,将主题、数据处理、布局、视图、类型、交互等均作为插件来进行解决,提供更高层次的定制化需要,晋升更好的开源能力。

目录

整体采纳 monorepo 进行源码的仓库治理

  • packages

    • g6

      • docs
      • src
      • constant

        • index.ts
        • shape.ts
      • item

        • combo.ts
        • edge.ts
        • item.ts
        • node.ts
      • runtime

        • controller

          • data.ts
          • extensions.ts
          • index.ts
          • interaction.ts
          • item.ts
          • layout.ts
          • plugin.ts
          • theme.ts
          • viewport.ts
        • graph.ts
        • hooks.ts
      • stdlib

        • behavior

          • activate-relations.ts
          • brush-select.ts
          • click-select.ts
          • drag-canvas.ts
          • drag-node.ts
          • hover-activate.ts
          • lasso-select.ts
          • orbit-canvas-3d.ts
          • rotate-canvas-3d.ts
          • track-canvas-3d.ts
          • zoom-canvas-3d.ts
          • zoom-canvas.ts
        • data

          • comboFromNode.ts
        • item

          • edge

            • base.ts
            • index.ts
            • line.ts
          • node

            • base.ts
            • base3d.ts
            • circle.ts
            • index.ts
            • sphere.ts
        • plugin

          • grid

            • index.ts
          • legend

            • index.ts
          • minimap

            • index.ts
        • selector

          • lasso.ts
          • rect.ts
        • theme

          • dark.ts
          • light.ts
        • themeSolver

          • base.ts
          • spec.ts
          • subject.ts
        • index.ts
      • types

        • animate.ts
        • behavior.ts
        • combo.ts
        • common.ts
        • data.ts
        • edge.ts
        • event.ts
        • graph.ts
        • hook.ts
        • index.ts
        • item.ts
        • layout.ts
        • node.ts
        • plugin.ts
        • render.ts
        • spec.ts
        • stdlib.ts
        • theme.ts
        • view.ts
      • util

        • animate.ts
        • array.ts
        • canvas.ts
        • event.ts
        • extend.ts
        • extension.ts
        • index.ts
        • item.ts
        • mapper.ts
        • math.ts
        • point.ts
        • shape.ts
        • shape3d.ts
        • text.ts
        • type.ts
        • zoom.ts
      • index.ts

        • tests

源码

从架构档次能够看出,整体对外裸露的就是 Graph 的类以及 stdLib 的规范库,因此在剖析源码调用过程中,咱们抓住 Graph 进行逐渐的往外拓展,从而把握整体的一个设计链路,防止陷入部分无奈抽离。

Graph

对外裸露的 Graph 类是整个 G6 图的外围类

// https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts
export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
  extends EventEmitter
  implements IGraph<B, T>
{
  public hooks: Hooks;
  // for nodes and edges, which will be separate into groups
  public canvas: Canvas;
  // the container dom for the graph canvas
  public container: HTMLElement;
  // the tag to indicate whether the graph instance is destroyed
  public destroyed: boolean;
  // the renderer type of current graph
  public rendererType: RendererName;
  // for transient shapes for interactions, e.g. transient node and related edges while draging, delegates
  public transientCanvas: Canvas;
  // for background shapes, e.g. grid, pipe indices
  public backgroundCanvas: Canvas;
  // the tag indicates all the three canvases are all ready
  private canvasReady: boolean;
  private specification: Specification<B, T>;
  private dataController: DataController;
  private interactionController: InteractionController;
  private layoutController: LayoutController;
  private viewportController: ViewportController;
  private itemController: ItemController;
  private extensionController: ExtensionController;
  private themeController: ThemeController;
  private pluginController: PluginController;

  private defaultSpecification = {
    theme: {
      type: 'spec',
      base: 'light',
    },
  };

  constructor(spec: Specification<B, T>) {super();
    // TODO: analyse cfg

    this.specification = Object.assign({}, this.defaultSpecification, spec);
    this.initHooks();
    this.initCanvas();
    this.initControllers();

    this.hooks.init.emit({
      canvases: {
        background: this.backgroundCanvas,
        main: this.canvas,
        transient: this.transientCanvas,
      },
    });

    const {data} = spec;
    if (data) {
      // TODO: handle multiple type data configs
      this.read(data as GraphData);
    }
  }

  // 初始化控制器,用于各种类型插件的依赖注入
  private initControllers() {this.dataController = new DataController(this);
    this.interactionController = new InteractionController(this);
    this.layoutController = new LayoutController(this);
    this.themeController = new ThemeController(this);
    this.itemController = new ItemController(this);
    this.viewportController = new ViewportController(this);
    this.extensionController = new ExtensionController(this);
    this.pluginController = new PluginController(this);
  }

  // 初始化画布
  private initCanvas() {const { renderer, container, width, height} = this.specification;
    let pixelRatio;
    if (renderer && !isString(renderer)) {
      // @ts-ignore
      this.rendererType = renderer.type || 'canvas';
      // @ts-ignore
      pixelRatio = renderer.pixelRatio;
    } else {
      // @ts-ignore
      this.rendererType = renderer || 'canvas';
    }
    const containerDOM = isString(container)
      ? document.getElementById(container as string)
      : container;
    if (!containerDOM) {
      console.error(`Create graph failed. The container for graph ${containerDOM} is not exist.`,
      );
      this.destroy();
      return;
    }
    this.container = containerDOM;
    const size = [width, height];
    if (size[0] === undefined) {size[0] = containerDOM.scrollWidth;
    }
    if (size[1] === undefined) {size[1] = containerDOM.scrollHeight;
    }

    this.backgroundCanvas = createCanvas(
      this.rendererType,
      containerDOM,
      size[0],
      size[1],
      pixelRatio,
    );
    this.canvas = createCanvas(
      this.rendererType,
      containerDOM,
      size[0],
      size[1],
      pixelRatio,
    );
    this.transientCanvas = createCanvas(
      this.rendererType,
      containerDOM,
      size[0],
      size[1],
      pixelRatio,
      true,
      {pointerEvents: 'none',},
    );
    Promise.all([this.backgroundCanvas, this.canvas, this.transientCanvas].map((canvas) => canvas.ready,
      ),
    ).then(() => (this.canvasReady = true));
  }

  // 扭转渲染类型,默认为 Canvas
  public changeRenderer(type) { }

  // 初始化生命周期钩子函数
  private initHooks() {
    this.hooks = {
      init: new Hook<{
        canvases: {
          background: Canvas;
          main: Canvas;
          transient: Canvas;
        };
      }>({name: 'init'}),
      datachange: new Hook<{data: GraphData; type: DataChangeType}>({name: 'datachange',}),
      itemchange: new Hook<{
        type: ITEM_TYPE;
        changes: GraphChange<NodeModelData, EdgeModelData>[];
        graphCore: GraphCore;
        theme: ThemeSpecification;
      }>({name: 'itemchange'}),
      render: new Hook<{
        graphCore: GraphCore;
        theme: ThemeSpecification;
        transientCanvas: Canvas;
      }>({name: 'render',}),
      layout: new Hook<{graphCore: GraphCore}>({name: 'layout'}),
      viewportchange: new Hook<ViewportChangeHookParams>({name: 'viewport'}),
      modechange: new Hook<{mode: string}>({name: 'modechange'}),
      behaviorchange: new Hook<{
        action: 'update' | 'add' | 'remove';
        modes: string[];
        behaviors: (string | BehaviorOptionsOf<{}>)[];}>({name: 'behaviorchange'}),
      itemstatechange: new Hook<{ids: ID[];
        states?: string[];
        value?: boolean;
      }>({name: 'itemstatechange',}),
      itemvisibilitychange: new Hook<{ids: ID[]; value: boolean }>({name: 'itemvisibilitychange',}),
      transientupdate: new Hook<{
        type: ITEM_TYPE | SHAPE_TYPE;
        id: ID;
        config: {
          style: ShapeStyle;
          action: 'remove' | 'add' | 'update' | undefined;
        };
        canvas: Canvas;
      }>({name: 'transientupdate'}),
      pluginchange: new Hook<{
        action: 'update' | 'add' | 'remove';
        plugins: (
          | string
          | {key: string; type: string; [cfgName: string]: unknown }
        )[];}>({name: 'pluginchange'}),
      themechange: new Hook<{
        theme: ThemeSpecification;
        canvases: {
          background: Canvas;
          main: Canvas;
          transient: Canvas;
        };
      }>({name: 'init'}),
      destroy: new Hook<{}>({ name: 'destroy'}),
    };
  }

  // 更改 spec 配置
  public updateSpecification(spec: Specification<B, T>): Specification<B, T> { }

  // 更改 theme 配置
  public updateTheme(theme: ThemeOptionsOf<T>) { }

  // 获取配置信息
  public getSpecification(): Specification<B, T> {}

  // 数据渲染,diff 比对
  public async read(data: GraphData) { }

  // 更改图数据
  public async changeData(
    data: GraphData,
    type: 'replace' | 'mergeReplace' = 'mergeReplace',
  ) { }

  // 清空画布
  public clear() {}

  // 获取视图核心
  public getViewportCenter(): PointLike {}
  
  // 更给视图转换
  public async transform(
    options: GraphTransformOptions,
    effectTiming?: CameraAnimationOptions,
  ): Promise<void> { }

  // 立即进行以后过渡变动
  public stopTransformTransition() {}

  // 画布位移
  public async translate(
    distance: Partial<{
      dx: number;
      dy: number;
      dz: number;
    }>,
    effectTiming?: CameraAnimationOptions,
  ) { }

  // 画布挪动至视图坐标
  public async translateTo({ x, y}: PointLike,
    effectTiming?: CameraAnimationOptions,
  ) { }

  // 画布放大 / 放大
  public async zoom(
    ratio: number,
    origin?: PointLike,
    effectTiming?: CameraAnimationOptions,
  ) { }

  // 画布放大 / 放大至
  public async zoomTo(
    zoom: number,
    origin?: PointLike,
    effectTiming?: CameraAnimationOptions,
  ) { }

  // 获取画布放大 / 放大比例
  public getZoom() {}

  // 旋转画布
  public async rotate(
    angle: number,
    origin?: PointLike,
    effectTiming?: CameraAnimationOptions,
  ) { }

  // 旋转画布至 
  public async rotateTo(
    angle: number,
    origin?: PointLike,
    effectTiming?: CameraAnimationOptions,
  ) { }

  // 自适应画布
  public async fitView(
    options?: {
      padding: Padding;
      rules: FitViewRules;
    },
    effectTiming?: CameraAnimationOptions,
  ) { }
  
  // 对齐画布核心与视图核心
  public async fitCenter(effectTiming?: CameraAnimationOptions) { }
  // 对齐元素
  public async focusItem(id: ID | ID[], effectTiming?: CameraAnimationOptions) { }

  // 获取画布大小
  public getSize(): number[] { }

  // 设置画布大小
  public setSize(size: number[]) { }

  // 获取视图下的渲染坐标
  public getCanvasByViewport(viewportPoint: Point): Point { }

  // 获取画布下的渲染视图
  public getViewportByCanvas(canvasPoint: Point): Point { }

  // 获取浏览器坐标
  public getClientByCanvas(canvasPoint: Point): Point { }

  // 获取画布坐标
  public getCanvasByClient(clientPoint: Point): Point { }

  // ===== item operations =====
  
  // 获取节点数据
  public getNodeData(condition: ID | Function): NodeModel | undefined { }
  
  // 获取边数据
  public getEdgeData(condition: ID | Function): EdgeModel | undefined { }
 
  // 获取 combo 数据
  public getComboData(condition: ID | Function): ComboModel | undefined { }
  
  // 获取所有节点数据
  public getAllNodesData(): NodeModel[] { }
  
  // 获取所有边数据
  public getAllEdgesData(): EdgeModel[] { }
  
  // 获取所有 combo 类型数据
  public getAllCombosData(): ComboModel[] { }
  
  // 获取相干边数据
  public getRelatedEdgesData(
    nodeId: ID,
    direction: 'in' | 'out' | 'both' = 'both',
  ): EdgeModel[] {}
  
  // 获取邻近节点数据
  public getNeighborNodesData(
    nodeId: ID,
    direction: 'in' | 'out' | 'both' = 'both',
  ): NodeModel[] {}

  // 获取状态类型的 id
  public findIdByState(
    itemType: ITEM_TYPE,
    state: string,
    value: string | boolean = true,
    additionalFilter?: (item: NodeModel | EdgeModel | ComboModel) => boolean,
  ): ID[] {}
  
  // 增加数据
  public addData(
    itemType: ITEM_TYPE,
    models:
      | NodeUserModel
      | EdgeUserModel
      | ComboUserModel
      | NodeUserModel[]
      | EdgeUserModel[]
      | ComboUserModel[],
    stack?: boolean,
  ):
    | NodeModel
    | EdgeModel
    | ComboModel
    | NodeModel[]
    | EdgeModel[]
    | ComboModel[] {}
  
  // 移除数据
  public removeData(itemType: ITEM_TYPE, ids: ID | ID[], stack?: boolean) { }
  
  // 更新数据
  public updateData(
    itemType: ITEM_TYPE,
    models:
      | Partial<NodeUserModel>
      | Partial<EdgeUserModel>
      | Partial<
          | ComboUserModel
          | Partial<NodeUserModel>[]
          | Partial<EdgeUserModel>[]
          | Partial<ComboUserModel>[]
        >,
    stack?: boolean,
  ):
    | NodeModel
    | EdgeModel
    | ComboModel
    | NodeModel[]
    | EdgeModel[]
    | ComboModel[] {}

  // 更新节点地位
  public updateNodePosition(
    models:
      | Partial<NodeUserModel>
      | Partial<
          ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
        >,
    stack?: boolean,
  ) { }
  
  // 显示类型元素
  public showItem(ids: ID | ID[], disableAniamte?: boolean) { }
  
  // 暗藏类型元素
  public hideItem(ids: ID | ID[], disableAniamte?: boolean) { }
  
  // 设置类型元素状态
  public setItemState(ids: ID | ID[],
    states: string | string[],
    value: boolean,
  ) { }
  
  // 获取类型元素状态
  public getItemState(id: ID, state: string) { }

  // 清空类型元素状态
  public clearItemState(ids: ID | ID[], states?: string[]) { }

  // 获取渲染器 box
  public getRenderBBox(id: ID | undefined): AABB | false { }

  // 获取显示类型 id
  public getItemVisible(id: ID) { }

  // ===== combo operations =====
  
  // 创立组
  public createCombo(
    combo: string | ComboUserModel,
    childrenIds: string[],
    stack?: boolean,
  ) { }
 
  // 勾销组
  public uncombo(comboId: ID, stack?: boolean) { }
  
  // 开释组
  public collapseCombo(comboId: ID, stack?: boolean) { }
  
  // 扩大组
  public expandCombo(comboId: ID, stack?: boolean) { }

  // ===== layout =====
  
  // 设置布局参数
  public async layout(options?: LayoutOptions) { }

  // 勾销布局算法
  public stopLayout() {}

  // 设置交互模式
  public setMode(mode: string) { }

  // 增加交互行为
  public addBehaviors(behaviors: BehaviorOptionsOf<B>[],
    modes: string | string[],) { }
  
  // 移除交互行为
  public removeBehaviors(behaviorKeys: string[], modes: string | string[]) { }

  // 更新交互行为
  public updateBehavior(behavior: BehaviorOptionsOf<B>, mode?: string) { }

  // 增加插件
  public addPlugins(
    pluginCfgs: (
      | {
          key: string;
          type: string;
          [cfgName: string]: unknown; // TODO: configs from plugins
        }
      | string
    )[],) { }

  // 移除插件
  public removePlugins(pluginKeys: string[]) { }

  // 更新插件
  public updatePlugin(plugin: {
    key: string;
    type: string;
    [cfg: string]: unknown;
  }) { }

  // 绘制过渡动效
  public drawTransient(
    type: ITEM_TYPE | SHAPE_TYPE,
    id: ID,
    config: {
      action: 'remove' | 'add' | 'update' | undefined;
      style: ShapeStyle;
      onlyDrawKeyShape?: boolean;
    },
  ): DisplayObject { }

  // 销毁画布
  public destroy(callback?: Function) {}}

StdLib

规范库用于提供和社区开发者进行交互的规范构件,不便自定义开发及共建共享。其中,提供了 dataextensionsinteractionitemlayoutplugintheme 以及 viewport 的插件化能力。

这里,以 plugin 的控制器接入为例,代码如下:

// https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/controller/plugin.ts

export class PluginController {public extensions: any = [];
  public graph: IGraph;

  /**
   * Plugins on graph.
   * @example
   * {'minimap': Minimap, 'tooltip': Tooltip}
   */
  private pluginMap: Map<string, {type: string; plugin: Plugin}> = new Map();

  /**
   * Listeners added by all current plugins.
   * @example
   * {*   'minimap': { 'afterlayout': function},
   * }
   */
  private listenersMap: Record<string, Record<string, Listener>> = {};

  constructor(graph: IGraph<any, any>) {
    this.graph = graph;
    this.tap();}

  /**
   * Subscribe the lifecycle of graph.
   */
  private tap() {this.graph.hooks.init.tap(this.onPluginInit.bind(this));
    this.graph.hooks.pluginchange.tap(this.onPluginChange.bind(this));
    this.graph.hooks.destroy.tap(this.onDestroy.bind(this));
  }

  private onPluginInit() {
    // 1. Initialize new behaviors.
    this.pluginMap.clear();
    const {graph} = this;
    const pluginConfigs = graph.getSpecification().plugins || [];
    pluginConfigs.forEach((config) => {this.initPlugin(config);
    });

    // 2. Add listeners for each behavior.
    this.listenersMap = {};
    this.pluginMap.forEach((item, key) => {const { plugin} = item;
      this.addListeners(key, plugin);
    });
  }

  private initPlugin(config) {const { graph} = this;
    const Plugin = getExtension(config, registry.useLib, 'plugin');
    const options = typeof config === 'string' ? {} : config;
    const type = typeof config === 'string' ? config : config.type;
    const key = typeof config === 'string' ? config : config.key || type;
    const plugin = new Plugin(options);
    plugin.init(graph);
    this.pluginMap.set(key, { type, plugin});
    return {key, type, plugin};
  }

  private onPluginChange(params: {
    action: 'update' | 'add' | 'remove';
    plugins: (string | { key: string; type: string; options: any})[];}) {const { action, plugins: pluginCfgs} = params;
    if (action === 'add') {pluginCfgs.forEach((config) => {const { key, plugin} = this.initPlugin(config);
        this.addListeners(key, plugin);
      });
      return;
    }

    if (action === 'remove') {pluginCfgs.forEach((config) => {
        const key =
          typeof config === 'string' ? config : config.key || config.type;
        const item = this.pluginMap.get(key);
        if (!item) return;
        const {plugin} = item;
        this.removeListeners(key);
        plugin.destroy();
        this.pluginMap.delete(key);
      });
      return;
    }

    if (action === 'update') {pluginCfgs.forEach((config) => {if (typeof config === 'string') return;
        const key = config.key || config.type;
        const item = this.pluginMap.get(key);
        if (!item) return;
        const {plugin} = item;
        plugin.updateCfgs(config);
        this.removeListeners(key);
        this.addListeners(key, plugin);
      });
      return;
    }
  }

  private addListeners = (key: string, plugin: Plugin) => {const events = plugin.getEvents();
    this.listenersMap[key] = {};
    Object.keys(events).forEach((eventName) => {
      // Wrap the listener with error logging.
      const listener = wrapListener(
        key,
        eventName,
        events[eventName].bind(plugin),
      );
      this.graph.on(eventName, listener);
      this.listenersMap[key][eventName] = listener;
    });
  };

  private removeListeners = (key: string) => {const listeners = this.listenersMap[key];
    Object.keys(listeners).forEach((eventName) => {const listener = listeners[eventName];
      if (listener) {this.graph.off(eventName, listener);
      }
    });
  };

  private onDestroy() {this.pluginMap.forEach((item) => {const { plugin} = item;
      plugin.destroy();});
  }

  destroy() {}
}

而其余的规范库控制器,则通过配置的形式,在 Graph 初始化时进行接入,代码如下:

// https://github.com/antvis/G6/blob/v5/packages/g6/src/stdlib/index.ts
const stdLib = {
  transforms: {comboFromNode,},
  themes: {
    light: LightTheme,
    dark: DarkTheme,
  },
  themeSolvers: {
    spec: SpecThemeSolver,
    subject: SubjectThemeSolver,
  },
  layouts: layoutRegistry,
  behaviors: {
    'activate-relations': ActivateRelations,
    'drag-canvas': DragCanvas,
    'hover-activate': HoverActivate,
    'zoom-canvas': ZoomCanvas,
    'drag-node': DragNode,
    'click-select': ClickSelect,
    'brush-select': BrushSelect,
    'lasso-select': LassoSelect,
    'zoom-canvas-3d': ZoomCanvas3D,
    'rotate-canvas-3d': RotateCanvas3D,
    'track-canvas-3d': TrackCanvas3D,
    'orbit-canvas-3d': OrbitCanvas3D,
  },
  plugins: {
    minimap: Minimap,
    legend: Legend,
  },
  nodes: {
    'circle-node': CircleNode,
    'sphere-node': SphereNode,
  },
  edges: {'line-edge': LineEdge,},
  combos: {},};

export {stdLib}

总结

综上所述,AntV G6 5.0 提供了更好的插件化机制,将性能进行解耦后提供裸露的通用规范制式不便开发者可能更好的开源与共建共享,从而利用开源社区的力量将 G6 建设成为更加通用且适配更多场景的图可视化库。开源的商业逻辑从来不在开源自身,用闭源的思路做开源就失去了开源的价值,共勉!!!

参考

  • G6 官网
  • G6 源码
  • AntV 图发布会圆满收官,G6 5.0 招募社区大牛,独特拥抱开源!
退出移动版