Vue项目中使用AntV G6绘制自适应图谱
一、需求
-
需求1:
Vue3.x
项目下使用AntV G6
绘制图谱 -
需求2:图谱节点为两个
IP
地址,节点间存在多条连线情况 -
需求3:鼠标悬浮到节点上方时,高亮当前节点并出现
tooltip
气泡提示,展示相关信息 -
需求4:点击图谱连线后需要高亮关联节点和当前连线
-
需求5:图谱大小需要保证自适应(这里可以是根据窗口自适应,也可以是可调整大小的弹窗
DOM
内的自适应,后续会分开实现)
二、实现
1.初始Vue3.x
的项目可以看之前的博客:通过vite搭建Vue3.x项目,初始化项目后需要安装AntV G6
的依赖,执行如下命令:
npm install --save @antv/g6
2.安装G6
后在需要使用的页面进行导入,在创建好容器后利用G6
进行实例化创建一个画布,后续的图谱绘制都将在画布内进行,目前为止需求1部分已经完成了,代码如下:
<template>
<div
class="home"
id="container"
>
</div>
</template>
<script setup>
import G6 from '@antv/g6';
import { onMounted, ref } from '@vue/runtime-core';
// 图谱实例化对象
const graph = ref(null);
/* 挂载钩子 */
onMounted(() => {
initGraph();
});
/* 初始化图谱 */
const initGraph = () => {
graph.value = new G6.Graph({
container: 'container', // String | HTMLElement,必须,容器 id 或容器本身
width: document.getElementById('container').clientWidth, // Number,必须,图的宽度
height: document.getElementById('container').clientHeight, // Number,必须,图的高度
});
}
</script>
<style lang="less" scoped>
.home {
width: 100%;
height: 100%;
position: relative;
}
</style>
3.为了方便后续步骤的进行,这里先造一些节点和连线的数据,大致仿照 G6官网示例中的数据,根据需求2在模拟数据的边集中添加两个节点存在多条连线的情况(实际场景下根据实际数据做处理即可),如下:
<script setup>
...
// 模拟数据
const data = {
// 点集
nodes: [
{
id: '10.0.0.1', // String,该节点存在则必须,节点的唯一标识
label: '节点1',
},
{
id: '192.168.1.1',
label: '节点2',
},
{
id: '192.168.1.2',
label: '节点3',
},
{
id: '10.0.0.2',
label: '节点4',
},
],
// 边集
edges: [
{
source: '10.0.0.1', // String,必须,起始点 id
target: '192.168.1.1', // String,必须,目标点 id
label: '节点1->节点2',
},
{
source: '10.0.0.1',
target: '192.168.1.1',
label: '节点1->节点2',
},
{
source: '192.168.1.1',
target: '10.0.0.1',
label: '节点2->节点1',
},
{
source: '192.168.1.1',
target: '192.168.1.2',
label: '节点2->节点3',
},
{
source: '192.168.1.2',
target: '192.168.1.2',
label: '节点3->节点3',
},
{
source: '192.168.1.2',
target: '10.0.0.2',
label: '节点3->节点4',
},
{
source: '10.0.0.2',
target: '192.168.1.2',
label: '节点4->节点3',
},
{
source: '10.0.0.2',
target: '10.0.0.1',
label: '节点4->节点1',
},
],
};
...
</script>
4.创建好模拟数据后继续完善画布的配置,这时可以参考官网的Api
对画布、节点样式、连线样式、气泡提示tooltip
进行一些配置
- 这里需要注意的是,拖动画布或缩放画布时有概率出现**残影(拖影)**的问题,需要单独处理
- 利用
G6
的工具函数将连线类型设置为贝塞尔曲线就能保证两个节点之间多条连线不会出现重叠的问题,这样就实现了需求2的部分 - 需求3则利用
G6
实例化一个Tooltip
后,再利用plugins
字段配置到到画布内,当鼠标移入节点时会出现一个tooltip
显示节点的IP
地址(id
),但是移出节点后tooltip
并不会消失,需要在移出节点的事件中单独处理一次 - 需求4可以给节点添加一个点击事件,根据点击的节点信息找到关联的两个节点的
IP
地址(id
)并进行高亮处理 - 代码如下:
<script setup>
...
/* 初始化图谱 */
const initGraph = () => {
// 节点添加提示框
const tooltip = new G6.Tooltip({
offsetX: 10, // tooltip 的 x 方向偏移值,需要考虑父级容器的 padding
offsetY: 20, // tooltip 的 y 方向偏移值,需要考虑父级容器的 padding
getContent(e) {
const outDiv = document.createElement('div');
outDiv.style.width = '180px';
outDiv.innerHTML = `
<ul>
<li>节点IP:${e.item.getModel().id}</li>
</ul>`;
return outDiv;
},
itemTypes: ['node'] // 给node添加tooltip
});
graph.value = new G6.Graph({
container: 'container', // String | HTMLElement,必须,容器 id 或容器本身
width: document.getElementById('container').clientWidth, // Number,必须,图的宽度
height: document.getElementById('container').clientHeight, // Number,必须,图的高度
fitCenter: true, // 是否平移图使其中心对齐到画布中心
plugins: [tooltip], // 添加tooltip
// 画布配置
modes: {
default: ['drag-canvas', 'zoom-canvas', 'drag-node'], // 允许拖拽画布、放缩画布、拖拽节点
},
// 基本配置
layout: {
type: 'force', // 指定为力导向布局
preventOverlap: true, // 防止节点重叠
linkDistance: 180, // 指定边距离为150
nodeSpacing: 85, // 防止重叠时节点边缘间距的最小值
alpha: 1, // 当前的迭代收敛阈值
alphaDecay: 0.05, // 迭代阈值的衰减率。范围 [0, 1]
},
// 默认节点配置
defaultNode: {
type: 'rect', // 节点类型
size: [120, 46],
style: {
fill: '#F6FCFE', // 填充色
stroke: '#2EA1FF', // 节点描边颜色
shadowColor: 'rgba(78,89,105,0.3)', // 阴影颜色
lineWidth: 1, // 描边宽度
radius: 3, // 圆角
shadowBlur: 2, // 阴影大小
},
// 文本配置
labelCfg: {
position: 'center',
style: {
fontSize: 14,
fill: '#0282FF',
fontWeight: 400,
},
},
},
// 默认边配置
defaultEdge: {
type: 'loop', // 设置边默认为“自环”类型
style: {
stroke: '#2EA1FF',
lineWidth: 1,
// 边添加箭头
endArrow: {
path: G6.Arrow.triangle(4, 4, 2),
d: 2,
fill: '#2EA1FF',
}
},
labelCfg: {
autoRotate: true,
refY: 12,
style: {
fontSize: 12,
fill: '#1D2129',
}
},
},
// 边状态样式
edgeStateStyles: {
selected: {
stroke: '#0282FF',
shadowBlur: 0,
'text-shape': {
fill: "#0282FF",
fontWeight: 600,
}
}
},
// 节点状态样式
nodeStateStyles: {
// 选中后样式
selected: {
fill: '#0282FF', // 填充色
stroke: '#0282FF', // 节点描边颜色
lineWidth: 1, // 描边宽度
shadowColor: 'rgba(0,102,210,0.5)',
'text-shape': {
fill: "#ffffff"
}
},
// 悬浮后样式
active: {
fill: '#CDEEFF', // 填充色
stroke: '#2EA1FF', // 节点描边颜色
lineWidth: 1, // 描边宽度
shadowColor: 'rgba(78,89,105,0.3)',
'text-shape': {
fill: "#0282FF",
fontWeight: 500,
}
}
}
});
// 添加节点间多条连线,贝塞尔曲线
G6.Util.processParallelEdges(data.edges, 65);
// 解决拖动产生残影问题
graph.value.get('canvas').set('localRefresh', false);
// 边添加点击事件
graph.value.on('edge:click', (evt) => {
const { item } = evt;
graph.value.getEdges().forEach((edge) => {
graph.value.clearItemStates(edge);
});
graph.value.getNodes().forEach((node) => {
graph.value.clearItemStates(node);
});
graph.value.setItemState(item, 'selected', true);
// 高亮关联节点
graph.value.setItemState(item['_cfg'].source, 'selected', true);
graph.value.setItemState(item['_cfg'].target, 'selected', true);
});
// 节点悬浮高亮
graph.value.on('node:mouseover', (e) => {
graph.value.setItemState(e.item, 'active', true);
});
// 节点鼠标移出后使tooltip消失并取消节点悬浮高亮
graph.value.on('node:mouseout', (e) => {
document.getElementsByClassName('g6-component-tooltip')[0].style.display = 'none';
graph.value.setItemState(e.item, 'active', false);
});
graph.value.data(data);
graph.value.render();
}
</script>
<style lang="less" scoped>
.home {
width: 100%;
height: 100%;
position: relative;
}
</style>
<style lang="less">
// g6的tooltip气泡提示样式需要写进不含scoped的style中,否则不生效
.g6-component-tooltip {
background-color: rgba(0, 0, 0, 0.7);
padding: 7px 17px;
box-shadow: none;
}
.g6-component-tooltip ul li {
font-size: 12px;
font-weight: 400;
color: #ffffff;
line-height: 19px;
}
</style>
5.需求5要分成两种情况来说
- 第一种情况是窗口大小改变时影响到画布,可以直接通过监听窗口
window
的resize
来对画布进行自适应的操作,代码如下:
<script setup>
...
graph.value.render();
// 监听window的resize事件
window.addEventListener('resize', () => {
// 利用changeSize()事件使画布自适应
graph.value.changeSize(document.getElementById('container').clientWidth, document.getElementById('container').clientHeight);
// 将图谱移动到画布正中间
graph.value.fitCenter();
...
</script>
-
第二种情况就稍微复杂一些,笔者实际中的业务是在一个可以拖动改变大小的弹窗中实现画布自适应;同理这里可以是收起导航菜单影响到画布的DOM亦或是改变布局等方式影响了画布的DOM大小发生改变,总之就是画布所在的DOM元素大小发生改变时的自适应,因为不是
window
的大小改变,所以不能使用第一种监听window
的resize
方式来处理,笔者这里使用的是一个js
库element-resize-detector
用来监听画布的DOM大小是否改变,步骤如下:- 安装
element-resize-detector
库,详情:https://www.npmjs.com/package/element-resize-detector
npm install --save element-resize-detector
- 使用
element-resize-detector
监听目标DOM大小是否发生改变
<script setup> // 引入 import elementResizeDetectorMaker from 'element-resize-detector'; ... graph.value.render(); // 使用 let erd = elementResizeDetectorMaker(); // 监听目标DOM的大小是否改变,以container为例 erd.listenTo(document.getElementById("container"), (element) => { let width = element.clientWidth; let height = element.clientHeight; // 利用changeSize()事件使画布自适应 graph.value.changeSize(width, height); // 将图谱移动到画布正中间 graph.value.fitCenter(); }); ... </script>
- 安装
6.最后在界面卸载时需要释放一次资源,将画布资源进行释放,代码如下:
<script setup>
...
/* 卸载钩子 */
onUnmounted(() => {
if (graph.value) {
// 清空数据(可省略)
graph.value.clear();
// 销毁画布
graph.value.destroy();
// 将画布实例对象置空
graph.value = null;
}
});
...
</script>
7.最终效果如下图,方便学习代码demo会放到Gitee
上,笔者根据需求5拆分出了两个示例,方便针对不同场景做不同的处理,仓库地址:g6_learning: Vue3+Elemet Plus+G6绘制自适应图谱demo (gitee.com),CSDN资源地址:g6_learning: Vue3+Elemet Plus+G6绘制自适应图谱demo(csdn)