使用 antV G6 技术在 Vue 中拖放拓扑图
G6 是一个图可视化引擎。它提供了图的绘制、布局、分析、交互、动画等图可视化的基础能力。旨在让关系变得透明,简单。让用户获得关系数据的 Insight。
安装 & 引用
在项目中引入 G6 有以下两种方式:NPM 引入;CDN 引入。
1 在项目中使用 NPM 包引入
Step 1: 使用命令行在项目目录下执行以下命令:
npm install --save @antv/g6
Step 2: 在需要用的 G6 的 JS 文件中导入:
import G6 from '@antv/g6';
2 在 HTML 中使用 CDN 引入
// version <= 3.2
<script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.g6-{$version}/build/g6.js"></script>
// version >= 3.3
<script src="https://gw.alipayobjects.com/os/lib/antv/g6/{$version}/dist/g6.min.js"></script>
// version >= 4.0
<script src="https://gw.alipayobjects.com/os/lib/antv/g6/4.3.11/dist/g6.min.js"></script>
⚠️ 注意:
- 在
{$version}
中填写版本号,例如3.7.1
; - 最新版可以在 NPM 查看最新版本及版本号;
- 详情参考 Github 分支:github.com/antvis/g6/t…。
快速试用
创建一个 G6 的关系图仅需要下面几个步骤:
- 创建关系图的 HTML 容器;
- 数据准备;
- 创建关系图;
- 配置数据源,渲染。
Step 1 创建容器
需要在 HTML 中创建一个用于容纳 G6 绘制的图的容器,通常为 div
标签。G6 在绘制时会在该容器下追加 canvas
标签,然后将图绘制在其中。
<div id="mountNode"></div>
Step 2 数据准备
引入 G6 的数据源为 JSON 格式的对象。该对象中需要有节点(nodes
)和边(edges
)字段,分别用数组表示:
const data = {
// 点集
nodes: [
{
id: 'node1', // String,该节点存在则必须,节点的唯一标识
x: 100, // Number,可选,节点位置的 x 值
y: 200, // Number,可选,节点位置的 y 值
},
{
id: 'node2', // String,该节点存在则必须,节点的唯一标识
x: 300, // Number,可选,节点位置的 x 值
y: 200, // Number,可选,节点位置的 y 值
},
],
// 边集
edges: [
{
source: 'node1', // String,必须,起始点 id
target: 'node2', // String,必须,目标点 id
},
],
};
注意
-
nodes
数组中包含节点对象。每个节点对象中唯一的、必要的id
以标识不同的节点,x
、y
指定该节点的位置; -
edges
数组中包含边对象。source
和target
是每条边的必要属性,分别代表了该边的起始点id
与 目标点id
。 - 点和边的其他属性参见链接:内置节点 和 内置边。
Step 3 创建关系图
创建关系图(实例化)时,至少需要为图设置容器、宽和高。
以下是vue+typescript下的创建实例
// 创建Graph实例
const graphDom: HTMLDivElement = this.$refs.mountNode as HTMLDivElement;
this.graph = new G6.Graph({
container: graphDom, // 图的 DOM 容器
fitView: true, // 是否开启画布自适应。开启后图自动适配画布大小。
fitViewPadding: 100, // 画布的padding值
modes: {
default: ['drag-canvas', 'zoom-canvas', 'drag-node'], // 允许拖拽画布、放缩画布、拖拽节点、设置高亮
}
});
this.graph.read(this.graphData); // 接收数据,并进行渲染,read 方法的功能相当于 data 和 render 方法的结合。
Step 4 配置数据源,渲染
graph.data(data); // 初始化的图数据,是一个包括 nodes 数组和 edges 数组的对象
graph.render(); // 渲染图
节点总览
G6 的内置节点包括 circle,rect,ellipse,diamond,triangle,star,image,modelRect,donut(v4.2.5 起支持)。这些内置节点的默认样式分别如下图所示。
具体参考官网教程配置节点总览 | G6 (antv.vision),这里不再做阐述
边总览
阅读时间约 15 分钟
G6 提供了 9 种内置边:
- line:直线,不支持控制点;
- polyline:折线,支持多个控制点;
- arc:圆弧线;
- quadratic:二阶贝塞尔曲线;
- cubic:三阶贝塞尔曲线;
- cubic-vertical:垂直方向的三阶贝塞尔曲线,不考虑用户从外部传入的控制点;
- cubic-horizontal:水平方向的三阶贝塞尔曲线,不考虑用户从外部传入的控制点;
- loop:自环。
这些内置边的默认样式分别如下图所示。
G6 中的各个内置边类型、内置边的通用属性、配置方法。各类型边详细配置项及配置方法参考官网边总览 | G6 (antv.vision)。
特殊交互
项目中需要实现一个节点hover时候弹窗显示该节点详情信息,如下图所示
代码如下
// 自定义tooltip插件,鼠标悬停显示详情信息
const tooltip = new G6.Tooltip({
offsetX: -80,
offsetY: -60,
getContent(e: any) {
const outDiv = document.createElement('div');
outDiv.style.width = '180px';
outDiv.innerHTML = `<ul>
<li>${e.item?.getModel().label}</li>
</ul>`
return outDiv;
},
itemTypes: ['node']
});
// 自定义tooltip插件,鼠标悬停显示详情信息
this.graph = new G6.Graph({
container: graphDom, // 图的 DOM 容器
plugins: [tooltip], // 自定义tooltip插件
layout: {
type: 'dagre',
rankdir: 'LR',
linkDistance: 150,
},
});
基础事件 Event | G6 (gitee.io)
使用方法示例如下
// 监听鼠标点击节点
this.graph.on('node:click', (e) => {
const nodeItem: any = e.item;
console.log('node:click', nodeItem.getModel());
});
事件汇总,如下
-
node:click 鼠标左键单击节点时触发
-
node:dblclick 鼠标双击左键节点时触发,同时会触发两次 node:click
-
node:mouseenter 鼠标移入节点时触发
-
node:mousemove 鼠标在节点内部移到时不断触发
-
node:mouseout 鼠标移出节点后触发
-
node:mouseover 鼠标移入节点上方时触发
-
node:mouseleave 鼠标移出节点时触发
-
node:mousedown 鼠标按钮在节点上按下(左键或者右键)时触发
-
node:mouseup 节点上按下的鼠标按钮被释放弹起时触发
-
node:dragstart 当节点开始被拖拽时触发,此事件作用在被拖拽节点上
-
node:drag 当节点在拖动过程中时触发,此事件作用于被拖拽节点上
-
node:dragend 当拖拽完成后触发,此事件作用在被拖拽节点上
-
node:dragenter 当拖拽节点进入目标元素的时候触发,此事件作用在目标元素上
-
node:dragleave 当拖拽节点离开目标元素的时候触发,此事件作用在目标元素上
-
node:dragover 当拖拽节点在另一目标元素上移动时触发,此事件作用在目标元素上
-
node:drop 被拖拽的节点在目标元素上同时鼠标放开触发,此事件作用在目标元素上
-
node:contextmenu 用户在节点上右击鼠标时触发并打开右键菜单
点击边触发指定边的骚操作
比如要实现下图的效果
仔细查阅了antV G6的整个API文档说明,终于让我找到了这么一个方法
通过graph.getEdges()方法,拿到所有边实例,然后进行遍历,比对当前点击的边是否同属于同一个路径关系(父级ID)的边,是则选中,否则清除选中效果
// 监听鼠标点击边
this.graph.on('edge:click', (e) => {
const currentItem: any = e.item.getModel();
// console.log('edge:click', currentItem);
this.graph.setAutoPaint(false);
const edges = this.graph.getEdges();
edges.map(item => {
const edgeItem = item.getModel();
const intersect = Common.intersect(edgeItem.pathId, currentItem.pathId);
if (intersect.length > 0) {
this.graph.setItemState(item, 'selected', true);
} else {
this.graph.setItemState(item, 'selected', false);
};
})
this.graph.paint();
this.graph.setAutoPaint(true);
});
Common.intersect方法
/**
* 比对两个数组返回交集
* @param arr1 数组1
* @param arr2 数组2
* @returns 返回两数组的交集
*/
intersect: (arr1: number[], arr2: number[]) => {
return arr1.filter(x => new Set(arr2).has(x));
}
展示所需数据格式如下
private graphData: any = {
// 点集
nodes: [
{
id: '1', // String,该节点存在则必须,节点的唯一标识
// x: 100, // Number,可选,节点位置的 x 值
// y: 200, // Number,可选,节点位置的 y 值
label: 'S1',
style: { // 包裹样式属性的字段 style 与其他属性在数据结构上并行
fill: '#16e473', // 样式属性,元素的填充色
stroke: '#888', // 样式属性,元素的描边色
lineWidth: 1, // 节点描边粗细
// ... // 其他样式属性
},
icon: {
show: true,
img: '...',
// text: '...', 使用 iconfont
width: 20,
height: 20,
},
},
{
id: '8', // String,该节点存在则必须,节点的唯一标识
label: 'B', // 节点文本
labelCfg: { // 标签配置属性
position: 'bottom', // 标签的属性,标签在元素中的位置
style: { // 包裹标签样式属性的字段 style 与标签其他属性在数据结构上并行
fontSize: 12, // 标签的样式属性,文字字体大小
fill: '#ff578b'
// ... // 标签的其他样式属性
}
},
},
{
id: '2', // String,该节点存在则必须,节点的唯一标识
label: 'A',
},
{
id: '3', // String,该节点存在则必须,节点的唯一标识
label: 'Z1',
labelCfg: { // 标签配置属性
position: 'bottom', // 标签的属性,标签在元素中的位置
style: { // 包裹标签样式属性的字段 style 与标签其他属性在数据结构上并行
fontSize: 12, // 标签的样式属性,文字字体大小
fill: '#00f3f6'
// ... // 标签的其他样式属性
}
},
},
{
id: '4', // String,该节点存在则必须,节点的唯一标识
label: 'Z2', // 节点文本
labelCfg: { // 标签配置属性
position: 'bottom', // 标签的属性,标签在元素中的位置
style: { // 包裹标签样式属性的字段 style 与标签其他属性在数据结构上并行
fontSize: 12, // 标签的样式属性,文字字体大小
fill: 'red'
// ... // 标签的其他样式属性
}
},
},
{
id: '7', // String,该节点存在则必须,节点的唯一标识
label: 'Z', // 节点文本
labelCfg: { // 标签配置属性
position: 'bottom', // 标签的属性,标签在元素中的位置
style: { // 包裹标签样式属性的字段 style 与标签其他属性在数据结构上并行
fontSize: 12, // 标签的样式属性,文字字体大小
fill: '#ff578b'
// ... // 标签的其他样式属性
}
},
},
{
id: '5', // String,该节点存在则必须,节点的唯一标识
label: 'S2', // 节点文本
labelCfg: { // 标签配置属性
position: 'bottom', // 标签的属性,标签在元素中的位置
style: { // 包裹标签样式属性的字段 style 与标签其他属性在数据结构上并行
fontSize: 12 // 标签的样式属性,文字字体大小
// ... // 标签的其他样式属性
}
},
},
{
id: '6', // String,该节点存在则必须,节点的唯一标识
label: 'Y', // 节点文本
labelCfg: { // 标签配置属性
position: 'bottom', // 标签的属性,标签在元素中的位置
style: { // 包裹标签样式属性的字段 style 与标签其他属性在数据结构上并行
fontSize: 12 // 标签的样式属性,文字字体大小
// ... // 标签的其他样式属性
}
},
}
],
// 边集
edges: [
{
pathId: [2],
source: '1', // String,必须,起始点 id
target: '2', // String,必须,目标点 id
label: 'S1-A', // 节点文本
},
{
pathId: [1,3],
source: '8', // String,必须,起始点 id
target: '6', // String,必须,目标点 id
},
{
pathId: [2],
source: '8', // String,必须,起始点 id
target: '3', // String,必须,目标点 id
labelCfg: {
autoRotate: true, // 使文本随边旋转
style: {
stroke: 'white', // 给文本添加白边和白色背景
lineWidth: 5, // 文本白边粗细
fill: '#722ed1', // 文本颜色
}
},
},
{
pathId: [3],
source: '3', // String,必须,起始点 id
target: '4', // String,必须,目标点 id
},
{
pathId: [2,3],
source: '3', // String,必须,起始点 id
target: '6', // String,必须,目标点 id
},
{
pathId: [1,2],
source: '6', // String,必须,起始点 id
target: '7', // String,必须,目标点 id
},
{
pathId: [1,2,3],
source: '2', // String,必须,起始点 id
target: '8', // String,必须,目标点 id
label: 'A-B', // 节点文本
// color: '#722ed1', // 边颜色
labelCfg: {
position: 'end',
refY: -10,
}
},
{
pathId: [1,3],
source: '5', // String,必须,起始点 id
target: '2', // String,必须,目标点 id
label: 'S2-A', // 节点文本
labelCfg: {
refY: -10,
}
}
],
};