在part-1中我们重点讨论了G6启动的全流程。其实这个刚好就是graph文件所包含的内容,今天我们来讨论G6如何把用户的数据转化为节点实例,这里就涉及两个概念 Item 与 shape

  • Item是宏观的,nodes数据转化为Node实例,edges数据转化为Edge实例
  • shape是微观的,每个Node实例中包含多个shape图形,具体到在canvas层绘制的。

image.png

Item

Graph.addItem

通过part1的介绍,我们知道节点数据 通过addItem转化为G6的Item实例,核心代码如下

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

debug断点进入,发现它是将用户的数据node 作为model传入Node类中,同时将Node实例存储在itemMap中

PS:这块我是建议直接使用ES6的Map

image.png

这里有两个关键点

  • Item.Node 这个类是如何实现的?
  • 创建实例的参数 parent.addroup()做了什么事情?

按照代码的执行顺序,我们先看parent.addGroup()

parent.addGroup()

parent 是什么?

是在initGroup阶段,产生的nodeGroup。

  1. var parent = graph.get(type + 'Group') || graph.get('group');

nodeGroup 是什么?

是canvas.addGroup(“canvas-root”).addGroup(“canvas-node”)

  1. _initGroups() {
  2. const canvas = this.get('canvas');
  3. const id = this.get('canvas').get('el').id;
  4. const group = canvas.addGroup({
  5. id: id + '-root',
  6. className: Global.rootContainerClassName
  7. });
  8. if (this.get('groupByTypes')) {
  9. const edgeGroup = group.addGroup({
  10. id: id + '-edge',
  11. className: Global.edgeContainerClassName
  12. });
  13. const nodeGroup = group.addGroup({
  14. id: id + '-node',
  15. className: Global.nodeContainerClassName
  16. });
  17. this.set({ nodeGroup, edgeGroup });
  18. }
  19. this.set('group', group);
  20. }

canvas是什么?

canvs是由new G.canvas()实例化的对象

  1. const canvas = new G.Canvas({
  2. containerDOM: container,
  3. width: this.get('width'),
  4. height: this.get('height'),
  5. renderer: this.get('renderer'),
  6. pixelRatio: this.get('pixelRatio')
  7. });

G.canvas是什么?

G是antv可视化体系的底层绘图引擎,canvas类是由Group继承而来的,Group类是由Element继承而来的,Util.extend 可以理解为ES6的extends继承,Util.augment可以理解为对原型链的merge扩展

  1. const Canvas = function(cfg) {
  2. Canvas.superclass.constructor.call(this, cfg);
  3. };
  4. Util.extend(Canvas, Group);
  5. Util.augment(Canvas, Event, {init() {}})
  6. -------------------------------------------
  7. Util.extend(Group, Element);

这样追究下去,得专门写一篇关于G的源码解读,我们就先忽略,找到我们的需要研究的第二个关键方法addGroup

addGroup是什么?

addGroup是Group类的一个方法,用于图组的分类,目的是产生一个新的group实例,同时将这个实例push到当前的group的children中,便于后续的递归绘图

  1. // 简化后的核心代码
  2. addGroup(param, cfg) {
  3. const canvas = this.get('canvas');
  4. rst = new Group();
  5. this.add(rst);
  6. return rst;
  7. },
  8. add(items) {
  9. const self = this;
  10. const children = self.get('children');
  11. children.push(item);
  12. return self;
  13. },

分析到这里,我们回过头再来看parent.addGroup(),它做的事情就是在第二层Group(id:canvas-node)上,将所有的nodes节点,挂载在第二层Group的children中。
我们debug验证下:

  • 第一层group是root

image.png

  • 第二层group是node和edge两个大类

image.png

  • 第三层group就是用户的nodes节点或者是边的数据

image.png

  • 第四层是group是具体绘图的shape。也是G绘图的最小单元了,它的添加是通过addShape完成的

image.png

通过addGroup方法,我们可以将用户输入的节点数据转化为canvas的group,挂载在canvas的每层group的children中。这样后续我们使用canvas API就可以通过children,递归绘制图形

Item.Node()

constructor

了解完参数group的由来,我们接下来将重点放在Node类本身的实现上。源码地址 src/item/node.js

  1. class Node extends Item {}

