G6 使用入门与总结

这周的工作中,有一个需求需要我去在前端生成一个决策树,于是去考察了很多开源的方案,下面这是我一开始调研的图表,大家可以看一下,根据自己的选择去挑自己合适的,最终我选的是G6。

其实G6的文档已经算是非常不错的了,那我写这篇博客的目的主要有两点:

  • 根据自己的使用体验,重新组织成我习惯记忆的方式,方便以后自己查看。
  • 总结一下使用过程中踩的几个小坑。

这里吐槽一句,G6对于TypeScript实在是太不友好了,所有的属性都是可选的,有时候写下来一堆问号

组件库上手难度性能自由度/已有方案成熟型备注
G6文档较为详细:前言节点数少,但是单个节点比较复杂时候比较实用自由度较高,支持自定义节点主要是做图分析
X6文档较为详细 快速上手节点多,但是单个简单,且更多的是拖拽等操作时比较实用主要做的是图编辑
ggEditor几乎没有文档,也不更新文档了 alibaba/GGEditor蚂蚁金服团队基于G6+React封装的,与React兼容较好,但是早已不更新,使用的是3.5版本的G6
Graphin功能概览自由度较低,只支持圆形节点,无法定制且已有方案不符合要求 主要做图分析,而不是图编辑蚂蚁金服团队基于G6+React封装的,与React兼容较好
jointjs收费,且license为MPL-2.0
jsPlumb基于Jquery和canvas,大批量操作可能有问题使用上与React有些不搭配
d3文档比较清晰,但是较为偏底层,所以要实现需求难度较大自由度较高,但是感觉还比较原始,不如X6成熟如果把G6和X6比做arco,这个更像是react
spritejs文档比较清晰,但是较为偏底层,所以要实现需求难度较大同上同上

如何在React中使用G6

一开始我还在GGEditor和G6之间徘徊,因为我的前端项目是基于React的,使用GGEditor可能会方便一点,但是我在G6的官网找到了官方推荐的React中的使用示例,这才决定使用G6,这是官方势力的链接:https://g6.antv.vision/zh/docs/manual/advanced/g6InReact

G6核心概念

这里借用一下官方网站的图片

我来帮大家理一下,这几个核心概念之间的关系:

在G6中,主体(objects)与关系(relationships)的组成。它甚至不局限于视觉,主体与关系的数据也可以称为图。在 G6 中,Graph 对象是图的载体,它包含了图上的所有元素(节点、边等),同时挂载了图的相关操作(如交互监听、元素操作、渲染等)。Graph 对象的生命周期为:初始化 —> 加载数据 —> 渲染 —> 更新 —> 销毁。

图形

图形:又叫做Shape,Shape 指 G6 中的图形、形状,它可以是圆形、矩形、路径等。它一般与 G6 中的节点、边、Combo 相关。G6 中的每一种节点/边/ Combo 由一个或多个 Shape 组成。节点、边、Combo、标签文本的配置都会被体现到对应的图形上。

既然每种节点都可以由多个图形组成,那么同一个节点中的所有图形就可以组成一个组,,也就是图形分组,而每个组都有一个关键图形(keyShape),像内置的rect节点的关键图形就是个rect图形。

不过到目前为止,我所接触的分组概念一般是在自定义节点的时候才会用到,等会解释自定义节点的时候会说。

每个图形都有自己的属性,不同的图形有公共的属性,但也有各自独特的属性,公共的属性,比如图的填充色,边框色,边框宽度,阴影颜色,阴影位置等。

单独的属性,如矩形会有宽度和高度,圆会有半径等等。

图形分组 group 类似于SVG 中的 g 是用来组合图形对象的容器。在 group 上添加变换(例如剪裁、旋转、放缩、平移等)会应用到其所有的子元素上。在 group 上添加属性(例如颜色、位置等)会被其所有的子元素继承。此外, group 可以多层嵌套使用,因此可以用来定义复杂的对象。

图形变换则是指的旋转缩放等

布局

布局则是G6自带的一些帮助你安排自动节点位置的布局方式,由于我这次是要生成一个决策树,每个节点的位置需要我自己控制,所以就没有仔细去看,有需要的可以去官网查阅

