背景
最近业务场景中需要实现链路拓扑图,查找了一些资料,最终决定使用antV/g6。业务中需要实现的链路拓扑图并不复杂,也没有使用到图编辑的一些功能,但从实现简单链路拓扑的过程中,总结了一些基础知识,在这分享给大家。
选型
目前来说优秀的可视化库挺多的,它们主要都是基于SVG,Canvas,WebGL等技术,在这主要就是对比一些目前比较热门的可视化库。
1. Echarts
-
优点
- 配置简单
- 中文文档,文档详细上手快
-
缺点
- 灵活性较差,不支持拖拽等。
- 不能自定义节点类型。
Echarts具有配置简单,易上手的优点,正因为这个优点也就决定了它具有灵活性较差的缺点。配置简单主要是由于它是封装好的,我们只需要修改它的配置项即可。但我们只能修改它所存在的配置项,如果想要自己添加或修改配置项中不存在的东西是不可以的。另外对于我要实现的拓扑图来说,它的配置项中只提供了symbol属性配置节点标记的图形,只有有限的几种,不支持自定义节点的类型。因此我们要实现的拓扑图功能它很多都是满足不了的。
2. D3.js
-
优点
- 灵活性较高,可实现高度定制化图表
- 开源,文档完善。
-
缺点
- 使用门槛较高
- 英文文档
D3主要的优点就是灵活性较高,可实现高度定制化的图表。区别于echarts是使用canvas来绘制图形的,D3是通过Svg来绘制图形,这两者的不同之处在于,svg可以操作dom支持事件处理器,想要实现某个操作,直接调用相关的方法实现效果就行,拥有极大的自由度,几乎可以实现任何2d的设计需求,而canvas不支持事件处理器所以只能展示数据,而不能修改。
3. AntV G6
蚂蚁金服数据可视化解决方案中专门为流程图和关系分析推出的开源库,上手难度适中,灵活性很高,各种节点的自定义很好,而且和整个阿里开源生态是打通的,包括阿里图标库。可以支持 PC、移动端、小程序多个平台。
G6是什么
从各个维度解释G6
G6 是一个简单、易用、完备、开源的图可视化引擎,它在高定制能力的基础上,提供了一系列设计优雅、便于使用的图可视化解决方案。能帮助开发者搭建属于自己的图可视化、图分析(关系数据也被称为图数据,将关系数据可视化出来,即为图可视化,在图可视化的基础上,附加交互,布局,算法,分析方案等,完成一个分析任务,即为图可视分析。)、或图编辑器应用。
G6 是 antv 体系的一个图可视化品牌,主要关注关系图的绘制,展示关系数据,antv 还有其他应用,比如关注图表的 G2、F2,关注地理数据渲染的 L7 等
G6 是一个开源的 JavaScript 图形库,可以支持 PC、移动端、小程序多个平台。
图相关概念
1. 图元素
图的元素(Item)包含图上的节点 Node 、边 Edge 和 Combo 三大类。每个图元素由一个或多个 图形(Shape) 组成,且都会有自己的唯一关键图形(keyShape)。G6 内置了一系列具有不同基本图形样式的节点/边/ Combo,例如,节点可以是圆形、矩形、图片等。G6 中所有内置的元素样式详见 内置节点,内置边,内置 Combo。除了使用内置的节点/边/ Combo 外,G6 还允许用户通过自己搭配和组合 shape 进行节点/边/ Combo 的自定义,详见 自定义节点,自定义边,自定义 Combo。
2. 图形 shape与keyShape
Shape
Shape 指 G6 中的图形、形状,它可以是圆形、矩形、路径等。它一般与 G6 中的节点、边、Combo 相关。G6 中的每一种节点/边/ Combo 由一个或多个 Shape 组成。节点、边、Combo、标签文本的配置都会被体现到对应的图形上。
例如下图(左)的节点包含了一个圆形图形;下图(中)的节点含有有一个圆形和一个文本图形;下图(右)的节点中含有 5 个圆形(蓝绿色的圆和上下左右四个锚点)、一个文本图形。但每种节点/边/ Combo 都会有自己的唯一关键图形 keyShape,下图中三个节点的 keyShape 都是蓝绿色的圆,keyShape 主要用于交互检测、样式随状态自动更新等,见 keyShape。
keyShape
每一种节点/边/ Combo 都有一个唯一的关键图形 keyShape。它有两个主要特点:
响应样式
内置节点/边/ Combo 配置项中的 style 只体现在它的 keyShape 上。keyShape 是在节点/边/ Combo 的 draw() 方法或 drawShape() 方法中返回的图形对象。可通过node.get('keyShape') 获取
const data = {
nodes: [
{
x: 100,
y: 100,
label: 'rect',
type: 'rect',
style: {
// 仅在 keyShape 上生效
fill: 'lightblue',
stroke: '#888',
lineWidth: 1,
radius: 7,
},
linkPoints: {
top: true,
bottom: true,
left: true,
right: true,
// ... 四个圆的样式可以在这里指定
},
// labelCfg: {...} // 标签的样式可以在这里指定
},
],
};
const graph = new G6.Graph({
container: 'mountNode',
width: 500,
height: 300,
nodeStateStyles: {
// 各状态下的样式,平铺的配置项仅在 keyShape 上生效。需要在其他 shape 样式上响应状态变化则写法不同,参见上文提到的 配置状态样式 链接
hover: {
fillOpacity: 0.1,
lineWidth: 10,
},
},
});
graph.data(data);
graph.render();
// 监听鼠标进入节点事件
graph.on('node:mouseenter', (evt) => {
const node = evt.item;
// 激活该节点的 hover 状态
graph.setItemState(node, 'hover', true);
});
// 监听鼠标离开节点事件
graph.on('node:mouseleave', (evt) => {
const node = evt.item;
// 关闭该节点的 hover 状态
graph.setItemState(node, 'hover', false);
});
包围盒确定
确定节点 / Combo 的包围盒(Bounding Box) —— bbox(x, y, width, height) ,从而计算相关边的连入点(与相关边的交点)。若 keyShape 不同,节点与边的交点计算结果不同。
本例中的一个节点由一个 rect 图形和一个带灰色描边、填充透明的 circle 图形构成。
- 当节点的 keyShape 为 circle 时:
- 当节点的 keyShape 为 rect 时:
G6数据格式
1. 图的数据格式
// 定义数据源
const data = {
// 点集
nodes: [
{
id: '1',//节点id是唯一的,必须要设置
x: 100,
y: 100,
label:'node1'
},
{
id: '2',
x: 300,
y: 100,
label:'node2'
},
{
id: '3',
x: 300,
y: 300,
label:'node3'
},
],
// 边集
//边可不设置id,G6会为它生成唯一的id
edges: [
// 表示一条从 node1 节点连接到 node2 节点的边
{
source: '1',
target: '2',
},
// 表示一条从 node2 节点连接到 node3 节点的边
{
source: '2',
target: '3',
},
// 表示一条从 node3 节点连接到 node1 节点的边
{
source: '3',
target: '1',
},
],
};
2. 树图的数据格式
数据结构:树图的数据一般是嵌套结构,边的数据隐含在嵌套结构中,并不会特意指定 edge 。
// 定义数据源
const treeData = {
id: '1',
label: 'node1',
children: [
{
id: '2',
label: 'node2'
},
{
id: '3',
label: 'node3'
}
]
}
基础用法
- 准备好挂载节点。
- 实例化G6.
- 准备好渲染的json数据。
- 传入数据
//G6主要渲染流程
//1.准备好挂载节点。
//2.实例化G6.
//3.准备好渲染的json数据。
//4.传入数据
//在html中准备挂在节点
<div id="mountNode"></div>
// 定义数据源
//G6使用节点和边两个类型描述整个图,节点就是图的节点,边描述节点之间的关系。在此基础上有交互(behavior)、事件、状态管理、动画、布局几大功能。
const data = {
// 点集
nodes: [
{
id: 'node1',//节点id是唯一的,必须要设置
x: 100,
y: 200,
},
{
id: 'node2',
x: 300,
y: 200,
},
],
// 边集
edges: [
// 表示一条从 node1 节点连接到 node2 节点的边
//边可不设置id,G6会为它生成唯一的id
{
source: 'node1',
target: 'node2',
},
],
};
// 创建 G6 图实例
const graph = new G6.Graph({ //如果是树图,则使用 new G6.TreeGraph,并且必须配置layout属性
container: 'mountNode', // 指定图画布的容器 id,与第 9 行的容器对应
// 画布宽高
width: 800,
height: 500,
});
// 读取(加载)数据
graph.data(data);
// 渲染图
graph.render();
节点和边样式
配置节点和边样式有两种形式:
- 数据中直接配置,可以更方便灵活对每个节点和边配置样式
- graph实例上配置全局的节点和边的样式,在defalutNode和defalutEdge中统一配置节点和边的样式
// 定义数据源
const data = {
// 点集
nodes: [
{
id: 'node1',
x: 100,
y: 200,
//可在节点或者边内单独修改某些节点的样式
labelCfg: { // 标签配置属性
positions: 'center',// 标签的属性,标签在元素中的位置
style: { // 包裹标签样式属性的字段 style 与标签其他属性在数据结构上并行
fontSize: 12, // 标签的样式属性,文字字体大小
fill: 'red',
stroke: 'red',
// ... // 标签的其他样式属性
}
}
},
{
id: 'node2',
x: 300,
y: 200,
},
],
// 边集
edges: [
// 表示一条从 node1 节点连接到 node2 节点的边
{
source: 'node1',
target: 'node2',
},
]
};
// 创建 G6 图实例
const graph = new G6.Graph({
container: 'mountNode', // 指定图画布的容器 id,与第 9 行的容器对应
// 画布宽高
width: 800,
height: 500,
// 节点默认配置
defaultNode: {
//节点的样式
style: {
background: {
fill: '#ffffff',
},
},
//节点标签的样式
labelCfg: {
position: 'bottom',
offset: 2,
style: {
// ... 文本样式的配置
fontSize: 16,
},
},
size: 100,
type: 'donut',
label: 'donut',
donutAttrs: { // 甜甜圈字段,每个字段必须为 [key: string]: number
prop1: 1,
prop2: 2
},
donutColorMap: { // 甜甜圈颜色映射,字段名与 donutAttrs 中的字段名对应。不指定则使用默认色板
prop1: '#45952b',
prop2: '#eaa83f'
}
},
// 边默认配置
defaultEdge: {
type: 'quadratic',
style: {
endArrow: true,
lineAppendWidth: 5,
}
},
});
// 读取数据(加载数据)
graph.data(data);
// 渲染图
graph.render();
</script>
交互
1. 事件监听
G6 中所有元素监听都挂载在图实例上,如下代码中的 graph 对象是 G6.Graph 的实例,graph.on() 函数监听了某元素类型(node / edge)的某种事件(click / mouseenter / mouseleave / ... 所有事件参见:Event API)。
// 在图实例 graph 上监听
graph.on('元素类型:事件名', (e) => {
// do something
});
2. 状态样式设置
状态样式一般用于表达元素在不同状态交互,业务状态下的临时样式。例如:
hover 到元素上时,改元素设置为高亮
点击元素时,改元素设置为选中状态。
-
状态样式格式
{ [stateName:string]:{//stateName 是状态的名字字符串,自定义 //该元素keyShape在这个状态下的样式 ...(Style) //为元素内部的其他图形指定这个状态下的样式 [shapeName:string]:Style //shapeName是内部图形的name } }
在 G6 中,有三种方式配置不同状态的样式:
- 方式一:在节点/边数据中,在
stateStyles对象中定义状态; - 方式二:在实例化 Graph 时,通过
nodeStateStyles和edgeStateStyles对象定义; - 方式三:在自定义节点/边时,在 options 配置项的
stateStyles对象中定义状态。
方式一,方式二配置示例如下:
// 定义数据源
const data = {
// 点集
nodes: [
{
id: '1',
x: 100,
y: 100,
label: 'node1',
//方式一:通过stateStyles设置,只有设置了状态样式的节点才会存在状态样式
stateStyles: {
stateName1: { //keyShape状态名,可自定义
fill: 'CornflowerBlue',
stroke: 'blue',
'text-shape': {//文本图形
fill: 'green'
}
},
}
},
{
id: '2',
x: 400,
y: 100,
label: 'node2'
},
{
id: '3',
x: 400,
y: 300,
label: 'node3'
},
],
// 边集
edges: [
{
source: '1',
target: '2',
},
{
source: '2',
target: '3',
},
{
source: '3',
target: '1',
},
],
};
// 创建 G6 图实例
const graph = new G6.Graph({
container: 'mountNode', // 指定图画布的容器 id,与第 9 行的容器对应
// 画布宽高
width: 800,
height: 500,
// 节点默认配置
defaultNode: {
size: 100,
},
// 边默认配置
defaultEdge: {
type: 'quadratic',
style: {
endArrow: true,
}
},
// //方式二:在实例化 Graph 时,通过 `nodeStateStyles` 和 `edgeStateStyles` 对象定义,定义的状态样式会对所有节点生效
// nodeStateStyles: {
// stateName2: { //keyShape状态名,可自定义
// fill: 'BlueViolet',
// stroke: 'red',
// 'text-shape': {//文本图形
// fill: 'yellow'
// }
// },
// }
});
// 读取(加载)数据
graph.data(data);
// 渲染图
graph.render();
//通过事件来触发状态样式
graph.on('node:mouseenter', (evt) => {
const { item } = evt;
graph.setItemState(item, 'stateName1', true);
// graph.setItemState(item, 'stateName2', true);
});
graph.on('node:mouseleave', (evt) => {
const { item } = evt;
graph.setItemState(item, 'stateName1', false);
// graph.setItemState(item, 'stateName2', false);
});
鼠标移入效果➡
方式三配置方式: 使用这种方式可以为自定义的节点/边类型配置 state 样式。
G6.registerNode('customShape', {
// 自定义节点时的配置
options: {
size: 60,
style: {
lineWidth: 1
},
stateStyles: {
// ... 见上方例子,同方式一配置
}
}
}
自定义节点
G6 提供了一系列内置节点,包括 circle、rect、diamond、triangle、star、image、modelRect。若内置节点无法满足需求,用户还可以通过
G6.registerNode(typeName: string, nodeDefinition: object, extendedTypeName?: string)进行自定义节点,方便用户开发更加定制化的节点,包括含有复杂图形的节点、复杂交互的节点、带有动画的节点等。
其参数:
typeName:该新节点类型名称;extendedTypeName:被继承的节点类型,可以是内置节点类型名,也可以是其他自定义节点的类型名。extendedTypeName未指定时代表不继承其他类型的节点;nodeDefinition:该新节点类型的定义,其中必要函数详见 自定义机制 API。当有extendedTypeName时,没被复写的函数将会继承extendedTypeName的定义。
G6.registerNode(
'nodeName',
{
options: {
style: {},
stateStyles: {
hover: {},
selected: {},
},
},
/**
* 绘制节点,包含文本
* @param {Object} cfg 节点的配置项
* @param {G.Group} group 图形分组,节点中图形对象的容器
* @return {G.Shape} 返回一个绘制的图形作为 keyShape,通过 node.get('keyShape') 可以获取。
* 关于 keyShape 可参考文档 核心概念-节点/边/Combo-图形 Shape 与 keyShape
*/
draw(cfg, group) {},
/**
* 绘制后的附加操作,默认没有任何操作
* @param {Object} cfg 节点的配置项
* @param {G.Group} group 图形分组,节点中图形对象的容器
*/
afterDraw(cfg, group) {},
/**
* 更新节点,包含文本
* @override
* @param {Object} cfg 节点的配置项
* @param {Node} node 节点
*/
update(cfg, node) {},
/**
* 更新节点后的操作,一般同 afterDraw 配合使用
* @override
* @param {Object} cfg 节点的配置项
* @param {Node} node 节点
*/
afterUpdate(cfg, node) {},
/**
* 响应节点的状态变化。
* 在需要使用动画来响应状态变化时需要被复写,其他样式的响应参见下文提及的 [配置状态样式] 文档
* @param {String} name 状态名称
* @param {Object} value 状态值
* @param {Node} node 节点
*/
setState(name, value, node) {},
/**
* 获取锚点(相关边的连入点)
* @param {Object} cfg 节点的配置项
* @return {Array|null} 锚点(相关边的连入点)的数组,如果为 null,则没有控制点
*/
getAnchorPoints(cfg) {},
},
// 继承内置节点类型的名字,例如基类 'single-node',或 'circle', 'rect' 等
// 当不指定该参数则代表不继承任何内置节点类型
extendedNodeName,
)