antv/g6绘制数据流向图
- 前言
- 接口模拟数据
- html
- ts
- 页面效果
前言
在业务开发中需要绘制数据流向图,由于echarts关系图的限制以及需求的特殊要求,转而使用antv/g6实现,本文以代码的方式实现数据流向需求以及节点分组,版本"@antv/g6": “^4.8.24”,
本文主要列出关键性的代码,并非全部代码
接口模拟数据
getCenterFlowG6ApiV2(data = {}) {
return new Promise((resolve) => {
let nodes: any = []
for (let i = 1; i <= 14; i++) {
let node: any = { id: "node-" + i, label: "node-" + i }
if (i % 3 === 0) {
node.class = "c0"
} else if (i % 3 === 1) {
node.class = "c1"
} else if (i % 3 === 2) {
node.class = "c2"
}
nodes.push(node)
}
const res = {
"resultStat": "0",
"failCode": null,
"mess": "获取流向成功",
"callBack": null,
"data": {
nodes: [
{
id: "4",
label: "业务数据库A",
comboId:"group0",
imgType:"14",
"pointNodeDetail": {
"nodeName": "武汉关",
"nodeTypeName": "应用安全域",
"areaName": "武汉关",
"areaIpScope": "160.1.1.1-255.255.255.255",
"areaBelong": "tanzhi",
"areaType": "办公网接入域"
},
}, {
id: "8",
label: "业务数据库B",
comboId:"group1",
imgType:"10"
},{
id: "10",
label: "主机166.10.1.1",
comboId:"group2"
},{
id: "12",
label: "主机161.19.1.4",
comboId:"group4"
}, {
id: "14",
label: "业务数据库B",
comboId:"group3"
}
],
edges: [
{
eid: "4-8",
source: "4",
target: "8",
},{
eid: "8-4",
source: "8",
target: "4",
},{
eid: "10-4",
source: "10",
target: "4",
},{
eid: "10-8",
source: "10",
target: "8",
},{
eid: "12-8",
source: "12",
target: "8",
style:{
stroke: 'red', // 线的颜色
}
},{
eid: "4-14",
source: "4",
target: "14",
}
],
combos: [
{
id:'group0',
label:'信息中心',
collapsed: true,// 初始时,收起,不展示内部节点
style:{
fill: "r(0.5,0.5,0.9) 0.6:#f8fcff 1:#3b97f1",
opacity: 0.2
}
},
{
id:'group1',
label:'数据分析中心',
parentId:'group0',
collapsed: true,
style:{
fill:"#FCCBAE"
}
},
{
id:'group2',
label:'数据采集',
collapsed: true,
style:{
fill:"#ECF7CF"
}
},
{
id:'group3',
label:'业务办公区',
parentId:'group0',
collapsed: true,
style:{
fill:"#CECFD1"
}
},
{
id:'group4',
label:'某地海关',
collapsed: true,
style:{
fill:"#D1E9FF"
}
}
]
},
"redirectURL": null,
"total": null
}
resolve(res)
})
}
html
<div class="echart-box">
<div class="chart1" id="charts1" *ngIf="chartData.data != null && !pageLoading" ></div>
</div>
ts
import G6 from "@antv/g6"
import equipment from "../../../../assets/equipment.png"
import equipmentE from "../../../../assets/equipmentE.png"
import equipmentY from "../../../../assets/equipmentY.png"
import application from "../../../../assets/application.png"
import assetsE from "../../../../assets/assetsE.png"
import assetsY from "../../../../assets/assetsY.png"
import assets from "../../../../assets/assets.png"
import domain from "../../../../assets/domain.png"
import domainE from "../../../../assets/domainE.png"
import domainY from "../../../../assets/domainY.png"
import warning from "../../../../assets/warning.png"
import warningY from "../../../../assets/warningY.png"
import clusterAsset from "../../../../assets/clusterAsset.png"
import clusterAssetY from "../../../../assets/clusterAssetY.png"
import clusterAssetR from "../../../../assets/clusterAssetR.png"
import belongCenterY from "../../../../assets/belongCenterY.png"
import belongCenter from "../../../../assets/belongCenter.png"
import belongCenterR from "../../../../assets/belongCenterR.png"
import netDomain from "../../../../assets/netDomain.png"
import netDomainR from "../../../../assets/netDomainR.png"
import netDomainY from "../../../../assets/netDomainY.png"
import groupIcon from "../../../../assets/chart/img/g6/群组_02.png";
/**
* 加载流向图
*/
getDataFlow() {
this.pageLoading = true
this.apiService.getCenterFlowG6ApiV2(removeNullProperty({
...this.q
})).then((res: resType) => {
console.log(res);
if (res.resultStat === "0") {
this.chartData.data = this.transformData(res.data)
console.log(this.chartData.data);
setTimeout(() => {
this.initG6DataFlow(this.chartData.data)
}, 300);
}
this.pageLoading = false
}).catch(err => {
this.pageLoading = false
})
}
initG6DataFlow(data) {
let rectWidth = 800
let rectHeight = 600
const eContainer = document.getElementById("charts1")
if (eContainer) {
if (data.nodes.length < 100) {
eContainer.style.height = '100%' // 600px
eContainer.style.minHeight = '600px' // 600px
eContainer.style.width = '100%' // 800px
} else {
eContainer.style.height = '1080px'
eContainer.style.width = '1920px'
}
const rectObject = eContainer.getBoundingClientRect()
rectWidth = rectObject.right - rectObject.left
rectHeight = rectObject.bottom - rectObject.top;
console.log(rectObject);
console.log(rectWidth, rectHeight);
}
const graph = new G6.Graph({
container: 'charts1', // String | HTMLElement,必须,在 Step 1 中创建的容器 id 或容器本身
width: rectWidth - 10, // Number,必须,图的宽度
height: rectHeight - 10, // Number,必须,图的高度
fitView: false, // 将图适配到画布
fitViewPadding: 50, // 画布四周留白宽度
// 必须将 groupByTypes 设置为 false,带有 combo 的图中元素的视觉层级才能合理
groupByTypes: false,
fitCenter: true,
linkCenter: false,
//autoPaint: true,
layout: {
type: 'comboCombined',
spacing: 20,
comboPadding: 5
},
modes: {
// 允许拖拽画布、放缩画布、拖拽节点,
default: [
'drag-canvas',
'zoom-canvas',
{
type: 'drag-node',
onlyChangeComboSize: true,
},
{
type: "drag-combo",
enableDelegate: false,
onlyChangeComboSize: true,
},
{
type: 'collapse-expand-combo',
trigger: 'click',
relayout: false, // 收缩展开后,不重新布局
},
{
type: 'tooltip', // 提示框
formatText(model) {
// 提示框文本内容
const text = 'label: ' + model.label + '<br/> class: ' + model.class;
return text;
},
shouldUpdate: e => {
return true;
}
},
{
type: 'edge-tooltip', // 边提示框
formatText(model) {
// 边提示框文本内容
const text =
'source: ' +
model.source +
'<br/> target: ' +
model.target +
'<br/> weight: ' +
(model.weight || "");
return text;
},
shouldUpdate: e => {
return true;
}
}
],
},
defaultNode: {// 默认状态下的节点配置
size: 30,
// 节点样式配置
style: {
fill: 'steelblue', // 节点填充色
stroke: '#666', // 节点描边色
lineWidth: 2, // 节点描边粗细
},
// 节点上的标签文本配置
labelCfg: {
// 节点上的标签文本样式配置
style: {
fill: '#333', // 节点标签文字颜色
stroke: '#fff',
},
position:"bottom"
},
},
defaultEdge: {// 默认状态下的边配置
style: {
//opacity: 0.6, // 边透明度
lineWidth: 4, // 线宽
stroke: '#D6ECF3', // 线的颜色
//endArrow: true,// 默认箭头
endArrow: { // 自定义终点箭头
path: G6.Arrow.vee(5, 10, 10), // 使用内置箭头路径函数,参数为箭头的 宽度、长度、偏移量(默认为 0,与 d 对应)
d: 10
}
},
// 边上的标签文本配置
labelCfg: {
autoRotate: true, // 边上的标签文本根据边的方向旋转
refY: 10,
},
},
defaultCombo: {
collapsed: true,
padding:5,
labelCfg:{
"style": {
"fontSize": 12,
"fill": "r(0.5,0.5,0.1) 0:#ffffff 1:#555555",
"opacity": 1,
"stroke": "#fff",
"lineWidth": 1,
"fontFamily": "微软雅黑",
"text": "信息中心"
},
"position": "top"
},
collapsedSubstituteIcon: { // 群组收起时的图标
show: true,
img: groupIcon,
height: 30,
width: 30,
},
},
// 节点不同状态下的样式集合
nodeStateStyles: {
// 鼠标 hover 上节点,即 hover 状态为 true 时的样式
hover: {
fill: 'lightsteelblue',
},
// 鼠标点击节点,即 click 状态为 true 时的样式
click: {
stroke: '#000',
lineWidth: 3,
},
},
// 边不同状态下的样式集合
edgeStateStyles: {
// 鼠标点击边,即 click 状态为 true 时的样式
click: {
stroke: 'steelblue',
},
},
});
if (this.chartData.instance) {
this.chartData.instance.destroy()
}
this.chartData.instance = graph
graph.data(data); // 读取 Step 2 中的数据源到图上
graph.render(); // 渲染图
graph.get('canvas').set('localRefresh', false)
// 监听鼠标进入节点
graph.on('node:mouseenter', (e) => {
const nodeItem = e.item;
// 设置目标节点的 hover 状态 为 true
graph.setItemState(nodeItem, 'hover', true);
});
// 监听鼠标离开节点
graph.on('node:mouseleave', (e) => {
const nodeItem = e.item;
// 设置目标节点的 hover 状态 false
graph.setItemState(nodeItem, 'hover', false);
});
// 监听鼠标点击节点
graph.on('node:click', (e) => {
console.log(e);
this.pointNodeDetail = e.item._cfg.model.pointNodeDetail
// 先将所有当前有 click 状态的节点的 click 状态置为 false
const clickNodes = graph.findAllByState('node', 'click');
clickNodes.forEach((cn) => {
graph.setItemState(cn, 'click', false);
});
const nodeItem = e.item;
// 设置目标节点的 click 状态 为 true
graph.setItemState(nodeItem, 'click', true);
});
// 监听鼠标点击节点
graph.on('edge:click', (e) => {
// 先将所有当前有 click 状态的边的 click 状态置为 false
const clickEdges = graph.findAllByState('edge', 'click');
clickEdges.forEach((ce) => {
graph.setItemState(ce, 'click', false);
});
const edgeItem = e.item;
// 设置目标边的 click 状态 为 true
graph.setItemState(edgeItem, 'click', true);
});
}
/**
* 对接口数据进行加工
*/
transformData(data) {
for (let i = 0; i < data.nodes.length; i++) {
let node = data.nodes[i]
console.log(node);
if (!node.style) {
node.style = {}
}
switch (
node.class // 根据节点数据中的 class 属性配置图形
) {
case 'c0': {
node.type = 'circle'; // class = 'c0' 时节点图形为 circle
break;
}
case 'c1': {
debugger
node.type = 'rect'; // class = 'c1' 时节点图形为 rect
node.size = [35, 20]; // class = 'c1' 时节点大小
break;
}
case 'c2': {
node.type = 'ellipse'; // class = 'c2' 时节点图形为 ellipse
node.size = [35, 20]; // class = 'c2' 时节点大小
break;
}
}
if(node.imgType){
this.transNodeImg(node)
}
}
return data
}
/**
* 根据类型设置image图标
* @param node
*/
transNodeImg(node) {
node.type = 'image';
node.size = 30
switch (
node.imgType // 根据节点数据中的 class 属性配置图形
) {
case '1': {
node.img = domain
break;
}
case '2': {
node.img = equipment
break;
}
case '3': {
node.img = assets
break;
}
case '4': {
node.img = application
break;
}
case '5': {
node.img = domainY
break;
}
case '6': {
node.img = equipmentY
break;
}
case '7': {
node.img = assetsY
break;
}
case '8': {
node.img = warningY
break;
}
case '9': {
node.img = domainE
break;
}
case '10': {
node.img = equipmentE
break;
}
case '11': {
node.img = assetsE
break;
}
case '12': {
node.img = warning
break;
}
case '13': {
node.img = clusterAsset
break;
}
case '14': {
node.img = belongCenter
break;
}
case '15': {
node.img = belongCenter
break;
}
case '16': {
node.img = netDomain
break;
}
case '17': {
node.img = clusterAssetY
break;
}
case '18': {
node.img = belongCenterY
break;
}
case '19': {
node.img = belongCenterY
break;
}
case '20': {
node.img = netDomainY
break;
}
case '21': {
node.img = clusterAssetR
break;
}
case '22': {
node.img = belongCenterR
break;
}
case '23': {
node.img = belongCenterR
break;
}
case '24': {
node.img = netDomainR
break;
}
}
}