1756 lines
50 KiB
TypeScript
1756 lines
50 KiB
TypeScript
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();
|
||
}
|
||
}
|