简介:

G6 官网的介绍如下:

G6 是一个简单、易用、完备的图可视化引擎,它在高定制能力的基础上,
提供了一系列设计优雅、便于使用的图可视化解决方案。
能帮助开发者搭建属于自己的图 图分析应用或是 图编辑器应用。

这是一个很难抽象的库,既要有足够的拓展性以满足各种定制化的业务需求,又要让开发者用起来简单。

G6 跟 G2 一样依赖底层图表库 G, 这里我写了一个 demo, 用 G 的 API 做一个最简单的关系图, 有点繁琐,跟直接调用浏览器的 canvas API 画图差不多。G6 的工作就是在 G 之上再封装一层,让可视化更加容易。

G6 的代码用的是 @babel/preset-env 的语法, 没用 TypeScript, 所以看的有点难受,比如都用 Class 了但是没法用私有方法和私有属性,只能在名字前加个下划线 _ 区分。

首先我们来理解一下G6 代码的主流程。用 G6 做图时,最基础的步骤是:

const data = {
  nodes: [{
    id: 'node1',
    x: 100,
    y: 200
 },{
    id: 'node2',
    x: 300,
    y: 200
 }],
  edges: [{
    source: 'node1',
    target: 'node2'
 }]
};

const graph = new G6.Graph({
  container: 'mountNode',
  width: 500,
  height: 500
});
graph.data(data);
graph.render();

这里只有三个函数,我们先从 Graph 这个类的构造函数入手,再看 data 和 render 方法。

1. Graph 构造函数:

首先打开 src/graph/graph.js 文件,找到构造函数:

  constructor(inputCfg) {
    super();
    this._cfg = Util.deepMix(this.getDefaultCfg(), inputCfg);    // merge graph configs
    this._init();
  }

deepMix 类似 Object.assign, 用来 merge 默认配置和 Graph 初始化时传入的配置,并且把所有配置都存到了 this._cfg 下,set 和 get 方法很简单,就是改变和获取 this._cfg 里的属性。 然后再看调用的 _init 方法:

  _init() {
    this._initCanvas();
    const eventController = new Controller.Event(this);
    const viewController = new Controller.View(this);
    const modeController = new Controller.Mode(this);
    const itemController = new Controller.Item(this);
    const stateController = new Controller.State(this);
    this.set({ eventController, viewController, modeController, itemController, stateController });
    this._initPlugins();
  }
  1. 首先 _initCanvas
  2. 分别实例化了 Event, View, Mode, Item, State 5 个类
  3. 然后 _initPlugins。

1. _initCanvas:

  _initCanvas() {
    let container = this.get('container');
    if (Util.isString(container)) {
      container = document.getElementById(container);
      this.set('container', container);
    }
    if (!container) {
      throw Error('invalid container');
    }
    const canvas = new G.Canvas({
      containerDOM: container,
      width: this.get('width'),
      height: this.get('height'),
      renderer: this.get('renderer'),
      pixelRatio: this.get('pixelRatio')
    });
    this.set('canvas', canvas);
    this._initGroups();
  }

先调用 G.Canvas 创建一块画布,然后保存到 _cfg.canvas 里,这里 G.Canvas 如何作图暂时不展开了,因为那要深入到 G 的源码,就是另外一个话题了。然后再来看 this._initGroups:

  _initGroups() {
    const canvas = this.get('canvas');
    const id = this.get('canvas').get('el').id;
    const group = canvas.addGroup({ id: id + '-root', className: Global.rootContainerClassName });
    if (this.get('groupByTypes')) {
      const edgeGroup = group.addGroup({ id: id + '-edge', className: Global.edgeContainerClassName });
      const nodeGroup = group.addGroup({ id: id + '-node', className: Global.nodeContainerClassName });
      this.set({ nodeGroup, edgeGroup });
    }
    this.set('group', group);
  }

这里是初始化 Group 配置, Group 是 G 里面的一个概念。

图形分组可以嵌套图形和分组

就是说 Group 下可以嵌套再 addGroup, 也可以 addShape。其实分组很好理解,比如关系图中的节点,有图形和文字 label, 但是我们把他们加到一个 Group 里,这样方便一起操作,比如拖动,删除等,同时,所有的节点也在一个分组,这样构成了嵌套结构, 其中, 子 group 是存在于上一级的 children 属性中。可以在 debugger 中检查: image

以下面 http://127.0.0.1:2046/demos/index.html#/image-node 这个 demo为例:

image

它的 Group 结构是:

image

后面会说到,这个树形结构是在 render 方法调用时才形成的。G6 的思路很清晰,先组装这样一个 group 树,包含了绘图所需的所有信息,然后调用 Canvas.draw 画出即可,剩下的都是细枝末节。

2. 实例化 Event, View, Mode, Item, State 5 个类

这个先略过,后面再绕回来看。

3. _initPlugins

插件机制不影响主流程,先略过不看。

2. data 方法

  data(data) {
    this.set('data', data);
  }

就是简单的把 data 存到 this._cfg 下。

3. render 方法

   render() {
    const self = this;
    const data = this.get('data');
    if (!data) {
      throw new Error('data must be defined first');
    }
    this.clear();
    this.emit('beforerender');
    const autoPaint = this.get('autoPaint');
    this.setAutoPaint(false);
    Util.each(data.nodes, node => {
      self.add(NODE, node);
    });
    Util.each(data.edges, edge => {
      self.add(EDGE, edge);
    });
    if (self.get('fitView')) {
      self.get('viewController')._fitView();
    }
    self.paint();
    self.setAutoPaint(autoPaint);
    self.emit('afterrender');
  }

