2024-01-04
Angular
00
请注意,本文编写于 731 天前,最后修改于 664 天前,其中某些信息可能已经过时。

在业务开发中需要绘制数据流向图,由于echarts关系图的限制以及需求的特殊要求,转而使用antv/g6实现,本文以代码的方式实现数据流向需求以及节点分组,版本"@antv/g6": "^4.8.24",

本文主要列出关键性的代码,并非全部代码

静态数据如下

ts
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; } } }

页面效果如下:

image.png

image.png

image.png

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:千寻

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!