交互与事件

事件可以分为以下四个层次:

  • 画布、图形层次的事件,mousedownmouseupclickmouseentermouseleave 等;
  • 节点/边 上的事件,node:mousedownedge:click 等,以 type:eventName 为事件名称;
  • 时机事件:
    • 节点/边增删改时的事件, 例如:beforeadditemafteradditem 等;
    • 节点/边状态改变时的事件:beforerefreshitemafterrefreshitem
    • 布局时机:beforelayoutafterlayout

如果要了解 G6 支持的所有事件,请参考 Event API

在事件之上,G6定义了Behavior,其实就是事件及其处理函数的集合,我们从如何自定义一个事件就可以看出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
G6.registerBehavior('activate-node', {
getDefaultCfg() {
return {
multiple: true
};
},
getEvents() {
return {
'node:click': 'onNodeClick',
'canvas:click': 'onCanvasClick'
};
}
onNodeClick(e) {
const graph = this.graph;
const item = e.item;
if (item.hasState('active')) {
graph.setItemState(item, 'active', false);
return;
}
// this 上即可取到配置,如果不允许多个 'active',先取消其他节点的 'active' 状态
if (!this.multiple) {
this.removeNodesState();
}
// 置点击的节点状态 'active' 为 true
graph.setItemState(item, 'active', true);
},
onCanvasClick(e) {
// shouldUpdate 可以由用户复写,返回 true 时取消所有节点的 'active' 状态,即将 'active' 状态置为 false
if (this.shouldUpdate(e)) {
removeNodesState();
}
},
removeNodesState() {
graph.findAllByState('node', 'active').forEach(node => {
graph.setItemState(node, 'active', false);
});
}
});

当然,G6也有很多内置的事件,比如拖拽节点,拖拽或者缩放画布。

在事件之上,定义了交互模式Mode,每个交互模式就是几个Behavior的集合,如

1
2
3
4
5
6
7
8
9
10
const graph = new G6.Graph({
container: 'mountNode',
width: 500,
height: 500,
modes: {
// 支持的 behavior
default: ['drag-canvas', 'zoom-canvas'],
edit: ['click-select'],
},
});

以上是模式定义的一个例子。在图上定义了两个模式,分别是 defaultedit。其中 default 包含两个 Behavior'drag-canvas' 和 'zoom-canvas',都使用行为的默认参数。

状态

G6 中的 state,指的是节点或边的状态,包括交互状态业务状态两种。

在 G6 中,配置交互状态和业务状态的方式是相同的。对于部分只使用 G6 来完成某个需求的开发,而不想深入理解 G6 的用户,其实不用区分交互状态和业务状态的区别,使用相同的方式定义状态,完全没有理解成本。

交互状态

交互状态是与具体的交互动作密切相关的,如用户使用鼠标选中某个节点则该节点被选中,hover 到某条边则该边被高亮等。

G6 中默认处理的是交互状态。

业务状态

指根据用户业务需求自定义的状态。业务状态是与交互动作无关的,与具体业务逻辑强相关的,也可理解为是强数据驱动的。如某个任务的执行状态、某条申请的审批状态等,不同的数据值代表不同的业务状态。业务状态与用户交互动作无关,但在 G6 中的处理方式同交互状态一致。

不论是节点还是边,它们的属性分为两种:

  • 样式属性 style:对应 Canvas 中的各种样式,在元素状态 State 发生变化时,可以被改变;配置state样式
  • 其他属性:例如图形类型( type)、id(id )一类在元素状态 State 发生变化时不能被改变的属性。

图元素

这次主要用到的就是自定义节点和边,主要就是踩坑,都在下面的总结里了

总结

  • 图形属性与节点属性不同,虽然有很多相同配置
  • 自定义dom节点,只能在render是svg模式下生效
  • 自定义dom节点上的时间,不会被图监听到,而是被自身截获。
  • 自定义节点时,在图形分组上第一次调用addShape返回的就是关键shape,同时越往后的shape越靠上,也就是会覆盖在之前的shape上面。