Files
fe.ems.vue3/src/views/monitor/topology/graph.ts
2023-12-21 20:44:05 +08:00

1756 lines
50 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 `
<div
style="
display: flex;
flex-direction: column;
width: 140px;
"
>
<div id="show" style="cursor: pointer; margin-bottom: 2px">
1. 显示所有隐藏项
</div>
<div id="collapseAll" style="cursor: pointer; margin-bottom: 2px">
2. 折叠所有集群
</div>
</div>`;
}
if (!item) return '';
const itemType = item.getType();
const model = item.getModel();
if (itemType && model) {
if (itemType === 'node') {
if (model.level !== 0) {
return `
<div
style="
display: flex;
flex-direction: column;
width: 100px;
background: #e6f7ff;
"
>
<div id="expand" style="cursor: pointer; margin-bottom: 2px">
1. 展开集群
</div>
<div id="hide" style="cursor: pointer; margin-bottom: 2px">
2. 隐藏节点
</div>
</div>
`;
} else {
return `
<div
style="
display: flex;
flex-direction: column;
width: 160px;
background: #e6f7ff;
"
>
<div id="collapse" style="cursor: pointer; margin-bottom: 2px">
1. 折叠集群
</div>
<div id="hide" style="cursor: pointer; margin-bottom: 2px">
2. 隐藏节点
</div>
</div>
`;
}
} else {
return `
<div
style="
display: flex;
flex-direction: column;
width: 100px;
background: #e6f7ff;
"
>
<div id="hide" style="cursor: pointer; margin-bottom: 2px">
1. 隐藏边
</div>
</div>
`;
}
}
return '';
},
handleMenuClick: (target, item) => {
const model: any = item && item.getModel();
const liIdStrs = target.id.split('-');
let mixedGraphData;
switch (liIdStrs[0]) {
case 'hide':
graph.hideItem(item);
hiddenItemIds.push(model.id);
break;
case 'expand':
const newArray = manageExpandCollapseArray(
graph.getNodes().length,
model,
collapseArray,
expandArray
);
expandArray = newArray.expandArray;
collapseArray = newArray.collapseArray;
mixedGraphData = getMixedGraph(
clusteredData,
data,
nodeMap,
aggregatedNodeMap,
expandArray,
collapseArray
);
break;
case 'collapse':
const aggregatedNode = aggregatedNodeMap[model.clusterId];
manipulatePosition = { x: aggregatedNode.x, y: aggregatedNode.y };
collapseArray.push(aggregatedNode);
for (let i = 0; i < expandArray.length; i++) {
if (expandArray[i].id === model.clusterId) {
expandArray.splice(i, 1);
break;
}
}
mixedGraphData = getMixedGraph(
clusteredData,
data,
nodeMap,
aggregatedNodeMap,
expandArray,
collapseArray
);
break;
case 'collapseAll':
expandArray = [];
collapseArray = [];
mixedGraphData = getMixedGraph(
clusteredData,
data,
nodeMap,
aggregatedNodeMap,
expandArray,
collapseArray
);
break;
case 'restart':
console.log('restart');
break;
case 'show':
showItems(graph);
break;
default:
break;
}
if (mixedGraphData) {
cachePositions = cacheNodePositions(graph.getNodes());
currentUnproccessedData = mixedGraphData;
handleRefreshGraph(
graph,
currentUnproccessedData,
CANVAS_WIDTH,
CANVAS_HEIGHT,
largeGraphMode,
true
);
}
},
// offsetX and offsetY include the padding of the parent container
// 需要加上父级容器的 padding-left 16 与自身偏移量 10
offsetX: 16 + 10,
// 需要加上父级容器的 padding-top 24 、画布兄弟元素高度、与自身偏移量 10
offsetY: 0,
// the types of items that allow the menu show up
// 在哪些类型的元素上响应
itemTypes: ['node', 'edge', 'canvas'],
});
// 节点提示菜单
const tooltip = new Tooltip({
offsetX: 20,
offsetY: 20,
getContent(e: any) {
const node = e.item.getModel();
const neInfo = node.info;
if (!neInfo) {
return `<div><span>ID</span><span>${node.id}</span></div>`;
}
const serverState = neInfo.serverState;
console.log(neInfo);
const hasNeID = serverState.neId;
if (!hasNeID) {
return '<div><span>状态:</span><span>异常</span></div>';
}
return `
<div
style="
display: flex;
flex-direction: column;
width: 200px;
"
>
<div><span>状态:</span><span>
${hasNeID ? '正常' : '异常'}
</span></div>
<div><span>刷新时间:</span><span>
${serverState.refreshTime ?? '--'}
</span></div>
<div><span>ID</span><span>${hasNeID}</span></div>
<div><span>名称:</span><span>${serverState.neName ?? '--'}</span></div>
<div><span>版本:</span><span>
${serverState.version ?? '--'}
</span></div>
<div><span>SN</span><span>${serverState.sn ?? '--'}</span></div>
<div><span>有效期:</span><span>
${serverState.expire ?? '--'}
</span></div>
</div>
`;
},
itemTypes: ['node'],
});
const { edges: processedEdges } = processNodesEdges(
currentUnproccessedData.nodes,
currentUnproccessedData.edges,
CANVAS_WIDTH,
CANVAS_HEIGHT,
true,
true
);
graph = new Graph({
container: graphG6Dom,
width: graphG6Dom?.clientWidth,
height: graphG6Dom?.clientHeight,
linkCenter: true,
minZoom: 0.1,
groupByTypes: false,
modes: {
default: [
{
type: 'drag-canvas',
enableOptimize: true,
},
{
type: 'zoom-canvas',
enableOptimize: true,
optimizeZoom: 0.01,
},
'drag-node',
],
lassoSelect: [
{
type: 'zoom-canvas',
enableOptimize: true,
optimizeZoom: 0.01,
},
{
type: 'lasso-select',
selectedState: 'focus',
trigger: 'drag',
},
],
fisheyeMode: [],
},
defaultNode: {
type: 'aggregated-node',
size: DEFAULTNODESIZE,
},
plugins: [contextMenu, tooltip],
});
graph.get('canvas').set('localRefresh', false);
const layoutConfig = getForceLayoutConfig(graph, largeGraphMode);
layoutConfig.center = [CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2];
layout.instance = new Layout['gForce'](layoutConfig);
layout.instance.init({
nodes: aggregatedData.nodes,
edges: processedEdges,
});
layout.instance.execute();
bindListener(graph);
graph.data({ nodes: aggregatedData.nodes, edges: processedEdges });
graph.render();
// 节点展开
for (const model of aggregatedData.nodes) {
const newArray = manageExpandCollapseArray(
graph.getNodes().length,
model,
collapseArray,
expandArray
);
expandArray = newArray.expandArray;
collapseArray = newArray.collapseArray;
let mixedGraphData = getMixedGraph(
clusteredData,
data,
nodeMap,
aggregatedNodeMap,
expandArray,
collapseArray
);
if (mixedGraphData) {
cachePositions = cacheNodePositions(graph.getNodes());
let currentUnproccessedData = mixedGraphData;
handleRefreshGraph(
graph,
currentUnproccessedData,
CANVAS_WIDTH,
CANVAS_HEIGHT,
largeGraphMode,
true
);
}
}
return graph;
}
let gridLayout = false;
export function switchLayout() {
gridLayout = !gridLayout;
if (gridLayout) {
stopLayout();
graph.updateLayout({
type: 'grid',
begin: [20, 20],
});
} else {
layout.instance.execute();
}
}