G6实现可向下向上扩展的自定义流程图/层级图/架构图
G6是我们现在比较常用的可视化图形插件,各种图形也比较丰富,但是各种各样的业务场景,就还是需要我们去自定义才能实现想要的功能。比如我最近做的需求,业务场景是,一个中心任务,有向下扩展的任务,也有向上依赖的任务,并且有可能相互依赖。我的实现思路是,使用层级布局,自定义节点,层次布局的官方介绍详情可看官网:https://antv-g6.gitee.io/zh/docs/api/graphLayout/dagre
实现的效果如下:
交互:在点击原点的时候请求后端接口展开或收起。
中间实现的关键点是每个节点的所有父级和子级的id的处理。具体代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="https://gw.alipayobjects.com/os/lib/antv/g6/3.8.5/dist/g6.min.js"></script> <title>Document</title> </head> <style> #container { width: 100%; height: 100vh; display: flex; flex-direction: column; overflow: hidden; } .color-tip-container { display: flex; padding: 12px; } .color-tip-item { margin-right: 8px; padding: 4px 10px; border-radius: 4px; } </style> <body> <div id="container"> <div class="color-tip-container"> <div class="color-tip-item" style="color: #d5a4cf;border: 1px solid #d5a4cf">type1</div> <div class="color-tip-item" style="color: #41b6e6;border: 1px solid #41b6e6">type2</div> <div class="color-tip-item" style="color: #e1982f;border: 1px solid #e1982f">type3</div> <div class="color-tip-item" style="color: #7c4326;border: 1px solid #7c4326">type4</div> </div> <div id="chartView"></div> </div> </body> <script> const container = document.getElementById('chartView'); const curTaskNode = { id: '-1', taskId: '10000', name: '这是中心任务abcdefg', level: 'base', // base, parent, child textColor: '#d5a4cf', hasParent: true, // 默认有父节点 hasChild: true, // 默认有子节点 parentCollapse: true, // 父节点是否折叠 childCollapse: true, // 子节点是否折叠 childIds: [], parentIds: [], }; // 不同类型的块的颜色设置 const colorData = { 1: { textColor: '#d5a4cf', }, 2: { textColor: '#41b6e6', }, 3: { textColor: '#e1982f', }, 4: { textColor: '#7c4326', }, }; autoIncrementId = 1; // 自增id,这里是因为前端mock数据,所以使用,正常情况下这个id由后端数据提供 nodesData = []; // 所有的节点集合 edgesData = []; // 边数据 // 随机取[1, 4]的正整数 const getRandomIntInclusive = (min = 1, max = 4) => { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; //含最大值,含最小值 } // 得到目标数组中除了当前数组以外的数组 const getDiffData = (originArr, targetArr) => { const diffArr = []; originArr.forEach(item => { if (!targetArr.includes(item)) { diffArr.push(item); } }); return diffArr; } /** * 点击收起/展开的事件 * @param {*} evtName 节点名称 * @param {*} model 当前点击的数据模型 */ const updateGraph = (evtName, model) => { let newNodesData = []; // 新的节点数据 let newEdgesData = []; // 新的边数据 if (evtName === 'p-marker') { if (model.parentCollapse) { // 目前是折叠状态,需要展开 newEdgesData = [].concat(edgesData); const newAddNodes = []; const newIds = []; for (let i = 0; i < 2; i++) { // 固定添加两条数据,之后改成请求 // newNodesData[curNodeIndex].parentIds.push(String(autoIncrementId)); newIds.push(String(autoIncrementId)); const randomType = getRandomIntInclusive(); console.log(randomType, 'randomType'); const colorItem = colorData[randomType]; newAddNodes.push({ id: String(autoIncrementId), name: `测试新增父name${autoIncrementId}`, taskId: `10000${autoIncrementId}`, parentIds: [], childIds: model.childIds.concat([model.id]), level: 'parent', hasChild: true, hasParent: true, parentCollapse: true, childCollapse: false, textColor: colorItem.textColor, }); newEdgesData.push({ target: model.id, source: String(autoIncrementId) }); autoIncrementId ++; } const curNodeIndex = nodesData.findIndex(item => item.id === model.id); // 当前点击的节点在节点数据中的下标 nodesData[curNodeIndex].parentCollapse = false; nodesData[curNodeIndex].parentIds = nodesData[curNodeIndex].parentIds.concat(newIds); nodesData.concat(newAddNodes).forEach(node => { if (model.childIds.includes(node.id)) { node.parentIds = node.parentIds.concat(newIds); } newNodesData.push(node); }); } else { // 目前是展开状态,需要折叠 // 所有以当前的所有父节点为源头的箭头指向的箭头都要去掉 newEdgesData = edgesData.filter(edge => !model.parentIds.includes(edge.source)); nodesData.forEach(node => { if (!node.childIds.includes(model.id)) { // 所有子节点中有当前节点的节点都要去掉,并且留下的父节点也要去掉要删除的节点数据 node.parentIds = getDiffData(node.parentIds, model.parentIds); newNodesData.push(node); } }); const curNodeIndex = newNodesData.findIndex(item => item.id === model.id); // 当前点击的节点在节点数据中的下标 newNodesData[curNodeIndex].parentCollapse = true; } } else if (evtName === 'c-marker') { if (model.childCollapse) { // 目前是折叠状态,需要展开 newEdgesData = [].concat(edgesData); // 边数据 const newAddNodes = []; // 新增加的子节点数据 const newIds = []; for (let i = 0; i < 2; i++) { // 固定添加两条数据,之后改成请求 newIds.push(String(autoIncrementId)); const colorItem = colorData[getRandomIntInclusive()]; newAddNodes.push({ id: String(autoIncrementId), name: `测试新增子name${autoIncrementId}`, taskId: `10000${autoIncrementId}`, parentIds: model.parentIds.concat([model.id]), childIds: [], level: 'child', hasChild: true, hasParent: true, parentCollapse: false, childCollapse: true, textColor: colorItem.textColor, }); newEdgesData.push({ target: String(autoIncrementId), source: model.id }); autoIncrementId ++; } const curNodeIndex = nodesData.findIndex(item => item.id === model.id); // 当前点击的节点在节点数据中的下标 nodesData[curNodeIndex].childCollapse = false; nodesData[curNodeIndex].childIds = nodesData[curNodeIndex].childIds.concat(newIds); nodesData.concat(newAddNodes).forEach(node => { if (model.parentIds.includes(node.id)) { node.childIds = node.childIds.concat(newIds); } newNodesData.push(node); }); } else { // 目前是展开状态,需要折叠 // 去掉所有以当前的所有子节点中任意一个为目标点的箭头 newEdgesData = edgesData.filter(edge => !model.childIds.includes(edge.target)); nodesData.forEach(node => { if (!node.parentIds.includes(model.id)) { node.childIds = getDiffData(node.childIds, model.childIds); newNodesData.push(node); } }); const curNodeIndex = newNodesData.findIndex(item => item.id === model.id); // 当前点击的节点在节点数据中的下标 newNodesData[curNodeIndex].childCollapse = true; } } nodesData = newNodesData; edgesData = newEdgesData; console.log('newNodesData--🌈🌈', newNodesData); console.log('newEdgesData--🌧️🌧️', newEdgesData); graphObj.changeData({ nodes: newNodesData, edges: newEdgesData}); graphObj.fitCenter(); } // 初始化节点/边 const initGraphData = () => { G6.registerNode( 'dispatch-rect', { drawShape: (cfg, group) => { const { name = '', taskId = '', level = '', hasParent = false, hasChild = false, textColor } = cfg; // 矩形框 const rectConfig = { x: -90, y: -30, width: 180, height: 60, lineWidth: 1, fontSize: 12, fill: level === 'base' ? textColor : '#fff', radius: 4, opacity: 1, stroke: textColor, }; const rect = group.addShape('rect', { attrs: { ...rectConfig, }, }); // 当前事件id group.addShape('text', { attrs: { x: -76, y: -8, text: `id:${taskId}`, fontSize: 12, fill: level === 'base' ? '#fff' : textColor, cursor: 'pointer', }, name: 'id-text', }); // 当前事件名称 group.addShape('text', { attrs: { x: -76, y: 10, text: name.length > 14 ? `${name.substring(0, 14)}...` : name, fontSize: 14, fill: level === 'base' ? '#fff' : textColor, cursor: 'pointer', textBaseline: 'middle', }, name: 'name-text', }); // 操作上级的marker if ((level === 'base' || level === 'parent') && hasParent) { group.addShape('circle', { attrs: { x: 0, y: -30, r: 5, fill: '#dadada', cursor: 'pointer', }, name: 'p-marker', }); } // 操作下级的marker if ((level === 'base' || level === 'child') && hasChild) { group.addShape('circle', { attrs: { x: 0, y: 30, r: 5, fill: '#dadada', cursor: 'pointer', }, name: 'c-marker', }); } return rect; }, // update: (cfg, item) => { // console.log(cfg, 'cfg updated', item); // } }, 'rect' ); G6.registerEdge('flow-line', { draw(cfg, group) { const startPoint = cfg.startPoint; const endPoint = cfg.endPoint; const { style } = cfg; const shape = group.addShape('path', { attrs: { stroke: style.stroke, endArrow: style.endArrow, path: [ ['M', startPoint.x, startPoint.y], ['L', startPoint.x, (startPoint.y + endPoint.y) / 2], ['L', endPoint.x, (startPoint.y + endPoint.y) / 2], ['L', endPoint.x, endPoint.y], ], }, }); return shape; }, }); } // 绘制 const initDraw = () => { const parentContainer = document.getElementById('container'); const width = parentContainer.offsetWidth - 40; const height = parentContainer.offsetHeight - 180 || 500; const minimap = new G6.Minimap(); const graph = new G6.Graph({ container: container, width, height, plugins: [minimap], layout: { type: 'dagre', nodesep: 60, ranksep: 40, controlPoints: true, }, defaultNode: { type: 'dispatch-rect', anchorPoints: [[0.5, 0],[0.5, 1]], }, defaultEdge: { type: 'flow-line', style: { stroke: '#dadada', endArrow: { path: G6.Arrow.triangle(), fill: '#dadada' } } }, nodeStateStyles: { selected: { stroke: '#d9d9d9', fill: '#5394ef', }, }, modes: { default: [ 'drag-canvas', 'zoom-canvas', { type: 'tooltip', formatText(model) { const { id, name } = model; return `<div style="background: #fff;padding: 12px 20px;box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%)"> id:${id}<br/> name:${name} </div>`; }, offset: 30, }, ], }, fitCenter: true, }); nodesData.push(curTaskNode); graph.data({ nodes: [ curTaskNode ], edges: [] }); graph.render(); graph.on('node:click', evt => { // 在注册节点的时候每个图形都有个name属性,可以根据name属性确定用户点击的事具体哪个图形,进行相应的操作 const name = evt.target.get('name'); if (name === 'name-text') { // 跳转case window.open('https://antv-g6.gitee.io/zh', '_blank') } else if (name === 'p-marker' || name === 'c-marker') { const model = evt.item.getModel(); updateGraph(name, model); } }) graphObj = graph; } initGraphData(); initDraw(); if (typeof window !== 'undefined') { window.onresize = () => { if (!graphObj || graphObj.get('destroyed')) return; if (!container || !container.scrollWidth || !container.scrollHeight) return; graphObj.changeSize(container.scrollWidth, container.scrollHeight); }; } </script> </html>