我们在Node class中没有发现constructor函数,那这是ES6 class的一种简写,得先走一遍父类的constructor函数。通过查看父类Item的构造函数,我们发现

  1. class Item {
  2. constructor(cfg) {
  3. const defaultCfg = {};
  4. this._cfg = Util.mix(defaultCfg, this.getDefaultCfg(), cfg);
  5. const group = cfg.group;
  6. group.set('item', this);
  7. let id = this.get('model').id;
  8. if (!id || id === '') {
  9. id = Util.uniqueId(this.get('type'));
  10. }
  11. this.set('id', id);
  12. group.set('id', id);
  13. this.init();
  14. this.draw();
  15. }
  • 写法和Graph实例风格保持一致,所有的配置存在_cfg中
  • group.set(‘item’, this);说明Item实例对象被存储在canvas的group的item(key值)下。这也就能解释为什么G6的事件,参数e中有item对象
  • id 如果没有在model(数据)中设置,那么就会在内部生成一个uuid

    init /draw

    初始化是要得到ShapeFactory对象,type只有两种类型,node和edge。绘制就是拿到这个shapeFactory对象,调用其draw方法

  1. init() {
  2. const shapeFactory = Shape.getFactory(this.get('type'));
  3. this.set('shapeFactory', shapeFactory);
  4. }
  5. _drawInner() {
  6. const self = this;
  7. const shapeFactory = self.get('shapeFactory');
  8. const model = self.get('model');
  9. self.updatePosition(model);
  10. const cfg = self.getShapeCfg(model);
  11. shapeFactory.draw(cfg.shape, cfg, group);
  12. }

估计大家看到这儿,头一次觉得晕乎乎,Shape是什么?shapeFactory是什么?

Shape

Shape初始化是一个空对象,它的核心方法有两个 registerFactorygetFactory
**

registerFactory

注册Shape的工厂方法,比如用户想要注册”Node”,那么将在Shape这个对象上,添加一个Node类,这个类的基类是ShapeFactoryBase,同时产生registerNode方法,挂载在Shape对象上。

建议大家直接debug理解,因为它的注册方法不是genRegisterFactory,是通过一个函数,修改一个外部的对象,追加上注册方法,还用到了mixin,工厂方法。对于习惯了函数式编程和数据不可变的同学,可能会抓狂,感觉像goto语句,稍不留神就不知道跑到哪里去了,所以建议直接debug查看比较好。

image.png

  • registerNode

    getFactory

    这个方法就是获得通过registerFactory注册的factoryType对象,比如NodeFactory和EdgeFactory
  1. // 获得 ShapeFactory
  2. Shape.getFactory = function(factoryType) {
  3. const self = this;
  4. factoryType = ucfirst(factoryType);
  5. return self[factoryType];
  6. };

读到这里,我们需要回到item的init/draw阶段,init阶段拿到的是NodeFactory,draw阶段调用的是NodeFactory的draw方法

image.png

shape.draw是什么?

接上图,shape.draw 对于我们自定义节点,就是registerNode(‘my-node’,{draw:{} })中的draw方法。这个时候,我们将demo中的自定义节点减少,只保留“hsf” 这个自定义节点,核心思路是通过canvas中的group.addShape将用户自定义的shape存放在当前group的children下。

  1. G6.registerNode('hsf', {
  2. draw(cfg,group) {
  3. group.addShape('circle', {
  4. attrs: {
  5. x: 36,
  6. y: 40,
  7. r:55,
  8. stroke:'#71cd00',
  9. lineWidth: 5,
  10. }
  11. })
  12. return group
  13. }
  14. })

image.png

summary

思路梳理

  • Item是宏观上的,定义了Node和Edge的数据结构
  • Shape是微观上的,定义了每个Node实例下的每个Shape是如何组合和定义的

    用户把数据 {nodes,edges} 通过addItem方法灌入G6,nodes转化为Node实例,edges转化为Edge实例,也通过group.addGroup,分别加入nodeGroup的children和edgeGroup的children中。用户通过官方定义的节点或者自己定义的节点,在draw方法里调用group.addShape方法,将每个图形shape存储在所属group的children中。

类表如下

分类 方法名 底层实现 功能描述
Item getBBox group.getBBox() 获取元素的包围盒
toFront group.toFront() 绘制元素到最前面
toBack group.toBack() 绘制元素到最后面
show group.show() 显示元素
hide group.hide() 隐藏元素
destory group.remove 销毁元素
updatePosition group.resetMatrix()
group.translate(model.x,model.y)
更新位置
refresh/update
- x,y改变,调用updatePostion
- model改变,调用draw方法
更新/刷新元素
setState ShapeFactory.setState 更改元素状态
clearStates ShapeFactory.setState 清除元素状态
Node getInEdges
getOutEdges
Edge setSource
setTarget

思维导图

G6源码阅读-Part2-Item与Shape - 图10

后续

接下来,我们需要完成最后一步:G6.paint(),通过G6完成图的绘制,详见Part-3