import { Algorithm, Util, registerNode, registerEdge, Layout, Menu, Graph, Tooltip, } from '@antv/g6'; const { labelPropagation, louvain, findShortestPath } = Algorithm; const { uniqueId } = Util; const NODESIZEMAPPING = 'degree'; const SMALLGRAPHLABELMAXLENGTH = 12; let labelMaxLength = SMALLGRAPHLABELMAXLENGTH; const DEFAULTNODESIZE = 30; const DEFAULTAGGREGATEDNODESIZE = 56; const NODE_LIMIT = 40; // TODO: find a proper number for maximum node number on the canvas let graph: any = null; let currentUnproccessedData = { nodes: [], edges: [] }; let nodeMap: any = {}; let aggregatedNodeMap: any = {}; let hiddenItemIds: any[] = []; // 隐藏的元素 id 数组 let largeGraphMode = true; let cachePositions: any = {}; let manipulatePosition: any = undefined; let descreteNodeCenter: any; let layout: any = { type: '', instance: null, destroyed: true, }; let expandArray: any = []; let collapseArray: any = []; let shiftKeydown = false; let CANVAS_WIDTH = 800, CANVAS_HEIGHT = 800; const duration = 2000; const animateOpacity = 0.6; const animateBackOpacity = 0.1; const virtualEdgeOpacity = 0.1; const realEdgeOpacity = 0.2; const darkBackColor = 'rgb(43, 47, 51)'; const disableColor = '#777'; const theme = 'dark'; // 'dark'; const subjectColors = [ '#5F95FF', // blue '#61DDAA', '#65789B', '#F6BD16', '#7262FD', '#78D3F8', '#9661BC', '#F6903D', '#008685', '#F08BB4', ]; const colorSets = Util.getColorSetsBySubjectColors( subjectColors, darkBackColor, theme, disableColor ); const globalStyle = { node: { style: { fill: '#2B384E', }, labelCfg: { style: { fill: '#acaeaf', stroke: '#191b1c', }, }, stateStyles: { focus: { fill: '#2B384E', }, }, }, edge: { style: { stroke: '#acaeaf', realEdgeStroke: '#acaeaf', //'#f00', realEdgeOpacity, strokeOpacity: realEdgeOpacity, }, labelCfg: { style: { fill: '#acaeaf', realEdgeStroke: '#acaeaf', //'#f00', realEdgeOpacity: 0.5, stroke: '#191b1c', }, }, stateStyles: { focus: { stroke: '#fff', // '#3C9AE8', }, }, }, }; // Custom super node registerNode( 'aggregated-node', { draw(cfg, group) { let width = 53, height = 27; const style = cfg.style || {}; const colorSet = cfg.colorSet || colorSets[0]; // halo for hover group.addShape('rect', { attrs: { x: -width * 0.55, y: -height * 0.6, width: width * 1.1, height: height * 1.2, fill: colorSet.mainFill, opacity: 0.9, lineWidth: 0, radius: (height / 2 || 13) * 1.2, }, // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type name: 'halo-shape', visible: false, }); // focus stroke for hover group.addShape('rect', { attrs: { x: -width * 0.55, y: -height * 0.6, width: width * 1.1, height: height * 1.2, fill: colorSet.mainFill, // '#3B4043', stroke: '#AAB7C4', lineWidth: 1, lineOpacty: 0.85, radius: (height / 2 || 13) * 1.2, }, name: 'stroke-shape', visible: false, }); const keyShape = group.addShape('rect', { attrs: { x: -width / 2, y: -height / 2, width, height, fill: colorSet.mainFill, // || '#3B4043', stroke: colorSet.mainStroke, lineWidth: 2, cursor: 'pointer', radius: height / 2 || 13, lineDash: [2, 2], ...style, }, // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type name: 'aggregated-node-keyShape', }); let labelStyle = {}; if (cfg.labelCfg) { labelStyle = Object.assign(labelStyle, cfg.labelCfg.style); } group.addShape('text', { attrs: { text: `${cfg.count}`, x: 0, y: 0, textAlign: 'center', textBaseline: 'middle', cursor: 'pointer', fontSize: 12, fill: '#fff', opacity: 0.85, fontWeight: 400, }, // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type name: 'count-shape', className: 'count-shape', draggable: true, }); // tag for new node // if (cfg.new) { // group.addShape('circle', { // attrs: { // x: width / 2 - 3, // y: -height / 2 + 3, // r: 4, // fill: '#6DD400', // lineWidth: 0.5, // stroke: '#FFFFFF', // }, // // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type // name: 'typeNode-tag-circle', // }); // } return keyShape; }, setState: (name, value, item: any) => { const group = item.get('group'); if (name === 'layoutEnd' && value) { const labelShape = group.find( (e: any) => e.get('name') === 'text-shape' ); if (labelShape) labelShape.set('visible', true); } else if (name === 'hover') { if (item.hasState('focus')) { return; } const halo = group.find((e: any) => e.get('name') === 'halo-shape'); const keyShape = item.getKeyShape(); const colorSet = item.getModel().colorSet || colorSets[0]; if (value) { halo && halo.show(); keyShape.attr('fill', colorSet.activeFill); } else { halo && halo.hide(); keyShape.attr('fill', colorSet.mainFill); } } else if (name === 'focus') { const stroke = group.find((e: any) => e.get('name') === 'stroke-shape'); const keyShape = item.getKeyShape(); const colorSet = item.getModel().colorSet || colorSets[0]; if (value) { stroke && stroke.show(); keyShape.attr('fill', colorSet.selectedFill); } else { stroke && stroke.hide(); keyShape.attr('fill', colorSet.mainFill); } } }, update: undefined, }, 'single-node' ); // Custom real node registerNode( 'real-node', { draw(cfg, group) { let r = 30; if (typeof cfg.size === 'number') { r = cfg.size / 2; } else if (Array.isArray(cfg.size)) { r = cfg.size[0] / 2; } const style = cfg.style || {}; const colorSet = cfg.colorSet || colorSets[0]; console.log('=== ', cfg); // halo for hover group.addShape('circle', { attrs: { x: 0, y: 0, r: r + 5, fill: style.fill || colorSet.mainFill || '#2B384E', opacity: 0.9, lineWidth: 0, }, // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type name: 'halo-shape', visible: false, }); // focus stroke for hover group.addShape('circle', { attrs: { x: 0, y: 0, r: r + 5, fill: style.fill || colorSet.mainFill || '#2B384E', stroke: '#fff', strokeOpacity: 0.85, lineWidth: 1, }, // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type name: 'stroke-shape', visible: false, }); // 默认形状 const keyShape = group.addShape('circle', { attrs: { x: 0, y: 0, r, fill: colorSet.mainFill, stroke: colorSet.mainStroke, lineWidth: 2, cursor: 'pointer', ...style, }, // 设置 draggable 以允许响应鼠标的图拽事件 draggable: true, // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type name: 'aggregated-node-keyShape', }); // 网元状态标识 if (cfg.info) { const neInfo: any = cfg.info; const hasNeState = neInfo.serverState.neId; const neStateShape = group.addShape('circle', { attrs: { x: r - 3, y: -r + 3, r: 5, fill: hasNeState ? '#6DD400' : 'red', lineWidth: 0.5, stroke: '#FFFFFF', }, // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type name: 'ne-state', }); // 添加动画 neStateShape.animate( { // Magnifying and disappearing r: 5, opacity: 0.3, }, { duration: 1000, easing: 'easeCubic', delay: 0, repeat: true, // repeat } ); } if (cfg.label) { const text = cfg.label; let labelStyle: any = { textAlign: 'center', textBaseLine: 'alphabetic', cursor: 'pointer', fontSize: 8, fill: '#fff', opacity: 0.85, fontWeight: 400, }; let refY = 0; if (cfg.labelCfg) { labelStyle = Object.assign(labelStyle, cfg.labelCfg.style); refY += cfg.labelCfg.offset || 0; } labelStyle.fontSize = labelStyle.fontSize < 8 ? 8 : labelStyle.fontSize; group.addShape('text', { attrs: { text, x: 0, y: r + refY + 15, ...labelStyle, }, // 设置 draggable 以允许响应鼠标的图拽事件 draggable: true, // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type name: 'text-shape', className: 'text-shape', }); } if (cfg.icon) { // left icon group.addShape('image', { attrs: { x: 0, cursor: 'pointer', ...cfg.icon, }, // 设置 draggable 以允许响应鼠标的图拽事件 draggable: true, // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type name: 'icon-shape', }); } return keyShape; }, setState: (name, value, item: any) => { const group = item.get('group'); if (name === 'layoutEnd' && value) { const labelShape = group.find( (e: any) => e.get('name') === 'text-shape' ); if (labelShape) labelShape.set('visible', true); } else if (name === 'neState') { // 设置网元状态 const neState = group.find((e: any) => e.get('name') === 'ne-state'); if (value) { neState.attr('fill', '#52c41a'); } else { neState.attr('fill', '#f5222d'); } } else if (name === 'hover') { if (item.hasState('focus')) { return; } const halo = group.find((e: any) => e.get('name') === 'halo-shape'); const keyShape = item.getKeyShape(); const colorSet = item.getModel().colorSet || colorSets[0]; if (value) { halo && halo.show(); keyShape.attr('fill', colorSet.activeFill); } else { halo && halo.hide(); keyShape.attr('fill', colorSet.mainFill); } } else if (name === 'focus') { const stroke = group.find((e: any) => e.get('name') === 'stroke-shape'); const label = group.find((e: any) => e.get('name') === 'text-shape'); const keyShape = item.getKeyShape(); const colorSet = item.getModel().colorSet || colorSets[0]; if (value) { stroke && stroke.show(); keyShape.attr('fill', colorSet.selectedFill); label && label.attr('fontWeight', 800); } else { stroke && stroke.hide(); keyShape.attr('fill', colorSet.mainFill); // '#2B384E' label && label.attr('fontWeight', 400); } } // const model = item.getModel(); // const neState = group.find( // (e: any) => e.get('name') === 'ne-state-circle' // ); // if (neState) { // console.log('neState', neState); // item.getKeyShape().attr('fill', '#6DD400'); // } }, update: undefined, }, 'aggregated-node' ); // 这样可以继承 aggregated-node 的 setState // Custom the quadratic edge for multiple edges between one node pair registerEdge( 'custom-quadratic', { setState: (name, value, item: any) => { const group = item.get('group'); const model = item.getModel(); if (name === 'focus') { const back = group.find((ele: any) => ele.get('name') === 'back-line'); if (back) { back.stopAnimate(); back.remove(); back.destroy(); } const keyShape = group.find( (ele: any) => ele.get('name') === 'edge-shape' ); const arrow = model.style.endArrow; if (value) { if (keyShape.cfg.animation) { keyShape.stopAnimate(true); } keyShape.attr({ strokeOpacity: animateOpacity, opacity: animateOpacity, stroke: '#fff', endArrow: { ...arrow, stroke: '#fff', fill: '#fff', }, }); if (model.isReal) { const { lineWidth, path, endArrow, stroke } = keyShape.attr(); const back = group.addShape('path', { attrs: { lineWidth, path, stroke, endArrow, opacity: animateBackOpacity, }, // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type name: 'back-line', }); back.toBack(); const length = keyShape.getTotalLength(); keyShape.animate( (ratio: any) => { // the operations in each frame. Ratio ranges from 0 to 1 indicating the prograss of the animation. Returns the modified configurations const startLen = ratio * length; // Calculate the lineDash const cfg = { lineDash: [startLen, length - startLen], }; return cfg; }, { repeat: true, // Whether executes the animation repeatly duration, // the duration for executing once } ); } else { let index = 0; const lineDash = keyShape.attr('lineDash'); const totalLength = lineDash[0] + lineDash[1]; keyShape.animate( () => { index++; if (index > totalLength) { index = 0; } const res = { lineDash, lineDashOffset: -index, }; // returns the modified configurations here, lineDash and lineDashOffset here return res; }, { repeat: true, // whether executes the animation repeatly duration, // the duration for executing once } ); } } else { keyShape.stopAnimate(); const stroke = '#acaeaf'; const opacity = model.isReal ? realEdgeOpacity : virtualEdgeOpacity; keyShape.attr({ stroke, strokeOpacity: opacity, opacity, endArrow: { ...arrow, stroke, fill: stroke, }, }); } } }, }, 'quadratic' ); // Custom the line edge for single edge between one node pair registerEdge( 'custom-line', { setState: (name, value, item: any) => { const group = item.get('group'); const model = item.getModel(); if (name === 'focus') { const keyShape = group.find( (ele: any) => ele.get('name') === 'edge-shape' ); const back = group.find((ele: any) => ele.get('name') === 'back-line'); if (back) { back.stopAnimate(); back.remove(); back.destroy(); } const arrow = model.style.endArrow; if (value) { if (keyShape.cfg.animation) { keyShape.stopAnimate(true); } keyShape.attr({ strokeOpacity: animateOpacity, opacity: animateOpacity, stroke: '#fff', endArrow: { ...arrow, stroke: '#fff', fill: '#fff', }, }); if (model.isReal) { const { path, stroke, lineWidth } = keyShape.attr(); const back = group.addShape('path', { attrs: { path, stroke, lineWidth, opacity: animateBackOpacity, }, // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type name: 'back-line', }); back.toBack(); const length = keyShape.getTotalLength(); keyShape.animate( (ratio: any) => { // the operations in each frame. Ratio ranges from 0 to 1 indicating the prograss of the animation. Returns the modified configurations const startLen = ratio * length; // Calculate the lineDash const cfg = { lineDash: [startLen, length - startLen], }; return cfg; }, { repeat: true, // Whether executes the animation repeatly duration, // the duration for executing once } ); } else { const lineDash = keyShape.attr('lineDash'); const totalLength = lineDash[0] + lineDash[1]; let index = 0; keyShape.animate( () => { index++; if (index > totalLength) { index = 0; } const res = { lineDash, lineDashOffset: -index, }; // returns the modified configurations here, lineDash and lineDashOffset here return res; }, { repeat: true, // whether executes the animation repeatly duration, // the duration for executing once } ); } } else { keyShape.stopAnimate(); const stroke = '#acaeaf'; const opacity = model.isReal ? realEdgeOpacity : virtualEdgeOpacity; keyShape.attr({ stroke, strokeOpacity: opacity, opacity: opacity, endArrow: { ...arrow, stroke, fill: stroke, }, }); } } }, }, 'single-edge' ); const descendCompare = (p: any) => { // 这是比较函数 return function (m: any, n: any) { const a = m[p]; const b = n[p]; return b - a; // 降序 }; }; const clearFocusItemState = (graph: any) => { if (!graph) return; clearFocusNodeState(graph); clearFocusEdgeState(graph); }; // 清除图上所有节点的 focus 状态及相应样式 const clearFocusNodeState = (graph: any) => { const focusNodes = graph.findAllByState('node', 'focus'); focusNodes.forEach((fnode: any) => { graph.setItemState(fnode, 'focus', false); // false }); }; // 清除图上所有边的 focus 状态及相应样式 const clearFocusEdgeState = (graph: any) => { const focusEdges = graph.findAllByState('edge', 'focus'); focusEdges.forEach((fedge: any) => { graph.setItemState(fedge, 'focus', false); }); }; // 截断长文本。length 为文本截断后长度,elipsis 是后缀 const formatText = (text: any, length = 5, elipsis = '...') => { if (!text) return ''; if (text.length > length) { return `${text.substr(0, length)}${elipsis}`; } return text; }; const labelFormatter = (text: any, minLength = 10) => { if (text && text.split('').length > minLength) return `${text.substr(0, minLength)}...`; return text; }; const processNodesEdges = ( nodes: any, edges: any, width: any, height: any, largeGraphMode: any, edgeLabelVisible: any ) => { if (!nodes || nodes.length === 0) return {}; const currentNodeMap: any = {}; let maxNodeCount = -Infinity; const paddingRatio = 0.3; const paddingLeft = paddingRatio * width; const paddingTop = paddingRatio * height; nodes.forEach((node: any) => { node.type = node.level === 0 ? 'real-node' : 'aggregated-node'; node.isReal = node.level === 0 ? true : false; node.label = formatText(node.label, labelMaxLength, '...'); node.degree = 0; node.inDegree = 0; node.outDegree = 0; if (currentNodeMap[node.id]) { console.warn('node exists already!', node.id); node.id = `${node.id}${Math.random()}`; } currentNodeMap[node.id] = node; if (node.count > maxNodeCount) maxNodeCount = node.count; const cachePosition = cachePositions ? cachePositions[node.id] : undefined; if (cachePosition) { node.x = cachePosition.x; node.y = cachePosition.y; } else { if (manipulatePosition && !node.x && !node.y) { node.x = manipulatePosition.x + 30 * Math.cos(Math.random() * Math.PI * 2); node.y = manipulatePosition.y + 30 * Math.sin(Math.random() * Math.PI * 2); } } }); let maxCount = -Infinity; let minCount = Infinity; // let maxCount = 0; edges.forEach((edge: any) => { // to avoid the dulplicated id to nodes if (!edge.id) edge.id = uniqueId('edge'); else if (edge.id.split('-')[0] !== 'edge') edge.id = `edge-${edge.id}`; // TODO: delete the following line after the queried data is correct if (!currentNodeMap[edge.source] || !currentNodeMap[edge.target]) { console.warn( 'edge source target does not exist', edge.source, edge.target, edge.id ); return; } const sourceNode = currentNodeMap[edge.source]; const targetNode = currentNodeMap[edge.target]; if (!sourceNode || !targetNode) console.warn( 'source or target is not defined!!!', edge, sourceNode, targetNode ); // calculate the degree sourceNode.degree++; targetNode.degree++; sourceNode.outDegree++; targetNode.inDegree++; if (edge.count > maxCount) maxCount = edge.count; if (edge.count < minCount) minCount = edge.count; }); nodes.sort(descendCompare(NODESIZEMAPPING)); const maxDegree = nodes[0].degree || 1; const descreteNodes: any = []; nodes.forEach((node: any, i: any) => { // assign the size mapping to the outDegree const countRatio = node.count / maxNodeCount; const isRealNode = node.level === 0; node.size = isRealNode ? node.size || DEFAULTNODESIZE : DEFAULTAGGREGATEDNODESIZE; node.isReal = isRealNode; node.labelCfg = node.labelCfg || { position: 'bottom', offset: 5, style: { fill: globalStyle.node.labelCfg.style.fill, fontSize: 6 + countRatio * 6 || 12, stroke: globalStyle.node.labelCfg.style.stroke, lineWidth: 3, }, }; if (!node.degree) { descreteNodes.push(node); } }); const countRange = maxCount - minCount; const minEdgeSize = 1; const maxEdgeSize = 12; const edgeSizeRange = maxEdgeSize - minEdgeSize; edges.forEach((edge: any) => { // set edges' style const targetNode = currentNodeMap[edge.target]; const size = ((edge.count - minCount) / countRange) * edgeSizeRange + minEdgeSize || 1; edge.size = size; const arrowWidth = Math.max(size / 2 + 2, 3); const arrowLength = 10; const arrowBeging = targetNode.size + arrowLength; let arrowPath: any = `M ${arrowBeging},0 L ${ arrowBeging + arrowLength },-${arrowWidth} L ${arrowBeging + arrowLength},${arrowWidth} Z`; let d = targetNode.size / 2 + arrowLength; if (edge.source === edge.target) { edge.type = 'loop'; arrowPath = undefined; } const sourceNode = currentNodeMap[edge.source]; const isRealEdge = targetNode.isReal && sourceNode.isReal; edge.isReal = isRealEdge; const stroke = isRealEdge ? globalStyle.edge.style.realEdgeStroke : globalStyle.edge.style.stroke; const opacity = isRealEdge ? globalStyle.edge.style.realEdgeOpacity : globalStyle.edge.style.strokeOpacity; const dash = Math.max(size, 2); const lineDash = isRealEdge ? undefined : [dash, dash]; edge.style = { stroke, strokeOpacity: opacity, cursor: 'pointer', lineAppendWidth: Math.max(edge.size || 5, 5), fillOpacity: 1, lineDash, endArrow: arrowPath ? { path: arrowPath, d, fill: stroke, strokeOpacity: 0, } : false, }; edge.labelCfg = { autoRotate: true, style: { stroke: globalStyle.edge.labelCfg.style.stroke, fill: globalStyle.edge.labelCfg.style.fill, lineWidth: 4, fontSize: 12, lineAppendWidth: 10, opacity: 1, }, }; // 标签名称 if (!edge.oriLabel) edge.oriLabel = edge.label; if (largeGraphMode || !edgeLabelVisible) edge.label = ''; else { edge.label = labelFormatter(edge.label, labelMaxLength); } // arrange the other nodes around the hub const sourceDis = sourceNode.size / 2 + 20; const targetDis = targetNode.size / 2 + 20; if (sourceNode.x && !targetNode.x) { targetNode.x = sourceNode.x + sourceDis * Math.cos(Math.random() * Math.PI * 2); } if (sourceNode.y && !targetNode.y) { targetNode.y = sourceNode.y + sourceDis * Math.sin(Math.random() * Math.PI * 2); } if (targetNode.x && !sourceNode.x) { sourceNode.x = targetNode.x + targetDis * Math.cos(Math.random() * Math.PI * 2); } if (targetNode.y && !sourceNode.y) { sourceNode.y = targetNode.y + targetDis * Math.sin(Math.random() * Math.PI * 2); } if (!sourceNode.x && !sourceNode.y && manipulatePosition) { sourceNode.x = manipulatePosition.x + 30 * Math.cos(Math.random() * Math.PI * 2); sourceNode.y = manipulatePosition.y + 30 * Math.sin(Math.random() * Math.PI * 2); } if (!targetNode.x && !targetNode.y && manipulatePosition) { targetNode.x = manipulatePosition.x + 30 * Math.cos(Math.random() * Math.PI * 2); targetNode.y = manipulatePosition.y + 30 * Math.sin(Math.random() * Math.PI * 2); } }); descreteNodeCenter = { x: width - paddingLeft, y: height - paddingTop, }; descreteNodes.forEach((node: any) => { if (!node.x && !node.y) { node.x = descreteNodeCenter.x + 30 * Math.cos(Math.random() * Math.PI * 2); node.y = descreteNodeCenter.y + 30 * Math.sin(Math.random() * Math.PI * 2); } }); Util.processParallelEdges(edges, 12.5, 'custom-quadratic', 'custom-line'); return { maxDegree, edges, }; }; const getForceLayoutConfig = ( graph: any, largeGraphMode: any, configSettings?: any ) => { let { linkDistance, edgeStrength, nodeStrength, nodeSpacing, preventOverlap, nodeSize, collideStrength, alpha, alphaDecay, alphaMin, } = configSettings || { preventOverlap: true }; if (!linkDistance && linkDistance !== 0) linkDistance = 525; if (!edgeStrength && edgeStrength !== 0) edgeStrength = 50; if (!nodeStrength && nodeStrength !== 0) nodeStrength = 200; if (!nodeSpacing && nodeSpacing !== 0) nodeSpacing = 5; const config: any = { type: 'gForce', minMovement: 0.01, maxIteration: 5000, preventOverlap, damping: 0.99, linkDistance: (d: any) => { let dist = linkDistance; const sourceNode = nodeMap[d.source] || aggregatedNodeMap[d.source]; const targetNode = nodeMap[d.target] || aggregatedNodeMap[d.target]; // // 两端都是聚合点 // if (sourceNode.level && targetNode.level) dist = linkDistance * 3; // // 一端是聚合点,一端是真实节点 // else if (sourceNode.level || targetNode.level) dist = linkDistance * 1.5; if (!sourceNode?.level && !targetNode?.level) dist = linkDistance * 0.3; return dist; }, edgeStrength: (d: any) => { const sourceNode = nodeMap[d.source] || aggregatedNodeMap[d.source]; const targetNode = nodeMap[d.target] || aggregatedNodeMap[d.target]; // 聚合节点之间的引力小 if (sourceNode?.level && targetNode?.level) return edgeStrength / 2; // 聚合节点与真实节点之间引力大 if (sourceNode?.level || targetNode?.level) return edgeStrength; return edgeStrength; }, nodeStrength: (d: any) => { // 给离散点引力,让它们聚集 if (d.degree === 0) return -10; // 聚合点的斥力大 if (d.level) return nodeStrength * 2; return nodeStrength; }, nodeSize: (d: any) => { if (!nodeSize && d.size) return d.size; return 50; }, nodeSpacing: (d: any) => { if (d.degree === 0) return nodeSpacing * 2; if (d.level) return nodeSpacing; return nodeSpacing; }, onLayoutEnd: () => { if (largeGraphMode) { graph.getEdges().forEach((edge: any) => { console.log('onLayoutEnd', edge.oriLabel); if (!edge.oriLabel) return; edge.update({ label: labelFormatter(edge.oriLabel, labelMaxLength), }); }); } }, tick: () => { graph.refreshPositions(); }, }; if (nodeSize) config['nodeSize'] = nodeSize; if (collideStrength) config['collideStrength'] = collideStrength; if (alpha) config['alpha'] = alpha; if (alphaDecay) config['alphaDecay'] = alphaDecay; if (alphaMin) config['alphaMin'] = alphaMin; return config; }; const hideItems = (graph: any) => { hiddenItemIds.forEach(id => { graph.hideItem(id); }); }; const showItems = (graph: any) => { graph.getNodes().forEach((node: any) => { if (!node.isVisible()) graph.showItem(node); }); graph.getEdges().forEach((edge: any) => { if (!edge.isVisible()) edge.showItem(edge); }); hiddenItemIds = []; }; const handleRefreshGraph = ( graph: any, graphData: any, width: any, height: any, largeGraphMode: any, edgeLabelVisible: any ) => { if (!graphData || !graph) return; clearFocusItemState(graph); // reset the filtering graph.getNodes().forEach((node: any) => { if (!node.isVisible()) node.show(); }); graph.getEdges().forEach((edge: any) => { if (!edge.isVisible()) edge.show(); }); let nodes = [], edges = []; nodes = graphData.nodes; const processRes = processNodesEdges( nodes, graphData.edges || [], width, height, largeGraphMode, edgeLabelVisible ); edges = processRes.edges; graph.changeData({ nodes, edges }); hideItems(graph); graph.getNodes().forEach((node: any) => { node.toFront(); }); // layout.instance.stop(); // force 需要使用不同 id 的对象才能进行全新的布局,否则会使用原来的引用。因此复制一份节点和边作为 force 的布局数据 layout.instance.init({ nodes: graphData.nodes, edges, }); layout.instance.minMovement = 0.0001; // layout.instance.getCenter = d => { // const cachePosition = cachePositions[d.id]; // if (!cachePosition && (d.x || d.y)) return [d.x, d.y, 10]; // else if (cachePosition) return [cachePosition.x, cachePosition.y, 10]; // return [width / 2, height / 2, 10]; // } layout.instance.getMass = (d: any) => { const cachePosition = cachePositions[d.id]; if (cachePosition) return 5; return 1; }; layout.instance.execute(); return { nodes, edges }; }; const getMixedGraph = ( aggregatedData: any, originData: any, nodeMap: any, aggregatedNodeMap: any, expandArray: any, collapseArray: any ) => { let nodes: any = [], edges: any = []; const expandMap: any = {}, collapseMap: any = {}; expandArray.forEach((expandModel: any) => { expandMap[expandModel.id] = true; }); collapseArray.forEach((collapseModel: any) => { collapseMap[collapseModel.id] = true; }); aggregatedData.clusters.forEach((cluster: any, i: any) => { if (expandMap[cluster.id]) { nodes = nodes.concat(cluster.nodes); aggregatedNodeMap[cluster.id].expanded = true; } else { nodes.push(aggregatedNodeMap[cluster.id]); aggregatedNodeMap[cluster.id].expanded = false; } }); originData.edges.forEach((edge: any) => { const isSourceInExpandArray = expandMap[nodeMap[edge.source].clusterId]; const isTargetInExpandArray = expandMap[nodeMap[edge.target].clusterId]; if (isSourceInExpandArray && isTargetInExpandArray) { edges.push(edge); } else if (isSourceInExpandArray) { const targetClusterId = nodeMap[edge.target].clusterId; const vedge = { source: edge.source, target: targetClusterId, id: uniqueId('edge'), label: '', }; edges.push(vedge); } else if (isTargetInExpandArray) { const sourceClusterId = nodeMap[edge.source].clusterId; const vedge = { target: edge.target, source: sourceClusterId, id: uniqueId('edge'), label: '', }; edges.push(vedge); } }); aggregatedData.clusterEdges.forEach((edge: any) => { if (expandMap[edge.source] || expandMap[edge.target]) return; else edges.push(edge); }); return { nodes, edges }; }; const examAncestors = ( model: any, expandedArray: any, length: any, keepTags: any ) => { for (let i = 0; i < length; i++) { const expandedNode = expandedArray[i]; if (!keepTags[i] && model.parentId === expandedNode.id) { keepTags[i] = true; // 需要被保留 examAncestors(expandedNode, expandedArray, length, keepTags); break; } } }; // 展开节点数据控制 const manageExpandCollapseArray = ( nodeNumber: any, model: any, collapseArray: any, expandArray: any ) => { manipulatePosition = { x: model.x, y: model.y }; // 维护 expandArray,若当前画布节点数高于上限,移出 expandedArray 中非 model 祖先的节点) if (nodeNumber > NODE_LIMIT) { // 若 keepTags[i] 为 true,则 expandedArray 的第 i 个节点需要被保留 const keepTags: any = {}; const expandLen = expandArray.length; // 检查 X 的所有祖先并标记 keepTags examAncestors(model, expandArray, expandLen, keepTags); // 寻找 expandedArray 中第一个 keepTags 不为 true 的点 let shiftNodeIdx = -1; for (let i = 0; i < expandLen; i++) { if (!keepTags[i]) { shiftNodeIdx = i; break; } } // 如果有符合条件的节点,将其从 expandedArray 中移除 if (shiftNodeIdx !== -1) { let foundNode = expandArray[shiftNodeIdx]; if (foundNode.level === 2) { let foundLevel1 = false; // 找到 expandedArray 中 parentId = foundNode.id 且 level = 1 的第一个节点 for (let i = 0; i < expandLen; i++) { const eNode = expandArray[i]; if (eNode.parentId === foundNode.id && eNode.level === 1) { foundLevel1 = true; foundNode = eNode; expandArray.splice(i, 1); break; } } // 若未找到,则 foundNode 不变, 直接删去 foundNode if (!foundLevel1) expandArray.splice(shiftNodeIdx, 1); } else { // 直接删去 foundNode expandArray.splice(shiftNodeIdx, 1); } // const removedNode = expandedArray.splice(shiftNodeIdx, 1); // splice returns an array const idSplits = foundNode.id.split('-'); let collapseNodeId; // 去掉最后一个后缀 for (let i = 0; i < idSplits.length - 1; i++) { const str = idSplits[i]; if (collapseNodeId) collapseNodeId = `${collapseNodeId}-${str}`; else collapseNodeId = str; } const collapseNode = { id: collapseNodeId, parentId: foundNode.id, level: foundNode.level - 1, }; collapseArray.push(collapseNode); } } const currentNode = { id: model.id, level: model.level, parentId: model.parentId, }; // 加入当前需要展开的节点 expandArray.push(currentNode); graph.get('canvas').setCursor('default'); return { expandArray, collapseArray }; }; const cacheNodePositions = (nodes: any) => { const positionMap: any = {}; const nodeLength = nodes.length; for (let i = 0; i < nodeLength; i++) { const node = nodes[i].getModel(); positionMap[node.id] = { x: node.x, y: node.y, level: node.level, }; } return positionMap; }; // 布局停止动画 const stopLayout = () => { layout.instance.stop(); }; // 图监听事件 const bindListener = (graph: any) => { graph.on('node:mouseenter', (evt: any) => { const { item } = evt; graph.setItemState(item, 'hover', true); item.toFront(); }); graph.on('node:mouseleave', (evt: any) => { const { item } = evt; graph.setItemState(item, 'hover', false); }); graph.on('edge:mouseenter', (evt: any) => { const { item } = evt; const model = item.getModel(); const currentLabel = model.label; item.update({ label: model.oriLabel, }); model.oriLabel = currentLabel; item.toFront(); item.getSource().toFront(); item.getTarget().toFront(); }); graph.on('edge:mouseleave', (evt: any) => { const { item } = evt; const model = item.getModel(); const currentLabel = model.label; item.update({ label: model.oriLabel, }); model.oriLabel = currentLabel; }); // click node to show the detail drawer graph.on('node:click', (evt: any) => { stopLayout(); if (!shiftKeydown) clearFocusItemState(graph); else clearFocusEdgeState(graph); const { item } = evt; // highlight the clicked node, it is down by click-select graph.setItemState(item, 'focus', true); if (!shiftKeydown) { // 将相关边也高亮 const relatedEdges = item.getEdges(); relatedEdges.forEach((edge: any) => { graph.setItemState(edge, 'focus', true); }); } }); // click edge to show the detail of integrated edge drawer graph.on('edge:click', (evt: any) => { stopLayout(); if (!shiftKeydown) clearFocusItemState(graph); const { item } = evt; console.log(item); // highlight the clicked edge graph.setItemState(item, 'focus', true); }); // click canvas to cancel all the focus state graph.on('canvas:click', (evt: any) => { clearFocusItemState(graph); console.log( graph.getGroup(), graph.getGroup().getBBox(), graph.getGroup().getCanvasBBox() ); }); }; // 导出渲染函数 export function randerGroph(graphG6Dom: HTMLElement | undefined, data: any) { if (!graphG6Dom) return; // graphG6Dom.value.style.backgroundColor = '#2b2f33'; CANVAS_WIDTH = graphG6Dom.scrollWidth; CANVAS_HEIGHT = (graphG6Dom.scrollHeight || 500) - 30; const nodeMap: any = {}; const clusteredData = louvain(data, false, 'weight'); const aggregatedData: any = { nodes: [], edges: [] }; clusteredData.clusters.forEach((cluster, i) => { cluster.nodes.forEach(node => { const readNode = data.nodes.find((item: any) => item.id === node.id); node = Object.assign(node, readNode); node.level = 0; // node.label = node.id; node.type = ''; node.colorSet = colorSets[i]; nodeMap[node.id] = node; }); const cnode = { id: cluster.id, type: 'aggregated-node', count: cluster.nodes.length, level: 1, label: cluster.id, colorSet: colorSets[i], idx: i, }; aggregatedNodeMap[cluster.id] = cnode; aggregatedData.nodes.push(cnode); }); clusteredData.clusterEdges.forEach(clusterEdge => { const cedge: any = { ...clusterEdge, size: Math.log(clusterEdge.count), // label: '', id: uniqueId('edge'), }; // 自旋 if (cedge.source === cedge.target) { cedge.type = 'loop'; cedge.loopCfg = { dist: 20, }; } else { cedge.type = 'line'; } aggregatedData.edges.push(cedge); }); // data.edges.forEach((edge: any) => { // edge.label = `${edge.source}-${edge.target}`; // edge.id = uniqueId('edge'); // }); currentUnproccessedData = aggregatedData; // 节点上下文菜单 const contextMenu = new Menu({ shouldBegin(evt: any) { if (evt.target && evt.target.isCanvas && evt.target.isCanvas()) return true; if (evt.item) return true; return false; }, getContent(evt: any) { const { item } = evt; if (evt.target && evt.target.isCanvas && evt.target.isCanvas()) { return `