在part-1中我们重点讨论了G6启动的全流程。其实这个刚好就是graph文件所包含的内容,今天我们来讨论G6如何把用户的数据转化为节点实例,这里就涉及两个概念 Item 与 shape
- Item是宏观的,nodes数据转化为Node实例,edges数据转化为Edge实例
- shape是微观的,每个Node实例中包含多个shape图形,具体到在canvas层绘制的。
Item
Graph.addItem
通过part1的介绍,我们知道节点数据 通过addItem转化为G6的Item实例,核心代码如下
addItem(type, model) {
return this.get('itemController').addItem(type, model);
}
debug断点进入,发现它是将用户的数据node 作为model传入Node类中,同时将Node实例存储在itemMap中
PS:这块我是建议直接使用ES6的Map
这里有两个关键点
- Item.Node 这个类是如何实现的?
- 创建实例的参数 parent.addroup()做了什么事情?
按照代码的执行顺序,我们先看parent.addGroup()
parent.addGroup()
parent 是什么?
是在initGroup阶段,产生的nodeGroup。
var parent = graph.get(type + 'Group') || graph.get('group');
nodeGroup 是什么?
是canvas.addGroup(“canvas-root”).addGroup(“canvas-node”)
_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);
}
canvas是什么?
canvs是由new G.canvas()实例化的对象
const canvas = new G.Canvas({
containerDOM: container,
width: this.get('width'),
height: this.get('height'),
renderer: this.get('renderer'),
pixelRatio: this.get('pixelRatio')
});
G.canvas是什么?
G是antv可视化体系的底层绘图引擎,canvas类是由Group继承而来的,Group类是由Element继承而来的,Util.extend 可以理解为ES6的extends继承,Util.augment可以理解为对原型链的merge扩展
const Canvas = function(cfg) {
Canvas.superclass.constructor.call(this, cfg);
};
Util.extend(Canvas, Group);
Util.augment(Canvas, Event, {init() {}})
-------------------------------------------
Util.extend(Group, Element);
这样追究下去,得专门写一篇关于G的源码解读,我们就先忽略,找到我们的需要研究的第二个关键方法addGroup
addGroup是什么?
addGroup是Group类的一个方法,用于图组的分类,目的是产生一个新的group实例,同时将这个实例push到当前的group的children中,便于后续的递归绘图
// 简化后的核心代码
addGroup(param, cfg) {
const canvas = this.get('canvas');
rst = new Group();
this.add(rst);
return rst;
},
add(items) {
const self = this;
const children = self.get('children');
children.push(item);
return self;
},
分析到这里,我们回过头再来看parent.addGroup(),它做的事情就是在第二层Group(id:canvas-node)上,将所有的nodes节点,挂载在第二层Group的children中。
我们debug验证下:
- 第一层group是root
- 第二层group是node和edge两个大类
- 第三层group就是用户的nodes节点或者是边的数据
- 第四层是group是具体绘图的shape。也是G绘图的最小单元了,它的添加是通过addShape完成的
通过addGroup方法,我们可以将用户输入的节点数据转化为canvas的group,挂载在canvas的每层group的children中。这样后续我们使用canvas API就可以通过children,递归绘制图形
Item.Node()
constructor
了解完参数group的由来,我们接下来将重点放在Node类本身的实现上。源码地址 src/item/node.js
class Node extends Item {}
我们在Node class中没有发现constructor函数,那这是ES6 class的一种简写,得先走一遍父类的constructor函数。通过查看父类Item的构造函数,我们发现
class 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();
}
- 写法和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方法
init() {
const shapeFactory = Shape.getFactory(this.get('type'));
this.set('shapeFactory', shapeFactory);
}
_drawInner() {
const self = this;
const shapeFactory = self.get('shapeFactory');
const model = self.get('model');
self.updatePosition(model);
const cfg = self.getShapeCfg(model);
shapeFactory.draw(cfg.shape, cfg, group);
}
估计大家看到这儿,头一次觉得晕乎乎,Shape是什么?shapeFactory是什么?
Shape
Shape初始化是一个空对象,它的核心方法有两个 registerFactory和 getFactory
**
registerFactory
注册Shape的工厂方法,比如用户想要注册”Node”,那么将在Shape这个对象上,添加一个Node类,这个类的基类是ShapeFactoryBase,同时产生registerNode方法,挂载在Shape对象上。
建议大家直接debug理解,因为它的注册方法不是genRegisterFactory,是通过一个函数,修改一个外部的对象,追加上注册方法,还用到了mixin,工厂方法。对于习惯了函数式编程和数据不可变的同学,可能会抓狂,感觉像goto语句,稍不留神就不知道跑到哪里去了,所以建议直接debug查看比较好。
// 获得 ShapeFactory
Shape.getFactory = function(factoryType) {
const self = this;
factoryType = ucfirst(factoryType);
return self[factoryType];
};
读到这里,我们需要回到item的init/draw阶段,init阶段拿到的是NodeFactory,draw阶段调用的是NodeFactory的draw方法
shape.draw是什么?
接上图,shape.draw 对于我们自定义节点,就是registerNode(‘my-node’,{draw:{} })中的draw方法。这个时候,我们将demo中的自定义节点减少,只保留“hsf” 这个自定义节点,核心思路是通过canvas中的group.addShape将用户自定义的shape存放在当前group的children下。
G6.registerNode('hsf', {
draw(cfg,group) {
group.addShape('circle', {
attrs: {
x: 36,
y: 40,
r:55,
stroke:'#71cd00',
lineWidth: 5,
}
})
return group
}
})
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.paint(),通过G6完成图的绘制,详见Part-3