一个图可视化引擎。它提供了图的绘制、布局、分析、交互、动画等基础的图可视化能力。用于前端绘制拓扑结构
一、安装&引用
STEP1:安装
npm install --save @antv/g6
STEP2:导入
import G6 from '@antv/g6';
二、使用
Step1 创建容器
<template>
<div>
<div id="container"></div>
</div>
</template>
三、使用总结
1.TreeGraph 树图
1)子树自动收缩问题
需求描述:从接口请求到数据之后,把数据追加到图表上形成链式结构;
数据结构:
const newData = [
{
uuid: 'test1'
},
{
uuid: 'test2'
}
]
问题1:追加数据之后子树会自动收缩起来,可能是因为id没有获取到
根据此数据结构需使用updateChildren(newData, 父节点)来追加数据
因为树图的数据结构需要有id字段且源数据结构是uuid,所以在配置时使用getId来处理id
const graph = new G6.TreeGraph({
container: container as HTMLElement, // String | HTMLElement,必须,在 Step 1 中创建的容器 id 或容器本身
width: width,
height: height,
linkCenter: true,
plugins: [menu, tooltip],
// 画布交互配置
modes: {
default: [
{
type: 'collapse-expand',
trigger: 'dblclick',
},
'drag-canvas', // 拖拽画布
'zoom-canvas', // 缩放画布
'activate-relations', // 激活关系
],
},
// 配置节点的属性
defaultNode: {
type: 'image-node',
size: 30,
// 指定边连入节点的连接点的位置,可以为空
anchorPoints: [
[0, 0.5],
[1, 0.5],
],
},
defaultEdge: {
type: 'cubic-horizontal', // 默认边的类型为水平方向的三次曲线
},
layout: {
type: 'compactBox', // 紧凑树
direction: 'LR', // 根节点在左,往右布局 H / V / LR / RL / TB / BT
getHGap: function getHGap() {
return 50; // 每一层节点之间的间距
},
getVGap: function getVGap() {
return 10; // 每个节点的垂直间距
},
getId: function getId(node: any) {
return node.uuid; // 节点 id 的回调函数, 处理数据的id
},
},
});
问题2:删除节点后再增加相同节点,子树不会展开但实际上已添加进去了;
删除节点后,可以使用setTimeout来延迟添加节点
2)自定义节点的label不显示
如果需要让节点显示label,有两种方式:
1.返回的数据结构中需有 label 字段,自定义节点中配置 text,配置fill颜色,此处获取的是默认节点样式
//图表初始化时配置节点的默认属性
defaultNode: {
type: 'image-node',
size: 30,
labelCfg: {
style: {
fill: '#333',
},
},
anchorPoints: [
[0, 0.5],
[1, 0.5],
],
},
// 自定义节点中的配置
group.addShape('text', {
attrs: {
x: size[0] / 2,
y: size[1] + 15,
textAlign: 'center', // 文本在图形中的对齐方式
textBaseline: 'middle', // 当前文本基线
text: cfg.label,
...labelCfg?.style,
},
name: 'text-shape',
});
2.返回的数据结构中无label字段,需要在graph.node中配置label,然后在自定义节点中做相应配置
自定义节点文件:
import G6 from '@antv/g6';
import _ from '@lodash';
export function initImageNode(imageUrl: string, name = 'node') {
const img = new Image();
img.src = imageUrl;
const _name = `image-${name}`;
img.onload = () => {
G6.registerNode(
// 自定义节点
_name,
{
draw(cfg: any, group) {
let { size } = cfg;
const { labelCfg } = cfg;
if (!size) {
size = [40, 40];
}
if (_.isNumber(size)) {
size = [size, size];
}
group.addShape('text', {
attrs: {
x: size[0] / 2,
y: size[1] + 15,
textAlign: 'center', // 文本在图形中的对齐方式
textBaseline: 'middle', // 当前文本基线
text: cfg.label,
...labelCfg?.style,
},
name: 'text-shape',
});
return group.addShape('image', {
attrs: {
img,
width: size[0],
height: size[1],
...cfg.style,
},
draggable: true,
name: 'image-shape',
});
},
update(cfg, node) {},
options: {
stateStyles: {
active: {
// 活跃
opacity: 1,
'text-shape': {
opacity: 1,
fill: 'rgb(95, 149, 255)',
},
},
inactive: {
opacity: 0.3,
'text-shape': {
opacity: 0.3,
fill: '#333',
},
},
},
},
},
'single-node',
);
};
}
遇到的问题:
数据结构中已有label属性,且自定义节点中已对“text”做了配置。
问题1:只有节点高亮时才会显示active状态下的样式,且只有该节点更新之后才会显示label。
因为没有设置节点的文字颜色;
解决方法:
在graph.node()或defaultNode:{}中配置fill,然后自定义节点配置中获取文字样式
问题2:节点文字能正常显示之后,请求数据追加某个节点的子树数据之后该节点会显示两个label
解决方法:
在自定义节点中添加“update(cfg, node) {}”方法更新节点
3)自定义节点svg图标,根据节点类型渲染不同的图标样式
方式一:
在自定义节点时,动态获取节点图标的src,根据节点类型去修改图标路径
let nodeType = '';
if (['targetIp', 'sourceIp'].includes(node.fieldsKey)) {
nodeType = ipIcon;
} else if (node.fieldsKey === 'targetPort') {
nodeType = portIcon;
} else if (node.fieldsKey === 'protocols') {
nodeType = protocolIcon;
}
return group.addShape('image', {
attrs: {
img: nodeType,
width: size[0],
height: size[1],
...cfg.style,
},
draggable: true,
name: 'image-shape',
});
问题:
如果只自定义一种节点样式,然后根据节点类型去获取不同图标的路径(修改img的src),当鼠标经过节点时会重复加载图标文件;
方式二:
自定义多个节点样式,修改节点的类型
export function initImageNode(imageUrl: string, name= 'node') {
const img = new Image();
img.src = imageUrl;
const _name = `image-${name}`; // 不同的节点类型对应不同的图标路径
img.onload = () => {
G6.registerNode(
// 自定义节点
_name,
{
draw(cfg: any, group) {
let { size } = cfg;
const { labelCfg } = cfg;
if (!size) {
size = [40, 40];
}
if (_.isNumber(size)) {
size = [size, size];
}
group.addShape('text', {
attrs: {
x: size[0] / 2,
y: size[1] + 15,
textAlign: 'center', // 文本在图形中的对齐方式
textBaseline: 'middle', // 当前文本基线
text: cfg.label,
...labelCfg?.style,
},
name: 'text-shape',
});
return group.addShape('image', {
attrs: {
img,
width: size[0],
height: size[1],
...cfg.style,
},
draggable: true,
name: 'image-shape',
});
},
update(cfg, node) {},
options: {},
},
'single-node',
);
}
}
#导入svg文件
# 导入四种节点图标svg文件
import nodeIcon from '@/assets/images/node.svg';
import portIcon from '@/assets/images/port.svg';
import protocolIcon from '@/assets/images/protocol.svg';
import ipIcon from '@/assets/images/ip.svg';
#自定义多个不同的节点图标样式
# 加载不同图表的自定义节点样式
onBeforeMount(async () => {
await initImageNode(nodeIcon, 'node');
await initImageNode(ipIcon, 'ip');
await initImageNode(portIcon, 'port');
await initImageNode(protocolIcon, 'protocol');
});
onMounted(async () => {
// 初始化图表
nextTick(() => {
initGraph();
});
});
# 在初始化图表方法中定义节点样式
graph.node(function (node: any) {
const id = node.id;
let nodeType = '';
if (['targetIp', 'sourceIp'].includes(node.fieldsKey)) {
nodeType = 'image-ip';
} else if (node.fieldsKey === 'targetPort') {
nodeType = 'image-port';
} else if (node.fieldsKey === 'protocols') {
nodeType = 'image-protocol';
}
return {
id: id,
type: nodeType || 'image-node',
extraData: {
...node,
},
};
});
4) 节点高亮效果
鼠标移入节点时,设置相关的节点和边的高亮效果,并添加边信息
鼠标移出节点时,取消边信息的展示,取消高亮状态
1.对图表的画布交互配置中,设置 “activate-relations”
// 画布交互配置
modes: {
default: [
'drag-canvas', // 拖拽画布
'zoom-canvas', // 缩放画布
'activate-relations', // 激活关系
],
},
2.单独处理鼠标移入移出节点效果
# 鼠标进入节点:高亮相关边和节点,显示边信息
graph.on('node:mouseenter', (e: G6GraphEvent) => {
// 根据节点id获取当前节点所有相关边
const model = e.item.getModel() as any; // 节点模型
if (!model.extraData) return;
const inEdgeLabel = state.topologyQuery.find(
(item) => item.key === (model?.extraData as NodeType)?.fieldsKey,
)?.label;
const node = state.graph.findById(model.id);
const inEdges = node.getInEdges(); // 入边
const outEdges = node.getOutEdges(); // 出边
// 给入边添加label:
inEdges.forEach((edge: any) => {
const edgeModel = edge.getModel();
const item = graph.findById(edgeModel.id);
graph.updateItem(item, {
label: inEdgeLabel,
});
});
/**
* 给出边添加label:
* 根节点取usedQuery[0], 其余节点找子节点的fieldsKey, 如果没有则取当前查询条件state.currentQuery
*/
// 匹配到当前节点的子节点元素实例
const item = state.graph.find('node', (n: any) => {
return n.get('model').pid === model.id;
});
const sonFieldKey = item?.getModel()?.fieldsKey;
const outData = sonFieldKey ?? state.currentQuery.condition;
const outCondition = model.id === 'default' ? state.usedQuery?.[0]?.condition : outData;
const outEdgeLabel = state.topologyQuery.find((i) => i.key === outCondition)?.label;
outEdges.forEach((edge: any) => {
const edgeModel = edge.getModel();
const it = graph.findById(edgeModel.id);
graph.updateItem(it, {
label: outEdgeLabel,
});
});
const nodes = graph.getNodes(); // 所有的节点
const neighbors = state.graph.getNeighbors(model.id).map((n: any) => n.getModel().id);
/**
* 设置相关节点状态:acitve为true
* 设置不相关节点状态:inactive为true, active为false
*/
_.forEach(nodes, (n) => {
if ([...neighbors, model.id].includes(n.getModel().id)) {
graph.setItemState(n, 'active', true); // 设置相关节点的 active 状态为 true
} else {
graph.setItemState(n, 'active', false); // 设置不相关节点的 inactive 状态为 false (透明)
graph.setItemState(n, 'inactive', true);
}
});
});
# 鼠标移出节点时,取消边信息的展示,取消高亮状态
graph.on('node:mouseleave', (e: G6GraphEvent) => {
const nodeElement = document.getElementsByClassName('g6-component-tooltip');
(nodeElement[0] as HTMLElement).style.display = 'none'; // 不展示tooltip
graph.setItemState(e.item, 'hover', false); // 设置当前节点的 hover 状态为 false
// 取消显示边信息
const model = e.item.getModel();
const node = state.graph.findById(model.id);
const edges = node.getEdges();
// 取消边的label
edges.forEach((edge: Edge) => {
const edgeModel = edge?.getModel();
const item = graph.findById(edgeModel.id as string);
graph.updateItem(item, {
label: '',
});
});
// 取消节点的active状态
const nodes = graph.getNodes();
_.forEach(nodes, (n) => {
graph.setItemState(n, 'active', false);
graph.setItemState(n, 'inactive', false);
});
_.forEach(edges, (edge) => {
graph.setItemState(edge, 'active', false);
graph.setItemState(edge, 'inactive', false);
});
});
2.设置图表工具栏(放大、缩小……)
# 图表部分
<div v-if="state.graphData?.length" class="graph_main flex-1">
<div ref="graphRef" class="h-100% w-100%" id="graph-wrap"></div>
</div>
<el-empty v-else class="h-100% w-100% flex-1" :description="t('common.noData')" />
# 工具栏部分
<div id="flow-toolbar-wrap" v-if="state.graphData?.length">
<div class="flow-toolbar">
<el-link class="toolbar-item" :underline="false" @click="toolbarClick('zoomIn')">
{{ t('topologyAnalysis.graph_zoomIn') }}
</el-link>
<el-link class="toolbar-item" :underline="false" @click="toolbarClick('zoomOut')">
{{ t('topologyAnalysis.graph_zoomOut') }}
</el-link>
<el-divider direction="vertical" />
<el-link class="toolbar-item" :underline="false" @click="toolbarClick('zoomReset')">
{{ t('topologyAnalysis.graph_zoomReset') }}
</el-link>
<el-link class="toolbar-item" :underline="false" @click="toolbarClick('fitView')">
{{ t('topologyAnalysis.graph_fitView') }}
</el-link>
</div>
</div>
# 工具栏操作方法
function toolbarClick(action: string) {
if (!state.graph) return;
if (action === 'fitView') {
state.graph?.fitView();
return;
}
const zoom = state.graph.getZoom();
if (action === 'zoomIn') {
if (zoom > 10) return;
state.graph?.zoom(1.2);
state.graph?.fitCenter();
return;
}
if (action === 'zoomOut') {
if (zoom < 0.4) return;
state.graph?.zoom(0.8);
state.graph?.fitCenter();
return;
}
if (action === 'zoomReset') {
state.graph?.zoomTo(1);
state.graph?.fitCenter();
}
}
# 画布部分设置网格背景样式
.graph_main {
background-image: linear-gradient(90deg, rgba(60, 10, 30, 0.04) 3%, transparent 0),
linear-gradient(1turn, rgba(60, 10, 30, 0.04) 3%, transparent 0);
background-size: 20px 20px;
background-position: 50%;
background-repeat: repeat;
margin: 0 10px;
:deep(.minimap-container) {
position: absolute;
right: 10px;
top: 100px;
background-color: rgba(0, 0, 0, 0.1);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
.g6-minimap-viewport { // 缩略图样式
outline: 2px solid hsla(213, 100%, 55%, 0.5);
}
}
}
# 工具栏样式
#flow-toolbar-wrap {
width: 100%;
display: flex;
margin-bottom: 5px;
align-items: center;
justify-content: center;
.flow-toolbar {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 40px;
box-shadow: var(--el-box-shadow-light);
background-color: #fff;
padding: 0 16px;
z-index: 1;
.toolbar-item {
user-select: none;
}
}
}