这段代码最核心的是遍历边和节点,然后调用 add 方法, 也就是addItem 方法:

 addItem(type, model) {
    return this.get('itemController').addItem(type, model);
 }

其中, itemController 是构造函数里 _init 实例化的, 打开 src/graph/controller/item.js 文件, 找到 addItem 方法:

  addItem(type, model) {
    const graph = this.graph;
    const parent = graph.get(type + 'Group') || graph.get('group'); // nodeGroup, EdgeGroup
    const upperType = Util.upperFirst(type);
    // ... 这里省略一段暂时不需要关注的代码
    if (type === EDGE) {
      let source = model.source;
      let target = model.target;
      if (source && Util.isString(source)) {
        source = graph.findById(source);
      }
      if (target && Util.isString(target)) {
        target = graph.findById(target);
      }
      if (!source || !target) {
        console.warn('The source or target node of edge ' + model.id + ' does not exist!');
        return;
      }
      item = new Item[upperType]({
        model,
        source,
        target,
        styles,
        linkCenter: graph.get('linkCenter'),
        group: parent.addGroup()
      });
    } else {
      item = new Item[upperType]({
        model,
        styles,
        group: parent.addGroup()
      });
    }
    graph.get(type + 's').push(item); // nodes, edges 存着相应实例
    graph.get('itemMap')[item.get('id')] = item; // Node 和 Edge 实例以 id 为 key 存储在了 itemMap
    graph.autoPaint();
    graph.emit('afteradditem', { item, model });
    return item;
  }

这里做的事情就是对于每个节点或者边,实例化一个 Node 对象或者 Edge 对象, 并保存到 graph._cfg 的 nodes, edges 和 itemMap, 并用 id 作为 itemMap 的 key。注意实例化时的 group 参数,都调用 parent.addGroup() 新建了一个 group, 这里取的 parent group 是 Graph 里 _initGroup 创建的 group.

Node 类和 Edge 类都继承自 Item 类,接下来我们看看 Item 类:

   constructor(cfg) {
    const defaultCfg = {}
    this._cfg = Util.mix(defaultCfg, this.getDefaultCfg(), cfg);
    const group = cfg.group;
    group.set('item', this);
    let id = this.get('model').id;
    if (!id || id === '') {
      id = Util.uniqueId(this.get('type'));
    }
    this.set('id', id);
    group.set('id', id);
    this.init();
    this.draw();
  }

核心是 this.init(), this.draw();,

init 方法:

    const shapeFactory = Shape.getFactory(this.get('type'));
    this.set('shapeFactory', shapeFactory);

_drawInner 方法

  _drawInner() {
    const self = this;
    const shapeFactory = self.get('shapeFactory');
    const group = self.get('group');
    const model = self.get('model');
    group.clear();

    if (!shapeFactory) {
      return;
    }
    self.updatePosition(model);
    const cfg = self.getShapeCfg(model); // 可能会附加额外信息
    const shapeType = cfg.shape;
    const keyShape = shapeFactory.draw(shapeType, cfg, group);
    if (keyShape) {
      keyShape.isKeyShape = true;
      self.set('keyShape', keyShape);
      self.set('originStyle', this.getKeyShapeStyle());
    }
    // 防止由于用户外部修改 model 中的 shape 导致 shape 不更新
    this.set('currentShape', shapeType);
    this._resetStates(shapeFactory, shapeType);
  }

这里的 Shape 对象,用到了工厂模式,讲道理我非常讨厌这种写法,各种动态修改,建议在调试时打断点,不然可能被绕晕了。打开 src/shape/shape.js 文件。我简单梳理了下流程:

以节点为例,首次运行时:

  1. Shape.registerFactory('node', {...}) => 通过 addRegister 函数 在 Shape 对象上动态加上了registerNode 方法和 Node 属性,Node 属性保存的是 shapeFactory, shapeFactory 是覆盖了 ShapeFactoryBase 部分默认设置的一个对象
  2. 增加默认的节点, Shape.registerNode('cirle') 等 => 通过 Util.mix({}, extendShape, cfg) 覆盖默认的 draw 方法等实现自定义, 并保存到 shapeFactory 对象的 circle 属性上。

嗯 大概就是这样,如果看不懂很正常,自己断点多走几遍就懂了。

我们回到 Item 类的 init 和 draw 方法, init 方法很简单,就是获取上面提到的 shapeFactory, _drawInner 方法的核心是调用 shapeFactory.draw(shapeType, cfg, group):

  draw(type, cfg, group) {
    const shape = this.getShape(type);
    const rst = shape.draw(cfg, group);
    shape.afterDraw(cfg, group, rst);
    return rst;
  }

shapeFactory.draw 方法调用的 shape.draw 是用户在自定义节点时可以覆盖的,实际上自定义节点就是在 draw 方法里往 最后一层 Group 里不断 addShape, 从而形成了上文中提到的包含全部绘图信息的树形结构:

image

最后,再回到 Graph 的 render 方法,在上图中的树形结构形成后,调用 paint 方法,实际上就是简单的调用 G 的 API 画图。

this.get('canvas').draw();

其它

理解上面的主流程之后,剩下的都是细枝末节,可以有需要的时候再慢慢看。