Vue中使用antV G6技术实现可拖拽拓扑图
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, } } ], };
作者:飞越在掘金
链接:https://juejin.cn/post/7063248501441822728