g6/demos/tree-cloudatlas.html
2020-02-14 11:30:12 +08:00

984 lines
30 KiB
HTML
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.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Cloud Atlas Graph</title>
<div id="mountNode"></div>
</head>
<body>
<script src="../build/g6.js"></script>
<script src="../build/minimap.js"></script>
<script src="assets/hierarchy.js"></script>
<script src="./assets/jquery-3.2.1.min.js"></script>
<script>
const ERROR_COLOR = '#F5222D';
const SIMPLE_TREE_NODE = 'simple-tree-node';
const TREE_NODE = 'tree-node';
const SOFAROUTER_TEXT_CLASS = 'sofarouter-text-class';
const SOFAROUTER_RECT_CLASS = 'sofarouter-rect-class';
const CANVAS_WIDTH = 1000;
const CANVAS_HEIGHT = 600;
const LIMIT_OVERFLOW_WIDTH = CANVAS_WIDTH - 100;
const LIMIT_OVERFLOW_HEIGHT = CANVAS_HEIGHT - 100;
const TIP_HEIGHT = 28;
const getNodeConfig = node => {
if (node.nodeError) {
return {
basicColor: ERROR_COLOR,
fontColor: '#FFF',
borderColor: ERROR_COLOR,
bgColor: '#E66A6C',
};
}
let config = {
basicColor: '#722ED1',
fontColor: '#722ED1',
borderColor: '#722ED1',
bgColor: '#F6EDFC',
};
switch (node.type) {
case 'root': {
config = {
basicColor: '#E3E6E8',
fontColor: 'rgba(0,0,0,0.85)',
borderColor: '#E3E6E8',
bgColor: '#F7F9FA',
};
break;
}
case 'httpclient':
case 'rest':
case 'mvc':
case 'rpc':
case 'rpc2jvm':
config = {
basicColor: '#2F54EB',
fontColor: '#2F54EB',
borderColor: '#2F54EB',
bgColor: '#F3F6FD',
};
break;
case 'db':
config = {
basicColor: '#52C41A',
fontColor: '#52C41A',
borderColor: '#52C41A',
bgColor: '#F4FCEB',
};
break;
case 'msgPub':
case 'msgSub':
case 'zqmsgSend':
case 'zqmsgRecv':
case 'antqPub':
case 'antqSub':
config = {
basicColor: '#FA8C16',
fontColor: '#FA8C16',
borderColor: '#FA8C16',
bgColor: '#FCF4E3',
};
break;
case 'zdalTair':
case 'zdalOcs':
case 'zdalOss':
default:
break;
}
return config;
};
const COLLAPSE_ICON = function COLLAPSE_ICON(x, y, r) {
return [
['M', x - r, y],
['a', r, r, 0, 1, 0, r * 2, 0],
['a', r, r, 0, 1, 0, -r * 2, 0],
['M', x - r + 4, y],
['L', x - r + 2 * r - 4, y],
];
};
const EXPAND_ICON = function EXPAND_ICON(x, y, r) {
return [
['M', x - r, y],
['a', r, r, 0, 1, 0, r * 2, 0],
['a', r, r, 0, 1, 0, -r * 2, 0],
['M', x - r + 4, y],
['L', x - r + 2 * r - 4, y],
['M', x - r + r, y - r + 4],
['L', x, y + r - 4],
];
};
/* 精简节点和复杂节点共用的一些方法 */
const nodeBasicMethod = {
createNodeBox: (group, config, width, height, isRoot) => {
/* 最外面的大矩形 */
const container = group.addShape('rect', {
attrs: {
x: 0,
y: 0,
width,
height,
// fill: '#FFF',
// stroke: '#000',
},
});
if (!isRoot) {
/* 左边的小圆点 */
group.addShape('circle', {
attrs: {
x: 3,
y: height / 2,
r: 6,
fill: config.basicColor,
},
});
}
/* 矩形 */
group.addShape('rect', {
attrs: {
x: 3,
y: 0,
width: width - 19,
height,
fill: config.bgColor,
stroke: config.borderColor,
radius: 2,
cursor: 'pointer',
},
});
/* 左边的粗线 */
group.addShape('rect', {
attrs: {
x: 3,
y: 0,
width: 3,
height,
fill: config.basicColor,
radius: 1.5,
},
});
return container;
},
/* 生成树上的 marker */
createNodeMarker: (group, collapsed, x, y) => {
group.addShape('circle', {
attrs: {
x,
y,
r: 13,
fill: 'rgba(47, 84, 235, 0.05)',
opacity: 0,
zIndex: -2,
},
className: 'collapse-icon-bg',
});
group.addShape('marker', {
attrs: {
x,
y,
radius: 7,
symbol: collapsed ? EXPAND_ICON : COLLAPSE_ICON,
stroke: 'rgba(0,0,0,0.25)',
fill: 'rgba(0,0,0,0)',
lineWidth: 1,
cursor: 'pointer',
},
className: 'collapse-icon',
});
},
afterDraw: (cfg, group) => {
/* 操作 marker 的背景色显示隐藏 */
const icon = group.findByClassName('collapse-icon');
if (icon) {
const bg = group.findByClassName('collapse-icon-bg');
icon.on('mouseenter', () => {
bg.attr('opacity', 1);
graph.get('canvas').draw();
});
icon.on('mouseleave', () => {
bg.attr('opacity', 0);
graph.get('canvas').draw();
});
}
/* ip 显示 */
const ipBox = group.findByClassName('ip-box');
if (ipBox) {
/* ip 复制的几个元素 */
const ipLine = group.findByClassName('ip-cp-line');
const ipBG = group.findByClassName('ip-cp-bg');
const ipIcon = group.findByClassName('ip-cp-icon');
const ipCPBox = group.findByClassName('ip-cp-box');
const onMouseEnter = () => {
this.ipHideTimer && clearTimeout(this.ipHideTimer);
ipLine.attr('opacity', 1);
ipBG.attr('opacity', 1);
ipIcon.attr('opacity', 1);
graph.get('canvas').draw();
};
const onMouseLeave = () => {
this.ipHideTimer = setTimeout(() => {
ipLine.attr('opacity', 0);
ipBG.attr('opacity', 0);
ipIcon.attr('opacity', 0);
graph.get('canvas').draw();
}, 100);
};
ipBox.on('mouseenter', () => {
onMouseEnter();
});
ipBox.on('mouseleave', () => {
onMouseLeave();
});
ipCPBox.on('mouseenter', () => {
onMouseEnter();
});
ipCPBox.on('mouseleave', () => {
onMouseLeave();
});
ipCPBox.on('click', () => {});
}
},
setState: (name, value, item) => {
const hasOpacityClass = [
'ip-cp-line',
'ip-cp-bg',
'ip-cp-icon',
'ip-cp-box',
'ip-box',
'collapse-icon-bg',
];
const group = item.getContainer();
const childrens = group.get('children');
graph.setAutoPaint(false);
if (name === 'emptiness') {
if (value) {
childrens.forEach(shape => {
if (hasOpacityClass.indexOf(shape.get('className')) > -1) {
return;
}
shape.attr('opacity', 0.4);
});
} else {
childrens.forEach(shape => {
if (hasOpacityClass.indexOf(shape.get('className')) > -1) {
return;
}
shape.attr('opacity', 1);
});
}
}
graph.setAutoPaint(true);
},
};
/* 精简节点 */
G6.registerNode(
SIMPLE_TREE_NODE,
{
drawShape: (cfg, group) => {
const config = getNodeConfig(cfg);
const isRoot = cfg.type === 'root';
const nodeError = cfg.nodeError;
const container = nodeBasicMethod.createNodeBox(group, config, 171, 38, isRoot);
/* name */
const nameText = group.addShape('text', {
attrs: {
text: cfg.name,
x: 19,
y: 19,
fontSize: 14,
fontWeight: 700,
textAlign: 'left',
textBaseline: 'middle',
fill: config.fontColor,
cursor: 'pointer',
},
});
/* 修复 nameText 超长 */
fittingString(nameText, cfg.name, 133);
if (nodeError) {
group.addShape('image', {
attrs: {
x: 119,
y: 5,
height: 35,
width: 35,
img: '/static/images/warning-circle.svg',
},
});
}
const hasChildren = cfg.children && cfg.children.length > 0;
if (hasChildren) {
nodeBasicMethod.createNodeMarker(group, cfg.collapsed, 164, 19);
}
return container;
},
afterDraw: nodeBasicMethod.afterDraw,
setState: nodeBasicMethod.setState,
},
'single-shape',
);
/* 复杂节点 */
G6.registerNode(
TREE_NODE,
{
drawShape: (cfg, group) => {
const config = getNodeConfig(cfg);
const isRoot = cfg.type === 'root';
const nodeError = cfg.nodeError;
/* 最外面的大矩形 */
const container = nodeBasicMethod.createNodeBox(group, config, 243, 64, isRoot);
if (cfg.type !== 'root') {
/* 上边的 type */
group.addShape('text', {
attrs: {
text: cfg.type,
x: 3,
y: -10,
fontSize: 12,
textAlign: 'left',
textBaseline: 'middle',
fill: 'rgba(0,0,0,0.65)',
},
});
}
let ipWidth = 0;
if (cfg.ip) {
/* ip start */
/* ipBox */
const ipRect = group.addShape('rect', {
attrs: {
fill: nodeError ? null : '#FFF',
stroke: nodeError ? 'rgba(255,255,255,0.65)' : null,
radius: 2,
cursor: 'pointer',
},
});
/* ip */
const ipText = group.addShape('text', {
attrs: {
text: cfg.ip,
x: 0,
y: 19,
fontSize: 12,
textAlign: 'left',
textBaseline: 'middle',
fill: nodeError ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.65)',
cursor: 'pointer',
},
});
const ipBBox = ipText.getBBox();
/* ip 的文字总是距离右边 12px */
ipText.attr({
x: 224 - 12 - ipBBox.width,
});
/* ipBox */
ipRect.attr({
x: 224 - 12 - ipBBox.width - 4,
y: ipBBox.minY - 5,
width: ipBBox.width + 8,
height: ipBBox.height + 10,
});
/* 在 IP 元素上面覆盖一层透明层,方便监听 hover 事件 */
group.addShape('rect', {
attrs: {
stroke: '',
cursor: 'pointer',
x: 224 - 12 - ipBBox.width - 4,
y: ipBBox.minY - 5,
width: ipBBox.width + 8,
height: ipBBox.height + 10,
fill: '#fff',
opacity: 0,
},
className: 'ip-box',
});
/* copyIpLine */
group.addShape('rect', {
attrs: {
x: 194,
y: 7,
width: 1,
height: 24,
fill: '#E3E6E8',
opacity: 0,
},
className: 'ip-cp-line',
});
/* copyIpBG */
group.addShape('rect', {
attrs: {
x: 195,
y: 8,
width: 22,
height: 22,
fill: '#FFF',
cursor: 'pointer',
opacity: 0,
},
className: 'ip-cp-bg',
});
/* copyIpIcon */
group.addShape('image', {
attrs: {
x: 200,
y: 13,
height: 12,
width: 10,
img: 'https://os.alipayobjects.com/rmsportal/DFhnQEhHyPjSGYW.png',
cursor: 'pointer',
opacity: 0,
},
className: 'ip-cp-icon',
});
/* 放一个透明的矩形在 icon 区域上,方便监听点击 */
group.addShape('rect', {
attrs: {
x: 195,
y: 8,
width: 22,
height: 22,
fill: '#FFF',
cursor: 'pointer',
opacity: 0,
},
className: 'ip-cp-box',
tooltip: '复制IP',
});
const ipRectBBox = ipRect.getBBox();
ipWidth = ipRectBBox.width;
/* ip end */
}
/* name */
const nameText = group.addShape('text', {
attrs: {
text: cfg.name,
x: 19,
y: 19,
fontSize: 14,
fontWeight: 700,
textAlign: 'left',
textBaseline: 'middle',
fill: config.fontColor,
cursor: 'pointer',
},
// tooltip: cfg.name,
});
/* 根据 IP 的长度计算出 剩下的 留给 name 的长度! */
/* 修复 nameText 超长 */
fittingString(nameText, cfg.name, 224 - ipWidth - 20);
/* 下面的文字 */
const remarkText = group.addShape('text', {
attrs: {
text: cfg.keyInfo,
x: 19,
y: 45,
fontSize: 14,
textAlign: 'left',
textBaseline: 'middle',
fill: config.fontColor,
cursor: 'pointer',
},
// className: 'keyInfo',
// tooltip: cfg.keyInfo,
});
fittingString(remarkText, cfg.keyInfo, 204);
if (nodeError) {
group.addShape('image', {
attrs: {
x: 191,
y: 32,
height: 35,
width: 35,
img: '/static/images/warning-circle.svg',
},
});
}
const hasChildren = cfg.children && cfg.children.length > 0;
if (hasChildren) {
nodeBasicMethod.createNodeMarker(group, cfg.collapsed, 236, 32);
}
return container;
},
afterDraw: nodeBasicMethod.afterDraw,
setState: nodeBasicMethod.setState,
},
'single-shape',
);
/* 是否显示 sofarouter通过透明度来控制 */
G6.registerEdge(
'tree-edge',
{
draw(cfg, group) {
const targetNode = cfg.targetNode.getModel();
const edgeError = !!targetNode.edgeError;
const startPoint = cfg.startPoint;
const endPoint = cfg.endPoint;
const controlPoints = this.getControlPoints(cfg);
let points = [startPoint]; // 添加起始点
// 添加控制点
if (controlPoints) {
points = points.concat(controlPoints);
}
// 添加结束点
points.push(endPoint);
const path = this.getPath(points);
group.addShape('path', {
attrs: {
path,
lineWidth: 12,
stroke: edgeError ? 'rgba(245,34,45,0.05)' : 'rgba(47,84,235,0.05)',
opacity: 0,
zIndex: 0,
},
className: 'line-bg',
});
const keyShape = group.addShape('path', {
attrs: {
path,
lineWidth: 1,
stroke: edgeError ? '#FF7875' : 'rgba(0,0,0,0.25)',
zIndex: 1,
lineAppendWidth: 12,
},
edgeError: !!edgeError,
});
/* 连接线的中间点 */
const centerPoint = {
x: startPoint.x + (endPoint.x - startPoint.x) / 2,
y: startPoint.y + (endPoint.y - startPoint.y) / 2,
};
const textRect = group.addShape('rect', {
attrs: {
fill: '#FFF1F0',
radius: 2,
cursor: 'pointer',
opacity: 1,
},
/* sofarouter 需要 class以便控制 显示隐藏*/
className: SOFAROUTER_RECT_CLASS,
});
const text = group.addShape('text', {
attrs: {
text: 'Sofarouter',
x: 0,
y: 0,
fontSize: 12,
textAlign: 'left',
textBaseline: 'middle',
fill: '#F5222D',
opacity: 1,
},
/* sofarouter 需要 class以便控制 显示隐藏*/
className: SOFAROUTER_TEXT_CLASS,
});
const textBBox = text.getBBox();
/* text 的位置 */
text.attr({
x: centerPoint.x - textBBox.width / 2,
y: centerPoint.y,
});
/* text 的框框 */
textRect.attr({
x: centerPoint.x - textBBox.width / 2 - 4,
y: centerPoint.y - textBBox.height / 2 - 5,
width: textBBox.width + 8,
height: textBBox.height + 10,
});
return keyShape;
},
/* 操作 线 的背景色显示隐藏 */
afterDraw: (cfg, group) => {
/* 背景色 */
const lineBG = group.get('children')[0]; // 顺序根据 draw 时确定
/* 线条 */
const line = group.get('children')[1];
line.on('mouseenter', () => {
lineBG.attr('opacity', '1');
/* 线条如果在没有错误的情况下,在 hover 时候,是需要变成蓝色的 */
if (!line.get('edgeError')) {
line.attr('stroke', '#2F54EB');
}
graph.get('canvas').draw();
});
line.on('mouseleave', () => {
lineBG.attr('opacity', '0');
if (!line.get('edgeError')) {
line.attr('stroke', 'rgba(0,0,0,0.25)');
}
graph.get('canvas').draw();
});
},
setState: (name, value, item) => {
const group = item.getContainer();
const childrens = group.get('children');
graph.setAutoPaint(true);
if (name === 'emptiness') {
if (value) {
childrens.forEach(shape => {
if (shape.get('className') === 'line-bg') {
return;
}
shape.attr('opacity', 0.4);
});
} else {
childrens.forEach(shape => {
if (shape.get('className') === 'line-bg') {
return;
}
shape.attr('opacity', 1);
});
}
}
graph.setAutoPaint(true);
},
update: null,
},
'cubic-horizontal',
);
G6.registerBehavior('three-finger-drag-canvas', {
getEvents() {
return {
'canvas:dragstart': 'onDragStart',
'canvas:drag': 'onDrag',
'canvas:dragend': 'onDragEnd',
};
},
onDragStart: ev => {
ev.preventDefault();
this.dragDx = ev.x;
this.dragDy = ev.y;
},
onDrag: ev => {
ev.preventDefault();
translate(this.dragDx - ev.x, this.dragDy - ev.y);
},
onDragEnd: ev => {
ev.preventDefault();
},
});
G6.registerBehavior('double-finger-drag-canvas', {
getEvents() {
return {
wheel: 'onWheel',
};
},
onWheel: ev => {
if (ev.ctrlKey) {
const canvas = graph.get('canvas');
const point = canvas.getPointByClient(ev.clientX, ev.clientY);
let ratio = graph.getZoom();
if (ev.wheelDelta > 0) {
ratio = ratio + ratio * 0.05;
} else {
ratio = ratio - ratio * 0.05;
}
graph.zoomTo(ratio, {
x: point.x,
y: point.y,
});
} else {
const x = ev.deltaX || ev.movementX;
const y = ev.deltaY || ev.movementY;
translate(x, y);
}
ev.preventDefault();
},
});
/*G6.registerBehavior("tooltip", {
getEvents() {
return {
"node:mousemove": "onMouseMove",
"node:mouseeleave": "onMouseLeave"
};
},
onMouseMove: ev => {
const {
tooltip: { visible }
} = this.state;
const tooltip = ev.target.get("tooltip");
const group = ev.item.get("group");
if (tooltip && !visible) {
const bbox = ev.target.getBBox();
const matrix = group.getMatrix();
const zoom = graph.getZoom();
this.showTooltip(
tooltip,
matrix[6] + bbox.x + bbox.width / 2,
matrix[7] + bbox.y - TIP_HEIGHT / zoom
);
}
if (!tooltip && visible) {
this.closeTooptip();
}
},
onMouseLeave: () => {
const {
tooltip: { visible }
} = this.state;
if (visible) {
this.closeTooptip();
}
}
});*/
const minimap = new Minimap({
size: [184, 124],
className: 'minimap',
type: 'delegate',
});
let selectedItem;
graph = new G6.TreeGraph({
container: 'mountNode',
width: CANVAS_WIDTH,
height: CANVAS_HEIGHT,
plugins: [minimap],
modes: {
default: [
{
type: 'collapse-expand',
shouldUpdate: function shouldUpdate(e) {
/* 点击 node 禁止展开收缩 */
if (e.target.get('className') !== 'collapse-icon') {
return false;
}
return true;
},
onChange: function onChange(item, collapsed) {
selectedItem = item;
const icon = item.get('group').findByClassName('collapse-icon');
if (collapsed) {
icon.attr('symbol', EXPAND_ICON);
} else {
icon.attr('symbol', COLLAPSE_ICON);
}
},
animate: {
callback: () => {
debugger;
graph.focusItem(selectedItem);
},
},
},
'double-finger-drag-canvas',
'three-finger-drag-canvas',
'tooltip',
{
type: 'drag-canvas',
shouldUpdate: function shouldUpdate() {
return false;
},
shouldEnd: function shouldUpdate() {
return false;
},
},
],
},
defaultNode: {
shape: TREE_NODE,
anchorPoints: [
[0, 0.5],
[1, 0.5],
],
},
defaultEdge: {
shape: 'tree-edge',
style: {
stroke: '#A3B1BF',
},
},
layout: data => {
const result = Hierarchy.compactBox(data, {
direction: 'LR',
getId: function getId(d) {
return d.id;
},
getWidth: () => {
return 243;
},
getVGap: function getVGap() {
return 24;
},
getHGap: function getHGap() {
return 50;
},
});
return result;
},
});
function strLen(str) {
var len = 0;
for (var i = 0; i < str.length; i++) {
if (str.charCodeAt(i) > 0 && str.charCodeAt(i) < 128) {
len++;
} else {
len += 2;
}
}
return len;
}
function fittingString(c, str, maxWidth) {
const width = strLen(str) * 8;
const ellipsis = '…';
if (width > maxWidth) {
const actualLen = Math.floor((maxWidth - 10) / 8);
c.attr('text', str.substring(0, actualLen) + ellipsis);
c._cfg.tooltip = str;
}
}
function translate(x, y) {
// graph.translate(-x, -y);
let moveX = x;
let moveY = y;
const containerWidth = graph.get('width');
const containerHeight = graph.get('height');
/* 获得当前偏移量*/
const group = graph.get('group');
const bbox = group.getBBox();
const leftTopPoint = graph.getCanvasByPoint(bbox.minX, bbox.minY);
const rightBottomPoint = graph.getCanvasByPoint(bbox.maxX, bbox.maxY);
/* 如果 x 轴在区域内不允许左右超过100 */
if (x < 0 && leftTopPoint.x - x > LIMIT_OVERFLOW_WIDTH) {
moveX = 0;
}
if (x > 0 && rightBottomPoint.x - x < containerWidth - LIMIT_OVERFLOW_WIDTH) {
moveX = 0;
}
if (y < 0 && leftTopPoint.y - y > LIMIT_OVERFLOW_HEIGHT) {
moveY = 0;
}
if (y > 0 && rightBottomPoint.y - y < containerHeight - LIMIT_OVERFLOW_HEIGHT) {
moveY = 0;
}
graph.translate(-moveX, -moveY);
}
function fitView() {
const group = graph.get('group');
const width = graph.get('width'); // 视窗的宽度
const height = graph.get('height'); // 视窗的高度
group.resetMatrix();
// 内容
const bbox = group.getBBox();
// 视窗中间
const viewCenter = {
x: width / 2,
y: height / 2,
};
/* 内容的中点 */
const groupCenter = {
x: bbox.x + bbox.width / 2,
y: bbox.y + bbox.height / 2,
};
graph.translate(-bbox.x + 16, viewCenter.y - groupCenter.y);
}
function zoomTo(ratio, center) {
const width = graph.get('width'); // 视窗的宽度
const height = graph.get('height'); // 视窗的高度
const viewCenter = {
x: width / 2,
y: height / 2,
};
graph.zoomTo(ratio, center || viewCenter);
}
function formatData(data) {
const recursiveTraverse = (node, level = 0) => {
// TODO 贤月 目前 keyInfo 没有
const appName = 'testappName';
const keyInfo = 'testkeyinfo';
const ip = '111.22.33.44';
const targetNode = {
id: node.key + '',
rpcId: node.rpcId,
level,
type: node.appName === 'USER' ? 'root' : node.type,
name: appName,
keyInfo: keyInfo || '-',
ip,
nodeError: false,
edgeError: false,
sofarouter: true,
sofarouterError: true,
asyn: false,
origin: node,
};
if (node.children) {
targetNode.children = [];
node.children.forEach(item => {
targetNode.children.push(recursiveTraverse(item, level + 1));
});
}
return targetNode;
};
const result = recursiveTraverse(data);
return result;
}
graph.on('beforepaint', () => {
const topLeft = graph.getPointByCanvas(0, 0);
const bottomRight = graph.getPointByCanvas(1000, 600);
graph.getNodes().forEach(node => {
const model = node.getModel();
if (
model.x < topLeft.x - 200 ||
model.x > bottomRight.x ||
model.y < topLeft.y ||
model.y > bottomRight.y
) {
node.getContainer().hide();
} else {
node.getContainer().show();
}
});
const edges = graph.getEdges();
edges.forEach(edge => {
const sourceNode = edge.get('sourceNode');
const targetNode = edge.get('targetNode');
if (!sourceNode.get('visible') && !targetNode.get('visible')) {
edge.hide();
} else {
edge.show();
}
});
});
$.getJSON('./assets/sourceData.json', data => {
data = formatData(data);
graph.data(data);
graph.render();
graph.fitView();
});
</script>
</body>
</html>