mirror of
https://gitee.com/antv/g6.git
synced 2024-12-05 05:09:07 +08:00
commit
5e4dba8c3f
60
demos/arc-circle.html
Normal file
60
demos/arc-circle.html
Normal file
@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>自定义弧形节点</title>
|
||||
<script src="../build/g6.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mountNode"></div>
|
||||
<script>
|
||||
G6.registerNode('fannode', {
|
||||
draw(cfg, group) {
|
||||
const keyShape = group.addShape('fan', {
|
||||
attrs: {
|
||||
x: 50,
|
||||
y: 50,
|
||||
re: 40,
|
||||
rs: 30,
|
||||
startAngle: 1/2*Math.PI,
|
||||
endAngle: Math.PI,
|
||||
clockwise: false,
|
||||
fill: '#b7eb8f'
|
||||
}
|
||||
})
|
||||
return keyShape
|
||||
}
|
||||
})
|
||||
|
||||
const data = {
|
||||
nodes: [{
|
||||
id: 'node1',
|
||||
x: 100,
|
||||
y: 200
|
||||
},{
|
||||
id: 'node2',
|
||||
x: 300,
|
||||
y: 200
|
||||
}],
|
||||
edges: [{
|
||||
id: 'edge1',
|
||||
target: 'node2',
|
||||
source: 'node1'
|
||||
}]
|
||||
};
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 500,
|
||||
height: 500,
|
||||
defaultNode: {
|
||||
shape: 'fannode'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#91d5ff'
|
||||
}
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
493
demos/card-node.html
Normal file
493
demos/card-node.html
Normal file
@ -0,0 +1,493 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>自定义卡片节点</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mountNode"></div>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script>
|
||||
const ERROR_COLOR = "#F5222D";
|
||||
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;
|
||||
}
|
||||
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
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算字符串的长度
|
||||
* @param {string} str 指定的字符串
|
||||
*/
|
||||
const calcStrLen = (str) => {
|
||||
let len = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (str.charCodeAt(i) > 0 && str.charCodeAt(i) < 128) {
|
||||
len++;
|
||||
} else {
|
||||
len += 2;
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
G6.registerNode(
|
||||
'card-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,
|
||||
});
|
||||
|
||||
/* 下面的文字 */
|
||||
const remarkText = group.addShape("text", {
|
||||
attrs: {
|
||||
text: cfg.keyInfo,
|
||||
x: 19,
|
||||
y: 45,
|
||||
fontSize: 14,
|
||||
textAlign: "left",
|
||||
textBaseline: "middle",
|
||||
fill: config.fontColor,
|
||||
cursor: "pointer"
|
||||
}
|
||||
});
|
||||
|
||||
if (nodeError) {
|
||||
group.addShape("text", {
|
||||
attrs: {
|
||||
x: 191,
|
||||
y: 62,
|
||||
text: '⚠️',
|
||||
fill: '#000',
|
||||
fontSize: 18
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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"
|
||||
);
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
name: 'cardNodeApp',
|
||||
ip: '127.0.0.1',
|
||||
nodeError: true,
|
||||
type: 'root',
|
||||
keyInfo: 'this is a card node info',
|
||||
x: 100,
|
||||
y: 50
|
||||
},
|
||||
{
|
||||
name: 'cardNodeApp',
|
||||
ip: '127.0.0.1',
|
||||
nodeError: false,
|
||||
type: 'subRoot',
|
||||
keyInfo: 'this is sub root',
|
||||
x: 100,
|
||||
y: 150
|
||||
},
|
||||
{
|
||||
name: 'cardNodeApp',
|
||||
ip: '127.0.0.1',
|
||||
nodeError: false,
|
||||
type: 'subRoot',
|
||||
keyInfo: 'this is sub root',
|
||||
x: 100,
|
||||
y: 250,
|
||||
children: [
|
||||
{
|
||||
name: 'sub'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
edges: []
|
||||
}
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 800,
|
||||
height: 600,
|
||||
defaultNode: {
|
||||
shape: 'card-node'
|
||||
}
|
||||
})
|
||||
|
||||
graph.data(data)
|
||||
graph.render()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -6,123 +6,10 @@
|
||||
</head>
|
||||
<body>
|
||||
<button id='changeView'>改变布局</button>
|
||||
<div id="mountNode"></div>
|
||||
<div id="mountNode" style="background: #ccc;"></div>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script src="../build//grid.js"></script>
|
||||
<script src="../build/grid.js"></script>
|
||||
<script>
|
||||
|
||||
G6.registerBehavior('click-add-node', {
|
||||
getEvents() {
|
||||
return {
|
||||
'node:click': 'onClick'
|
||||
};
|
||||
},
|
||||
onClick(ev) {
|
||||
const itemModel = ev.item.getModel();
|
||||
clickedNodeId = itemModel.id;
|
||||
const graph = this.graph;
|
||||
const nodes = graph.getNodes();
|
||||
const edges = graph.getEdges();
|
||||
let newData;
|
||||
if (itemModel.id == 2) {
|
||||
newData = data2_m;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
let newNodeModels = newData.nodes;
|
||||
let newEdgeModels = [];
|
||||
// deduplication the items in newEdgeModels
|
||||
newData.edges.forEach(e => {
|
||||
let exist = false;
|
||||
newEdgeModels.forEach(ne => {
|
||||
if(ne.source === e.source && ne.target === e.target) exist = true;
|
||||
});
|
||||
if(!exist) {
|
||||
newEdgeModels.push(e);
|
||||
}
|
||||
});
|
||||
|
||||
// for graph.changeData()
|
||||
const allNodeModels = [];
|
||||
const allEdgeModels = [];
|
||||
|
||||
// add new nodes to graph
|
||||
const nodeMap = new Map();
|
||||
nodes.forEach((n, i)=>{
|
||||
const nModel = n.getModel();
|
||||
nodeMap.set(nModel.id, i);
|
||||
});
|
||||
|
||||
newNodeModels.forEach((nodeModel, i) => {
|
||||
if (nodeMap.get(nodeModel.id) === undefined) {
|
||||
// set the initial positions of the new nodes to the focus(clicked) node
|
||||
nodeModel.x = itemModel.x;
|
||||
nodeModel.y = itemModel.y;
|
||||
const node = graph.addItem('node', nodeModel);
|
||||
}
|
||||
});
|
||||
|
||||
// add new edges to graph
|
||||
const edgeMap = new Map();
|
||||
edges.forEach((e, i)=>{
|
||||
const eModel = e.getModel();
|
||||
edgeMap.set(eModel.source+","+eModel.target, i);
|
||||
});
|
||||
|
||||
const oldEdgeNum = edges.length;
|
||||
newEdgeModels.forEach((em, i) => {
|
||||
const exist = edgeMap.get(em.source+","+em.target);
|
||||
if (exist === undefined) {
|
||||
const edge = graph.addItem('edge', em);
|
||||
edgeMap.set(em.source+","+em.target, oldEdgeNum+i);
|
||||
}
|
||||
});
|
||||
|
||||
edges.forEach((e, i) => {
|
||||
allEdgeModels.push(e.getModel());
|
||||
});
|
||||
|
||||
nodes.forEach((n, i) => {
|
||||
allNodeModels.push(n.getModel());
|
||||
});
|
||||
// the max degree about foces(clicked) node in the newly added data
|
||||
const maxDegree = 4;
|
||||
// the max degree about foces(clicked) node in the original data
|
||||
const oMaxDegree = 3;
|
||||
const unitRadius = 40;
|
||||
const focusNodeId = "2";
|
||||
// re-place the clicked node far away the exisiting items
|
||||
// along the radius from center node to it
|
||||
const vx = itemModel.x - focusNode.x;
|
||||
const vy = itemModel.y - focusNode.y;
|
||||
const vlength = Math.sqrt(vx*vx+vy*vy);
|
||||
const ideallength = unitRadius * maxDegree + mainUnitRadius * oMaxDegree;
|
||||
itemModel.x = ideallength * vx / vlength + focusNode.x;
|
||||
itemModel.y = ideallength * vy / vlength + focusNode.y;
|
||||
|
||||
const subRadialLayout = new Radial({
|
||||
center: [ itemModel.x, itemModel.y ],
|
||||
maxIteration: 200,
|
||||
focusNode: "2",
|
||||
unitRadius,
|
||||
linkDistance: 180
|
||||
});
|
||||
graph.addPlugin(subRadialLayout);
|
||||
// only layout the newly added part around the clicked node
|
||||
subRadialLayout.layout(
|
||||
{
|
||||
'nodes': newNodeModels,
|
||||
'edges': newEdgeModels
|
||||
}
|
||||
);
|
||||
graph.changeData({"nodes": allNodeModels, "edges": allEdgeModels});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
const grid = new Grid()
|
||||
const graph = new G6.TreeGraph({
|
||||
plugins: [grid],
|
||||
@ -165,7 +52,7 @@
|
||||
});
|
||||
|
||||
let data = {
|
||||
isRoot: true,
|
||||
// isRoot: true,
|
||||
id: 'Root',
|
||||
style: {
|
||||
fill: 'red'
|
||||
|
@ -9,7 +9,6 @@
|
||||
<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";
|
||||
@ -603,7 +602,7 @@
|
||||
});
|
||||
const text = group.addShape("text", {
|
||||
attrs: {
|
||||
text: "Sofarouter",
|
||||
text: "edge-label",
|
||||
x: 0,
|
||||
y: 0,
|
||||
fontSize: 12,
|
||||
@ -806,7 +805,11 @@
|
||||
},
|
||||
"double-finger-drag-canvas",
|
||||
"three-finger-drag-canvas",
|
||||
"tooltip",
|
||||
{ type: "tooltip",
|
||||
formatText: data => {
|
||||
return `<div>${data.name}</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "drag-canvas",
|
||||
shouldUpdate: function shouldUpdate() {
|
||||
@ -830,8 +833,8 @@
|
||||
stroke: "#A3B1BF"
|
||||
}
|
||||
},
|
||||
layout: data => {
|
||||
const result = Hierarchy.compactBox(data, {
|
||||
layout: {
|
||||
type: 'compactBox',
|
||||
direction: "LR",
|
||||
getId: function getId(d) {
|
||||
return d.id;
|
||||
@ -845,8 +848,6 @@
|
||||
getHGap: function getHGap() {
|
||||
return 50;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
});
|
||||
function strLen(str) {
|
||||
@ -934,7 +935,6 @@
|
||||
};
|
||||
function formatData(data) {
|
||||
const recursiveTraverse = (node, level = 0) => {
|
||||
// TODO 贤月 目前 keyInfo 没有
|
||||
const appName = "testappName";
|
||||
const keyInfo = "testkeyinfo";
|
||||
const ip = "111.22.33.44";
|
||||
|
141
demos/collapse-expand-group.html
Normal file
141
demos/collapse-expand-group.html
Normal file
@ -0,0 +1,141 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>收起/展开群组</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mountNode"></div>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script>
|
||||
/**
|
||||
* 该案例演示以下功能:
|
||||
* 1、双击收起群组;
|
||||
* 2、双击展开群组。
|
||||
*/
|
||||
G6.registerNode('circleNode', {
|
||||
drawShape(cfg, group) {
|
||||
const keyShape = group.addShape('circle', {
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
r: 30,
|
||||
fill: '#87e8de'
|
||||
}
|
||||
});
|
||||
|
||||
return keyShape;
|
||||
}
|
||||
}, 'circle');
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 800,
|
||||
height: 600,
|
||||
defaultNode: {
|
||||
shape: 'circleNode'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
},
|
||||
modes: {
|
||||
default: [ 'drag-canvas', 'zoom-canvas', 'collapse-expand-group' ]
|
||||
}
|
||||
});
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node6',
|
||||
groupId: 'group3',
|
||||
label: 'node6-group3',
|
||||
x: 500,
|
||||
y: 400
|
||||
},
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'node1-group1',
|
||||
groupId: 'group1',
|
||||
x: 100,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node9',
|
||||
label: 'node9-p1',
|
||||
groupId: 'p1',
|
||||
x: 300,
|
||||
y: 210
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'node2-group2',
|
||||
groupId: 'group1',
|
||||
x: 150,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
label: 'node3-group2',
|
||||
groupId: 'group2',
|
||||
x: 300,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node7',
|
||||
groupId: 'p1',
|
||||
label: 'node7-p1',
|
||||
x: 200,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
id: 'node10',
|
||||
label: 'node10-p2',
|
||||
groupId: 'p2',
|
||||
x: 400,
|
||||
y: 410
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'node1',
|
||||
target: 'node2'
|
||||
},
|
||||
{
|
||||
source: 'node2',
|
||||
target: 'node3'
|
||||
}
|
||||
],
|
||||
groups: [
|
||||
{
|
||||
id: 'group1',
|
||||
title: '1',
|
||||
parentId: 'p1'
|
||||
},
|
||||
{
|
||||
id: 'group2',
|
||||
title: '2',
|
||||
parentId: 'p1'
|
||||
},
|
||||
{
|
||||
id: 'group3',
|
||||
title: '2',
|
||||
parentId: 'p2'
|
||||
},
|
||||
{
|
||||
id: 'p1',
|
||||
title: '3'
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
title: '3'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
graph.data(data)
|
||||
graph.render()
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -109,6 +109,17 @@
|
||||
graph.data(data)
|
||||
graph.render()
|
||||
|
||||
graph.on('edge:click', evt => {
|
||||
console.log(evt)
|
||||
const { item } = evt
|
||||
graph.updateItem(item, {
|
||||
style: {
|
||||
lineWidth: 3,
|
||||
stroke: 'red'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 创建ul
|
||||
const conextMenuContainer = document.createElement('ul')
|
||||
conextMenuContainer.id = 'contextMenu';
|
||||
|
@ -105,6 +105,12 @@
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
console.log(graph.getNodes())
|
||||
|
||||
graph.removeItem(graph.findById('node0'))
|
||||
|
||||
console.log('删除后的节点', graph.getNodes())
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
144
demos/drag-group.html
Normal file
144
demos/drag-group.html
Normal file
@ -0,0 +1,144 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>拖动群组</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mountNode"></div>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script>
|
||||
/**
|
||||
* 该案例演示以下功能:
|
||||
* 1、渲染群组所需要的数据结构;
|
||||
* 2、如何拖动一个群组;
|
||||
* 3、将节点从群组中拖出;
|
||||
* 4、将节点拖入到某个群组中;
|
||||
* 5、拖出拖入节点后动态改变群组大小。
|
||||
*/
|
||||
G6.registerNode('circleNode', {
|
||||
drawShape(cfg, group) {
|
||||
const keyShape = group.addShape('circle', {
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
r: 30,
|
||||
fill: '#87e8de'
|
||||
}
|
||||
});
|
||||
|
||||
return keyShape;
|
||||
}
|
||||
}, 'circle');
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 800,
|
||||
height: 600,
|
||||
defaultNode: {
|
||||
shape: 'circleNode'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
},
|
||||
modes: {
|
||||
default: [ 'drag-group', 'drag-node-with-group', 'collapse-expand-group' ]
|
||||
}
|
||||
});
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node6',
|
||||
groupId: 'group3',
|
||||
label: 'node6-group3',
|
||||
x: 100,
|
||||
y: 300,
|
||||
shape: 'rect'
|
||||
},
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'node1-group1',
|
||||
groupId: 'group1',
|
||||
x: 100,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node9',
|
||||
label: 'node9-p1',
|
||||
groupId: 'p1',
|
||||
x: 300,
|
||||
y: 210
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'node2-group2',
|
||||
groupId: 'group1',
|
||||
x: 150,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
label: 'node3-group2',
|
||||
groupId: 'group2',
|
||||
x: 300,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node7',
|
||||
groupId: 'p1',
|
||||
label: 'node7-p1',
|
||||
x: 200,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
id: 'node10',
|
||||
label: 'node10-p2',
|
||||
groupId: 'p2',
|
||||
x: 300,
|
||||
y: 210
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'node1',
|
||||
target: 'node2'
|
||||
},
|
||||
{
|
||||
source: 'node2',
|
||||
target: 'node3'
|
||||
}
|
||||
],
|
||||
groups: [
|
||||
{
|
||||
id: 'group1',
|
||||
title: '1',
|
||||
parentId: 'p1'
|
||||
},
|
||||
{
|
||||
id: 'group2',
|
||||
title: '2',
|
||||
parentId: 'p1'
|
||||
},
|
||||
{
|
||||
id: 'group3',
|
||||
title: '2',
|
||||
parentId: 'p2'
|
||||
},
|
||||
{
|
||||
id: 'p1',
|
||||
title: '3'
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
title: '3'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
graph.data(data)
|
||||
graph.render()
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
166
demos/dynamic-add-tree.html
Normal file
166
demos/dynamic-add-tree.html
Normal file
@ -0,0 +1,166 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<style>
|
||||
</style>
|
||||
<body>
|
||||
<div id="mountNode"></div>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script>
|
||||
const graph = new G6.TreeGraph({
|
||||
container: 'mountNode',
|
||||
width: 500,
|
||||
height: 500,
|
||||
pixelRatio: 2,
|
||||
renderer: 'svg',
|
||||
modes: {
|
||||
default: ['collapse-expand', 'drag-canvas']
|
||||
},
|
||||
fitView: true,
|
||||
layout: {
|
||||
type: 'compactBox',
|
||||
direction: 'LR',
|
||||
defalutPosition: [],
|
||||
getId(d) { return d.id; },
|
||||
getHeight() { return 16 },
|
||||
getWidth() { return 16 },
|
||||
getVGap() { return 50 },
|
||||
getHGap() { return 100 }
|
||||
}
|
||||
});
|
||||
graph.node(node => {
|
||||
return {
|
||||
size: 16,
|
||||
anchorPoints: [[0,0.5], [1,0.5]],
|
||||
style: {
|
||||
fill: '#40a9ff',
|
||||
stroke: '#096dd9'
|
||||
},
|
||||
label: node.id,
|
||||
labelCfg: {
|
||||
position: node.children && node.children.length > 0 ? 'left' : 'right'
|
||||
}
|
||||
}
|
||||
});
|
||||
let i = 0;
|
||||
graph.edge(() => {
|
||||
i++;
|
||||
return {
|
||||
shape: 'cubic-horizontal',
|
||||
color: '#A3B1BF',
|
||||
label: i
|
||||
}
|
||||
});
|
||||
const data = {
|
||||
isRoot: true,
|
||||
id: 'Root',
|
||||
style: {
|
||||
fill: 'red'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'SubTreeNode1',
|
||||
raw: {},
|
||||
children: [
|
||||
{
|
||||
id: 'SubTreeNode1.1'
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode1.2',
|
||||
children: [
|
||||
{
|
||||
id: 'SubTreeNode1.2.1'
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode1.2.2'
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode1.2.3'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode2',
|
||||
children: [
|
||||
{
|
||||
id: 'SubTreeNode2.1'
|
||||
}
|
||||
]
|
||||
}, {
|
||||
id: 'SubTreeNode3',
|
||||
children: [
|
||||
{
|
||||
id: 'SubTreeNode3.1'
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode3.2'
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode3.3'
|
||||
}
|
||||
]
|
||||
}, {
|
||||
id: 'SubTreeNode4'
|
||||
}, {
|
||||
id: 'SubTreeNode5'
|
||||
}, {
|
||||
id: 'SubTreeNode6'
|
||||
}
|
||||
]
|
||||
};
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
let count = 0;
|
||||
|
||||
graph.on('node:click', evt => {
|
||||
const { item } = evt
|
||||
const nodeId = item.get('id');
|
||||
const model = item.getModel()
|
||||
const children = model.children;
|
||||
if(!children || children.length === 0) {
|
||||
const childData = [
|
||||
{
|
||||
id: `child-data-${count}`,
|
||||
shape: 'rect',
|
||||
children: [
|
||||
{
|
||||
id: `x-${count}`
|
||||
},
|
||||
{
|
||||
id: `y-${count}`
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: `child-data1-${count}`,
|
||||
children: [
|
||||
{
|
||||
id: `x1-${count}`
|
||||
},
|
||||
{
|
||||
id: `y1-${count}`
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const parentData = graph.findDataById(nodeId);
|
||||
if (!parentData.children) {
|
||||
parentData.children = [];
|
||||
}
|
||||
// 如果childData是一个数组,则直接赋值给parentData.children
|
||||
// 如果是一个对象,则使用parentData.children.push(obj)
|
||||
parentData.children = childData
|
||||
graph.changeData()
|
||||
count++
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -20,7 +20,7 @@
|
||||
}
|
||||
});
|
||||
let marker;
|
||||
if (cfg.data.collapsed) {
|
||||
if (cfg.collapsed) {
|
||||
marker = group.addShape('marker', {
|
||||
attrs: {
|
||||
symbol: 'triangle',
|
||||
@ -30,7 +30,7 @@
|
||||
fill: '#666'
|
||||
},
|
||||
});
|
||||
} else if (cfg.data.children && cfg.data.children.length > 0) {
|
||||
} else if (cfg.children && cfg.children.length > 0) {
|
||||
marker = group.addShape('marker', {
|
||||
attrs: {
|
||||
symbol: 'triangle-down',
|
||||
@ -45,7 +45,7 @@
|
||||
attrs: {
|
||||
x: cfg.x + 15,
|
||||
y: cfg.y + 4,
|
||||
text: cfg.data.name,
|
||||
text: cfg.name,
|
||||
fill: '#666',
|
||||
fontSize: 16,
|
||||
textAlign: 'left'
|
||||
@ -74,7 +74,7 @@
|
||||
type: 'collapse-expand',
|
||||
animate: false,
|
||||
onChange(item, collapsed) {
|
||||
const data = item.get('model').data;
|
||||
const data = item.get('model');
|
||||
data.collapsed = collapsed;
|
||||
return true;
|
||||
}
|
||||
@ -138,12 +138,31 @@
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
graph.node(function(node) {
|
||||
return {
|
||||
shape: 'file-node',
|
||||
label: node.name
|
||||
};
|
||||
});
|
||||
|
||||
graph.edge(function(edge) {
|
||||
return {
|
||||
shape: 'step-line'
|
||||
};
|
||||
});
|
||||
|
||||
graph.on('node:click', evt => {
|
||||
|
||||
console.log(evt)
|
||||
})
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
graph.getNodes().forEach(node => {
|
||||
const model = node.get('model');
|
||||
model.shape = 'file-node';
|
||||
model.label = model.data.name;
|
||||
model.label = model.name;
|
||||
});
|
||||
graph.getEdges().forEach(edge => {
|
||||
edge.get('model').shape = 'step-line';
|
||||
|
71
demos/front-edge.html
Normal file
71
demos/front-edge.html
Normal file
@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>边前置</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<button id='changeView'>前置边</button>
|
||||
<div id="mountNode"></div>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script>
|
||||
const data = {
|
||||
nodes: [{
|
||||
id: 'node1',
|
||||
x: 100,
|
||||
y: 100,
|
||||
shape: 'rect',
|
||||
style: {
|
||||
fill: '#fff'
|
||||
}
|
||||
}, {
|
||||
id: 'node2',
|
||||
x: 200,
|
||||
y: 200,
|
||||
shape: 'rect',
|
||||
style: {
|
||||
fill: '#fff'
|
||||
}
|
||||
}, {
|
||||
id: 'node3',
|
||||
x: 300,
|
||||
y: 300,
|
||||
shape: 'rect',
|
||||
style: {
|
||||
fill: '#fff'
|
||||
}
|
||||
}],
|
||||
edges: [{
|
||||
id: 'edge1',
|
||||
target: 'node3',
|
||||
source: 'node1',
|
||||
style: {
|
||||
endArrow: true
|
||||
},
|
||||
labelCfg: {
|
||||
style: { stroke: 'white', lineWidth: 5 } // 加白边框
|
||||
}
|
||||
}]
|
||||
};
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 500,
|
||||
height: 500
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
graph.fitView()
|
||||
document.getElementById('changeView').addEventListener('click', (evt) => {
|
||||
const edge=graph.findById('edge1')
|
||||
const nodeGroup = graph.get('nodeGroup')
|
||||
const edgeGroup = graph.get('edgeGroup')
|
||||
const edge1G = edge.get('group')
|
||||
edge1G.toFront()
|
||||
nodeGroup.toBack();
|
||||
graph.paint()
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
126
demos/hide-show-label.html
Normal file
126
demos/hide-show-label.html
Normal file
@ -0,0 +1,126 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>边上Tooltip</title>
|
||||
</head>
|
||||
<style>
|
||||
.edgeTooltip {
|
||||
background: 'red';
|
||||
border: 1px solid 'blue';
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div id="mountNode"></div>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script>
|
||||
G6.registerNode('circleNode', {
|
||||
drawShape(cfg, group) {
|
||||
const keyShape = group.addShape('circle', {
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
r: 30,
|
||||
fill: '#87e8de'
|
||||
}
|
||||
});
|
||||
|
||||
return keyShape;
|
||||
}
|
||||
}, 'circle');
|
||||
|
||||
const data = {
|
||||
nodes: [{
|
||||
id: 'node1',
|
||||
x: 200,
|
||||
y: 200,
|
||||
label: '节点1',
|
||||
anchorPoints: [
|
||||
[0, 0.5], [1, 0.5]
|
||||
]
|
||||
}, {
|
||||
id: 'node2',
|
||||
x: 500,
|
||||
y: 450,
|
||||
label: '节点2',
|
||||
anchorPoints: [
|
||||
[0, 0.5], [1, 0.5]
|
||||
]
|
||||
}
|
||||
],
|
||||
edges: [{
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
labelText: '两点之间的连线', // 设置成label会添加默认的label
|
||||
shape: 'myEdge'
|
||||
}]
|
||||
};
|
||||
|
||||
|
||||
// 注册边
|
||||
G6.registerEdge('myEdge', {
|
||||
// 设置状态
|
||||
setState(name, value, item) {
|
||||
const group = item.getContainer();
|
||||
const shape = group.get('children')[0]; // 顺序根据 draw 时确定
|
||||
if (name === 'active') {
|
||||
if (value) {
|
||||
shape.attr('stroke', '#3bae7ff');
|
||||
} else {
|
||||
shape.attr('stroke', '#333');
|
||||
}
|
||||
}
|
||||
if (name === 'selected') {
|
||||
if (value) {
|
||||
shape.attr('lineWidth', 3);
|
||||
} else {
|
||||
shape.attr('lineWidth', 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 'cubic-horizontal');
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 1000,
|
||||
height: 600,
|
||||
defaultNode: {
|
||||
shape: 'circleNode'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff',
|
||||
size: 5
|
||||
},
|
||||
modes: {
|
||||
default: ['click-select', 'drag-canvas',
|
||||
{
|
||||
type: 'edge-tooltip',
|
||||
formatText(model) {
|
||||
return `
|
||||
<div class='edgeTooltip'>边tooltip:${model.labelText}</div>`
|
||||
}
|
||||
}],
|
||||
}
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
// 点击时选中,再点击时取消
|
||||
graph.on('edge:click', ev => {
|
||||
const edge = ev.item;
|
||||
graph.setItemState(edge, 'selected', !edge.hasState('selected')); // 切换选中
|
||||
});
|
||||
|
||||
graph.on('edge:mouseenter', ev => {
|
||||
const edge = ev.item;
|
||||
graph.setItemState(edge, 'active', true);
|
||||
});
|
||||
|
||||
graph.on('edge:mouseleave', ev => {
|
||||
const edge = ev.item;
|
||||
graph.setItemState(edge, 'active', false);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
253
demos/interactive-tree.html
Normal file
253
demos/interactive-tree.html
Normal file
@ -0,0 +1,253 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mountNode"></div>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script>
|
||||
G6.registerNode('expandNode', {
|
||||
draw(cfg, group) {
|
||||
const mainGroup = group.addGroup({
|
||||
id: 'main-group'
|
||||
})
|
||||
const keyShape = mainGroup.addShape('rect', {
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100 + 60 * cfg.values.length,
|
||||
height: 50,
|
||||
fill: '#f5f5f5'
|
||||
}
|
||||
})
|
||||
|
||||
// name text
|
||||
mainGroup.addShape('text', {
|
||||
attrs: {
|
||||
text: cfg.name,
|
||||
fill: '#000',
|
||||
width: 130,
|
||||
x: 10,
|
||||
y: 32
|
||||
}
|
||||
})
|
||||
|
||||
const subGroup = group.addGroup({
|
||||
id: 'sub-group'
|
||||
})
|
||||
cfg.values.forEach((data, index) => {
|
||||
subGroup.addShape('rect', {
|
||||
attrs: {
|
||||
x: 110 + index * 60,
|
||||
y: 0,
|
||||
width: 50,
|
||||
height: 50
|
||||
}
|
||||
})
|
||||
|
||||
subGroup.addShape('text', {
|
||||
attrs: {
|
||||
text: data.key,
|
||||
fill: '#000',
|
||||
x: 130 + index * 60,
|
||||
y: 20,
|
||||
fontSize: 10,
|
||||
textBaseline: 'middle',
|
||||
className: 'sub-group-text'
|
||||
}
|
||||
})
|
||||
|
||||
subGroup.addShape('text', {
|
||||
attrs: {
|
||||
text: data.value,
|
||||
fill: '#000',
|
||||
x: 130 + index * 60,
|
||||
y: 30,
|
||||
fontSize: 10,
|
||||
textBaseline: 'middle',
|
||||
textAlign: 'left',
|
||||
className: 'sub-group-text'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const listGroup = group.addGroup({
|
||||
id: 'detail-list-group'
|
||||
})
|
||||
|
||||
listGroup.addShape('rect', {
|
||||
attrs: {
|
||||
width: 100 + 60 * cfg.values.length - 70,
|
||||
height: 30 * cfg.properties.length + 20,
|
||||
fill: '#fff',
|
||||
x: 70,
|
||||
y: 30
|
||||
}
|
||||
})
|
||||
|
||||
const rectWidth = 100 + 60 * cfg.values.length - 80
|
||||
cfg.properties.forEach((property, index) => {
|
||||
listGroup.addShape('rect', {
|
||||
attrs: {
|
||||
width: rectWidth,
|
||||
height: 30,
|
||||
fill: '#e8e8e8',
|
||||
x: 80,
|
||||
y: 40 * index + 40
|
||||
}
|
||||
})
|
||||
let count = 0
|
||||
for(let p in property) {
|
||||
// 每个rect中添加5个文本
|
||||
listGroup.addShape('text', {
|
||||
attrs: {
|
||||
text: property[p],
|
||||
fill: '#000',
|
||||
x: 85 + count * (rectWidth / cfg.values.length) - count * 10,
|
||||
y: 40 * index + 40 + 15,
|
||||
fontSize: 10,
|
||||
textBaseline: 'middle',
|
||||
textAlign: 'left'
|
||||
}
|
||||
})
|
||||
count++
|
||||
}
|
||||
})
|
||||
listGroup.hide()
|
||||
return keyShape
|
||||
}
|
||||
})
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 500,
|
||||
height: 500,
|
||||
pixelRatio: 2,
|
||||
renderer: 'svg',
|
||||
modes: {
|
||||
default: ['drag-canvas']
|
||||
},
|
||||
fitView: true
|
||||
});
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'shape1',
|
||||
shape: 'expandNode',
|
||||
name: '主动进入',
|
||||
values: [
|
||||
{
|
||||
key: '曝光率',
|
||||
value: '19.09'
|
||||
},
|
||||
{
|
||||
key: '流入UV',
|
||||
value: '910'
|
||||
},
|
||||
{
|
||||
key: '点击率',
|
||||
value: '90'
|
||||
},
|
||||
{
|
||||
key: '占比',
|
||||
value: '90'
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
name: '搜索',
|
||||
value1: '102',
|
||||
value2: '102',
|
||||
value3: '102',
|
||||
value4: '102',
|
||||
},
|
||||
{
|
||||
name: '扫一扫',
|
||||
value1: '102',
|
||||
value2: '102',
|
||||
value3: '102',
|
||||
value4: '102',
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'shape1',
|
||||
x: 0,
|
||||
y: 150,
|
||||
shape: 'expandNode',
|
||||
name: '主动进入',
|
||||
values: [
|
||||
{
|
||||
key: '曝光率',
|
||||
value: '19.09'
|
||||
},
|
||||
{
|
||||
key: '流入UV',
|
||||
value: '910'
|
||||
},
|
||||
{
|
||||
key: '点击率',
|
||||
value: '90'
|
||||
},
|
||||
{
|
||||
key: '占比',
|
||||
value: '90'
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
name: '搜索',
|
||||
value1: '102',
|
||||
value2: '102',
|
||||
value3: '102',
|
||||
value4: '102',
|
||||
},
|
||||
{
|
||||
name: '扫一扫',
|
||||
value1: '102',
|
||||
value2: '102',
|
||||
value3: '102',
|
||||
value4: '102',
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
// 点击node,展开详情
|
||||
graph.on('node:click', evt => {
|
||||
const { target } = evt
|
||||
const parentGroup = target.get('parent').get('parent')
|
||||
const detailGroup = parentGroup.findById('detail-list-group')
|
||||
// 将sub-group中的内容网上移动一段距离
|
||||
const subGroup = parentGroup.findById('sub-group')
|
||||
const keyTexts = subGroup.findAll(item => {
|
||||
return item.attr('className') === 'sub-group-text'
|
||||
})
|
||||
const isVisible = detailGroup.get('visible')
|
||||
if(isVisible) {
|
||||
detailGroup.hide()
|
||||
keyTexts.forEach(text => {
|
||||
const top = text.attr('y')
|
||||
text.attr('y', top + 10)
|
||||
})
|
||||
} else {
|
||||
keyTexts.forEach(text => {
|
||||
const top = text.attr('y')
|
||||
text.attr('y', top - 10)
|
||||
})
|
||||
detailGroup.show()
|
||||
}
|
||||
console.log(keyTexts)
|
||||
graph.paint()
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
135
demos/label-ellipse.html
Normal file
135
demos/label-ellipse.html
Normal file
@ -0,0 +1,135 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>label上面文本太长显示省略号</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="mountNode"></div>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script>
|
||||
/**
|
||||
* 该案例演示当label太长时候,指定多少个字符后显示省略号。
|
||||
* 有两种方式进行处理:
|
||||
* 1、从数据中处理,处理完以后再进行渲染;
|
||||
* 2、自定义节点或边时,进行处理:
|
||||
* group.addShape('text', {
|
||||
* attrs: {
|
||||
* text: fittingString(cfg.label, 50, 12),
|
||||
* ...
|
||||
* }
|
||||
* })
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 计算字符串的长度
|
||||
* @param {string} str 指定的字符串
|
||||
*/
|
||||
const calcStrLen = (str) => {
|
||||
let len = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (str.charCodeAt(i) > 0 && str.charCodeAt(i) < 128) {
|
||||
len++;
|
||||
} else {
|
||||
len += 2;
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算显示的字符串
|
||||
* @param {string} str 要裁剪的字符串
|
||||
* @param {number} maxWidth 最大宽度
|
||||
* @param {number} fontSize 字体大小
|
||||
*/
|
||||
const fittingString = (str, maxWidth, fontSize) => {
|
||||
const fontWidth = fontSize * 1.3 //字号+边距
|
||||
maxWidth = maxWidth * 2 // 需要根据自己项目调整
|
||||
const width = calcStrLen(str) * fontWidth
|
||||
const ellipsis = '…'
|
||||
if (width > maxWidth) {
|
||||
const actualLen = Math.floor((maxWidth - 10) / fontWidth);
|
||||
const result = str.substring(0, actualLen) + ellipsis
|
||||
return result
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
const data = {
|
||||
nodes: [{
|
||||
x: 100,
|
||||
y: 100,
|
||||
shape: 'rect',
|
||||
label: '这个文案有点长',
|
||||
id:'node1',
|
||||
labelCfg: {
|
||||
position: 'bottom'
|
||||
},
|
||||
anchorPoints: [
|
||||
[0, 0.5],
|
||||
[1, 0.5]
|
||||
]
|
||||
},{
|
||||
x: 300,
|
||||
y: 100,
|
||||
shape: 'rect',
|
||||
label: '这个文案也有点长',
|
||||
id:'node2',
|
||||
labelCfg: {
|
||||
position: 'bottom'
|
||||
},
|
||||
anchorPoints: [
|
||||
[0, 0.5],
|
||||
[1, 0.5]
|
||||
]
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
label: 'label上面这个文本太长了我需要换行',
|
||||
labelCfg: {
|
||||
refY: 20
|
||||
},
|
||||
style: {
|
||||
endArrow: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 1000,
|
||||
height: 600,
|
||||
defaultNode: {
|
||||
style: {
|
||||
fill: '#87e8de',
|
||||
},
|
||||
color: '#87e8de'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
},
|
||||
modes: {
|
||||
default: ['drag-node', {
|
||||
type: 'drag-node',
|
||||
}],
|
||||
}
|
||||
});
|
||||
|
||||
// 直接修改原生数据中的label字段
|
||||
data.nodes.forEach(node => node.label = fittingString(node.label, 25, 12))
|
||||
data.edges.forEach(edge => edge.label = fittingString(edge.label, 100, 12))
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
88
demos/label-wrap.html
Normal file
88
demos/label-wrap.html
Normal file
@ -0,0 +1,88 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>label上面文本太长,需要换行:加入\n;</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="mountNode"></div>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script>
|
||||
/**
|
||||
* 该案例演示当label太长时候,如何换行显示。
|
||||
* by 镜曦。
|
||||
*
|
||||
*/
|
||||
const data = {
|
||||
nodes: [{
|
||||
x: 100,
|
||||
y: 100,
|
||||
shape: 'rect',
|
||||
label: '这个文案\n有点长',
|
||||
id:'node1',
|
||||
labelCfg: {
|
||||
position: 'bottom'
|
||||
},
|
||||
anchorPoints: [
|
||||
[0, 0.5, {type: 'circle', style: {stroke: 'red', fill: 'red'}}],
|
||||
[1, 0.5, {type: 'circle', style: {stroke: 'blue', fill: 'red'}}]
|
||||
]
|
||||
},{
|
||||
x: 300,
|
||||
y: 100,
|
||||
shape: 'rect',
|
||||
label: '这个文案\n也有点长',
|
||||
id:'node2',
|
||||
labelCfg: {
|
||||
position: 'bottom'
|
||||
},
|
||||
anchorPoints: [
|
||||
[0, 0.5, {type: 'circle', style: {stroke: 'red', fill: 'red'}}],
|
||||
[1, 0.5, {type: 'circle', style: {stroke: 'blue', fill: 'red'}}]
|
||||
]
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
label: 'label上面这个文本太长了\n我需要换行',
|
||||
labelCfg: {
|
||||
refY: 20
|
||||
},
|
||||
style: {
|
||||
endArrow: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 1000,
|
||||
height: 600,
|
||||
defaultNode: {
|
||||
style: {
|
||||
fill: '#87e8de',
|
||||
},
|
||||
color: '#87e8de'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
},
|
||||
modes: {
|
||||
default: ['drag-node', {
|
||||
type: 'drag-node',
|
||||
}],
|
||||
}
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
453
demos/light-dark.html
Normal file
453
demos/light-dark.html
Normal file
@ -0,0 +1,453 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>移动到某一个节点后,高亮所有相邻的节点和边</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mountNode"></div>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script src="https://d3js.org/d3.v5.min.js"></script>
|
||||
<script>
|
||||
const data = {
|
||||
nodes: [
|
||||
{ id: "Myriel" },
|
||||
{ id: "Napoleon" },
|
||||
{ id: "Mlle.Baptistine" },
|
||||
{ id: "Mme.Magloire" },
|
||||
{ id: "CountessdeLo" },
|
||||
{ id: "Geborand" },
|
||||
{ id: "Champtercier" },
|
||||
{ id: "Cravatte" },
|
||||
{ id: "Count" },
|
||||
{ id: "OldMan" },
|
||||
{ id: "Labarre" },
|
||||
{ id: "Valjean" },
|
||||
{ id: "Marguerite" },
|
||||
{ id: "Mme.deR" },
|
||||
{ id: "Isabeau" },
|
||||
{ id: "Gervais" },
|
||||
{ id: "Tholomyes" },
|
||||
{ id: "Listolier" },
|
||||
{ id: "Fameuil" },
|
||||
{ id: "Blacheville" },
|
||||
{ id: "Favourite" },
|
||||
{ id: "Dahlia" },
|
||||
{ id: "Zephine" },
|
||||
{ id: "Fantine" },
|
||||
{ id: "Mme.Thenardier" },
|
||||
{ id: "Thenardier" },
|
||||
{ id: "Cosette" },
|
||||
{ id: "Javert" },
|
||||
{ id: "Fauchelevent" },
|
||||
{ id: "Bamatabois" },
|
||||
{ id: "Perpetue" },
|
||||
{ id: "Simplice" },
|
||||
{ id: "Scaufflaire" },
|
||||
{ id: "Woman1" },
|
||||
{ id: "Judge" },
|
||||
{ id: "Champmathieu" },
|
||||
{ id: "Brevet" },
|
||||
{ id: "Chenildieu" },
|
||||
{ id: "Cochepaille" },
|
||||
{ id: "Pontmercy" },
|
||||
{ id: "Boulatruelle" },
|
||||
{ id: "Eponine" },
|
||||
{ id: "Anzelma" },
|
||||
{ id: "Woman2" },
|
||||
{ id: "MotherInnocent" },
|
||||
{ id: "Gribier" },
|
||||
{ id: "Jondrette" },
|
||||
{ id: "Mme.Burgon" },
|
||||
{ id: "Gavroche" },
|
||||
{ id: "Gillenormand" },
|
||||
{ id: "Magnon" },
|
||||
{ id: "Mlle.Gillenormand" },
|
||||
{ id: "Mme.Pontmercy" },
|
||||
{ id: "Mlle.Vaubois" },
|
||||
{ id: "Lt.Gillenormand" },
|
||||
{ id: "Marius" },
|
||||
{ id: "BaronessT" },
|
||||
{ id: "Mabeuf" },
|
||||
{ id: "Enjolras" },
|
||||
{ id: "Combeferre" },
|
||||
{ id: "Prouvaire" },
|
||||
{ id: "Feuilly" },
|
||||
{ id: "Courfeyrac" },
|
||||
{ id: "Bahorel" },
|
||||
{ id: "Bossuet" },
|
||||
{ id: "Joly" },
|
||||
{ id: "Grantaire" },
|
||||
{ id: "MotherPlutarch" },
|
||||
{ id: "Gueulemer" },
|
||||
{ id: "Babet" },
|
||||
{ id: "Claquesous" },
|
||||
{ id: "Montparnasse" },
|
||||
{ id: "Toussaint" },
|
||||
{ id: "Child1" },
|
||||
{ id: "Child2" },
|
||||
{ id: "Brujon" },
|
||||
{ id: "Mme.Hucheloup" }
|
||||
],
|
||||
edges: [
|
||||
{ source: "Napoleon", target: "Myriel", value: 1 },
|
||||
{ source: "Mlle.Baptistine", target: "Myriel", value: 8 },
|
||||
{ source: "Mme.Magloire", target: "Myriel", value: 10 },
|
||||
{ source: "Mme.Magloire", target: "Mlle.Baptistine", value: 6 },
|
||||
{ source: "CountessdeLo", target: "Myriel", value: 1 },
|
||||
{ source: "Geborand", target: "Myriel", value: 1 },
|
||||
{ source: "Champtercier", target: "Myriel", value: 1 },
|
||||
{ source: "Cravatte", target: "Myriel", value: 1 },
|
||||
{ source: "Count", target: "Myriel", value: 2 },
|
||||
{ source: "OldMan", target: "Myriel", value: 1 },
|
||||
{ source: "Valjean", target: "Labarre", value: 1 },
|
||||
{ source: "Valjean", target: "Mme.Magloire", value: 3 },
|
||||
{ source: "Valjean", target: "Mlle.Baptistine", value: 3 },
|
||||
{ source: "Valjean", target: "Myriel", value: 5 },
|
||||
{ source: "Marguerite", target: "Valjean", value: 1 },
|
||||
{ source: "Mme.deR", target: "Valjean", value: 1 },
|
||||
{ source: "Isabeau", target: "Valjean", value: 1 },
|
||||
{ source: "Gervais", target: "Valjean", value: 1 },
|
||||
{ source: "Listolier", target: "Tholomyes", value: 4 },
|
||||
{ source: "Fameuil", target: "Tholomyes", value: 4 },
|
||||
{ source: "Fameuil", target: "Listolier", value: 4 },
|
||||
{ source: "Blacheville", target: "Tholomyes", value: 4 },
|
||||
{ source: "Blacheville", target: "Listolier", value: 4 },
|
||||
{ source: "Blacheville", target: "Fameuil", value: 4 },
|
||||
{ source: "Favourite", target: "Tholomyes", value: 3 },
|
||||
{ source: "Favourite", target: "Listolier", value: 3 },
|
||||
{ source: "Favourite", target: "Fameuil", value: 3 },
|
||||
{ source: "Favourite", target: "Blacheville", value: 4 },
|
||||
{ source: "Dahlia", target: "Tholomyes", value: 3 },
|
||||
{ source: "Dahlia", target: "Listolier", value: 3 },
|
||||
{ source: "Dahlia", target: "Fameuil", value: 3 },
|
||||
{ source: "Dahlia", target: "Blacheville", value: 3 },
|
||||
{ source: "Dahlia", target: "Favourite", value: 5 },
|
||||
{ source: "Zephine", target: "Tholomyes", value: 3 },
|
||||
{ source: "Zephine", target: "Listolier", value: 3 },
|
||||
{ source: "Zephine", target: "Fameuil", value: 3 },
|
||||
{ source: "Zephine", target: "Blacheville", value: 3 },
|
||||
{ source: "Zephine", target: "Favourite", value: 4 },
|
||||
{ source: "Zephine", target: "Dahlia", value: 4 },
|
||||
{ source: "Fantine", target: "Tholomyes", value: 3 },
|
||||
{ source: "Fantine", target: "Listolier", value: 3 },
|
||||
{ source: "Fantine", target: "Fameuil", value: 3 },
|
||||
{ source: "Fantine", target: "Blacheville", value: 3 },
|
||||
{ source: "Fantine", target: "Favourite", value: 4 },
|
||||
{ source: "Fantine", target: "Dahlia", value: 4 },
|
||||
{ source: "Fantine", target: "Zephine", value: 4 },
|
||||
{ source: "Fantine", target: "Marguerite", value: 2 },
|
||||
{ source: "Fantine", target: "Valjean", value: 9 },
|
||||
{ source: "Mme.Thenardier", target: "Fantine", value: 2 },
|
||||
{ source: "Mme.Thenardier", target: "Valjean", value: 7 },
|
||||
{ source: "Thenardier", target: "Mme.Thenardier", value: 13 },
|
||||
{ source: "Thenardier", target: "Fantine", value: 1 },
|
||||
{ source: "Thenardier", target: "Valjean", value: 12 },
|
||||
{ source: "Cosette", target: "Mme.Thenardier", value: 4 },
|
||||
{ source: "Cosette", target: "Valjean", value: 31 },
|
||||
{ source: "Cosette", target: "Tholomyes", value: 1 },
|
||||
{ source: "Cosette", target: "Thenardier", value: 1 },
|
||||
{ source: "Javert", target: "Valjean", value: 17 },
|
||||
{ source: "Javert", target: "Fantine", value: 5 },
|
||||
{ source: "Javert", target: "Thenardier", value: 5 },
|
||||
{ source: "Javert", target: "Mme.Thenardier", value: 1 },
|
||||
{ source: "Javert", target: "Cosette", value: 1 },
|
||||
{ source: "Fauchelevent", target: "Valjean", value: 8 },
|
||||
{ source: "Fauchelevent", target: "Javert", value: 1 },
|
||||
{ source: "Bamatabois", target: "Fantine", value: 1 },
|
||||
{ source: "Bamatabois", target: "Javert", value: 1 },
|
||||
{ source: "Bamatabois", target: "Valjean", value: 2 },
|
||||
{ source: "Perpetue", target: "Fantine", value: 1 },
|
||||
{ source: "Simplice", target: "Perpetue", value: 2 },
|
||||
{ source: "Simplice", target: "Valjean", value: 3 },
|
||||
{ source: "Simplice", target: "Fantine", value: 2 },
|
||||
{ source: "Simplice", target: "Javert", value: 1 },
|
||||
{ source: "Scaufflaire", target: "Valjean", value: 1 },
|
||||
{ source: "Woman1", target: "Valjean", value: 2 },
|
||||
{ source: "Woman1", target: "Javert", value: 1 },
|
||||
{ source: "Judge", target: "Valjean", value: 3 },
|
||||
{ source: "Judge", target: "Bamatabois", value: 2 },
|
||||
{ source: "Champmathieu", target: "Valjean", value: 3 },
|
||||
{ source: "Champmathieu", target: "Judge", value: 3 },
|
||||
{ source: "Champmathieu", target: "Bamatabois", value: 2 },
|
||||
{ source: "Brevet", target: "Judge", value: 2 },
|
||||
{ source: "Brevet", target: "Champmathieu", value: 2 },
|
||||
{ source: "Brevet", target: "Valjean", value: 2 },
|
||||
{ source: "Brevet", target: "Bamatabois", value: 1 },
|
||||
{ source: "Chenildieu", target: "Judge", value: 2 },
|
||||
{ source: "Chenildieu", target: "Champmathieu", value: 2 },
|
||||
{ source: "Chenildieu", target: "Brevet", value: 2 },
|
||||
{ source: "Chenildieu", target: "Valjean", value: 2 },
|
||||
{ source: "Chenildieu", target: "Bamatabois", value: 1 },
|
||||
{ source: "Cochepaille", target: "Judge", value: 2 },
|
||||
{ source: "Cochepaille", target: "Champmathieu", value: 2 },
|
||||
{ source: "Cochepaille", target: "Brevet", value: 2 },
|
||||
{ source: "Cochepaille", target: "Chenildieu", value: 2 },
|
||||
{ source: "Cochepaille", target: "Valjean", value: 2 },
|
||||
{ source: "Cochepaille", target: "Bamatabois", value: 1 },
|
||||
{ source: "Pontmercy", target: "Thenardier", value: 1 },
|
||||
{ source: "Boulatruelle", target: "Thenardier", value: 1 },
|
||||
{ source: "Eponine", target: "Mme.Thenardier", value: 2 },
|
||||
{ source: "Eponine", target: "Thenardier", value: 3 },
|
||||
{ source: "Anzelma", target: "Eponine", value: 2 },
|
||||
{ source: "Anzelma", target: "Thenardier", value: 2 },
|
||||
{ source: "Anzelma", target: "Mme.Thenardier", value: 1 },
|
||||
{ source: "Woman2", target: "Valjean", value: 3 },
|
||||
{ source: "Woman2", target: "Cosette", value: 1 },
|
||||
{ source: "Woman2", target: "Javert", value: 1 },
|
||||
{ source: "MotherInnocent", target: "Fauchelevent", value: 3 },
|
||||
{ source: "MotherInnocent", target: "Valjean", value: 1 },
|
||||
{ source: "Gribier", target: "Fauchelevent", value: 2 },
|
||||
{ source: "Mme.Burgon", target: "Jondrette", value: 1 },
|
||||
{ source: "Gavroche", target: "Mme.Burgon", value: 2 },
|
||||
{ source: "Gavroche", target: "Thenardier", value: 1 },
|
||||
{ source: "Gavroche", target: "Javert", value: 1 },
|
||||
{ source: "Gavroche", target: "Valjean", value: 1 },
|
||||
{ source: "Gillenormand", target: "Cosette", value: 3 },
|
||||
{ source: "Gillenormand", target: "Valjean", value: 2 },
|
||||
{ source: "Magnon", target: "Gillenormand", value: 1 },
|
||||
{ source: "Magnon", target: "Mme.Thenardier", value: 1 },
|
||||
{ source: "Mlle.Gillenormand", target: "Gillenormand", value: 9 },
|
||||
{ source: "Mlle.Gillenormand", target: "Cosette", value: 2 },
|
||||
{ source: "Mlle.Gillenormand", target: "Valjean", value: 2 },
|
||||
{ source: "Mme.Pontmercy", target: "Mlle.Gillenormand", value: 1 },
|
||||
{ source: "Mme.Pontmercy", target: "Pontmercy", value: 1 },
|
||||
{ source: "Mlle.Vaubois", target: "Mlle.Gillenormand", value: 1 },
|
||||
{ source: "Lt.Gillenormand", target: "Mlle.Gillenormand", value: 2 },
|
||||
{ source: "Lt.Gillenormand", target: "Gillenormand", value: 1 },
|
||||
{ source: "Lt.Gillenormand", target: "Cosette", value: 1 },
|
||||
{ source: "Marius", target: "Mlle.Gillenormand", value: 6 },
|
||||
{ source: "Marius", target: "Gillenormand", value: 12 },
|
||||
{ source: "Marius", target: "Pontmercy", value: 1 },
|
||||
{ source: "Marius", target: "Lt.Gillenormand", value: 1 },
|
||||
{ source: "Marius", target: "Cosette", value: 21 },
|
||||
{ source: "Marius", target: "Valjean", value: 19 },
|
||||
{ source: "Marius", target: "Tholomyes", value: 1 },
|
||||
{ source: "Marius", target: "Thenardier", value: 2 },
|
||||
{ source: "Marius", target: "Eponine", value: 5 },
|
||||
{ source: "Marius", target: "Gavroche", value: 4 },
|
||||
{ source: "BaronessT", target: "Gillenormand", value: 1 },
|
||||
{ source: "BaronessT", target: "Marius", value: 1 },
|
||||
{ source: "Mabeuf", target: "Marius", value: 1 },
|
||||
{ source: "Mabeuf", target: "Eponine", value: 1 },
|
||||
{ source: "Mabeuf", target: "Gavroche", value: 1 },
|
||||
{ source: "Enjolras", target: "Marius", value: 7 },
|
||||
{ source: "Enjolras", target: "Gavroche", value: 7 },
|
||||
{ source: "Enjolras", target: "Javert", value: 6 },
|
||||
{ source: "Enjolras", target: "Mabeuf", value: 1 },
|
||||
{ source: "Enjolras", target: "Valjean", value: 4 },
|
||||
{ source: "Combeferre", target: "Enjolras", value: 15 },
|
||||
{ source: "Combeferre", target: "Marius", value: 5 },
|
||||
{ source: "Combeferre", target: "Gavroche", value: 6 },
|
||||
{ source: "Combeferre", target: "Mabeuf", value: 2 },
|
||||
{ source: "Prouvaire", target: "Gavroche", value: 1 },
|
||||
{ source: "Prouvaire", target: "Enjolras", value: 4 },
|
||||
{ source: "Prouvaire", target: "Combeferre", value: 2 },
|
||||
{ source: "Feuilly", target: "Gavroche", value: 2 },
|
||||
{ source: "Feuilly", target: "Enjolras", value: 6 },
|
||||
{ source: "Feuilly", target: "Prouvaire", value: 2 },
|
||||
{ source: "Feuilly", target: "Combeferre", value: 5 },
|
||||
{ source: "Feuilly", target: "Mabeuf", value: 1 },
|
||||
{ source: "Feuilly", target: "Marius", value: 1 },
|
||||
{ source: "Courfeyrac", target: "Marius", value: 9 },
|
||||
{ source: "Courfeyrac", target: "Enjolras", value: 17 },
|
||||
{ source: "Courfeyrac", target: "Combeferre", value: 13 },
|
||||
{ source: "Courfeyrac", target: "Gavroche", value: 7 },
|
||||
{ source: "Courfeyrac", target: "Mabeuf", value: 2 },
|
||||
{ source: "Courfeyrac", target: "Eponine", value: 1 },
|
||||
{ source: "Courfeyrac", target: "Feuilly", value: 6 },
|
||||
{ source: "Courfeyrac", target: "Prouvaire", value: 3 },
|
||||
{ source: "Bahorel", target: "Combeferre", value: 5 },
|
||||
{ source: "Bahorel", target: "Gavroche", value: 5 },
|
||||
{ source: "Bahorel", target: "Courfeyrac", value: 6 },
|
||||
{ source: "Bahorel", target: "Mabeuf", value: 2 },
|
||||
{ source: "Bahorel", target: "Enjolras", value: 4 },
|
||||
{ source: "Bahorel", target: "Feuilly", value: 3 },
|
||||
{ source: "Bahorel", target: "Prouvaire", value: 2 },
|
||||
{ source: "Bahorel", target: "Marius", value: 1 },
|
||||
{ source: "Bossuet", target: "Marius", value: 5 },
|
||||
{ source: "Bossuet", target: "Courfeyrac", value: 12 },
|
||||
{ source: "Bossuet", target: "Gavroche", value: 5 },
|
||||
{ source: "Bossuet", target: "Bahorel", value: 4 },
|
||||
{ source: "Bossuet", target: "Enjolras", value: 10 },
|
||||
{ source: "Bossuet", target: "Feuilly", value: 6 },
|
||||
{ source: "Bossuet", target: "Prouvaire", value: 2 },
|
||||
{ source: "Bossuet", target: "Combeferre", value: 9 },
|
||||
{ source: "Bossuet", target: "Mabeuf", value: 1 },
|
||||
{ source: "Bossuet", target: "Valjean", value: 1 },
|
||||
{ source: "Joly", target: "Bahorel", value: 5 },
|
||||
{ source: "Joly", target: "Bossuet", value: 7 },
|
||||
{ source: "Joly", target: "Gavroche", value: 3 },
|
||||
{ source: "Joly", target: "Courfeyrac", value: 5 },
|
||||
{ source: "Joly", target: "Enjolras", value: 5 },
|
||||
{ source: "Joly", target: "Feuilly", value: 5 },
|
||||
{ source: "Joly", target: "Prouvaire", value: 2 },
|
||||
{ source: "Joly", target: "Combeferre", value: 5 },
|
||||
{ source: "Joly", target: "Mabeuf", value: 1 },
|
||||
{ source: "Joly", target: "Marius", value: 2 },
|
||||
{ source: "Grantaire", target: "Bossuet", value: 3 },
|
||||
{ source: "Grantaire", target: "Enjolras", value: 3 },
|
||||
{ source: "Grantaire", target: "Combeferre", value: 1 },
|
||||
{ source: "Grantaire", target: "Courfeyrac", value: 2 },
|
||||
{ source: "Grantaire", target: "Joly", value: 2 },
|
||||
{ source: "Grantaire", target: "Gavroche", value: 1 },
|
||||
{ source: "Grantaire", target: "Bahorel", value: 1 },
|
||||
{ source: "Grantaire", target: "Feuilly", value: 1 },
|
||||
{ source: "Grantaire", target: "Prouvaire", value: 1 },
|
||||
{ source: "MotherPlutarch", target: "Mabeuf", value: 3 },
|
||||
{ source: "Gueulemer", target: "Thenardier", value: 5 },
|
||||
{ source: "Gueulemer", target: "Valjean", value: 1 },
|
||||
{ source: "Gueulemer", target: "Mme.Thenardier", value: 1 },
|
||||
{ source: "Gueulemer", target: "Javert", value: 1 },
|
||||
{ source: "Gueulemer", target: "Gavroche", value: 1 },
|
||||
{ source: "Gueulemer", target: "Eponine", value: 1 },
|
||||
{ source: "Babet", target: "Thenardier", value: 6 },
|
||||
{ source: "Babet", target: "Gueulemer", value: 6 },
|
||||
{ source: "Babet", target: "Valjean", value: 1 },
|
||||
{ source: "Babet", target: "Mme.Thenardier", value: 1 },
|
||||
{ source: "Babet", target: "Javert", value: 2 },
|
||||
{ source: "Babet", target: "Gavroche", value: 1 },
|
||||
{ source: "Babet", target: "Eponine", value: 1 },
|
||||
{ source: "Claquesous", target: "Thenardier", value: 4 },
|
||||
{ source: "Claquesous", target: "Babet", value: 4 },
|
||||
{ source: "Claquesous", target: "Gueulemer", value: 4 },
|
||||
{ source: "Claquesous", target: "Valjean", value: 1 },
|
||||
{ source: "Claquesous", target: "Mme.Thenardier", value: 1 },
|
||||
{ source: "Claquesous", target: "Javert", value: 1 },
|
||||
{ source: "Claquesous", target: "Eponine", value: 1 },
|
||||
{ source: "Claquesous", target: "Enjolras", value: 1 },
|
||||
{ source: "Montparnasse", target: "Javert", value: 1 },
|
||||
{ source: "Montparnasse", target: "Babet", value: 2 },
|
||||
{ source: "Montparnasse", target: "Gueulemer", value: 2 },
|
||||
{ source: "Montparnasse", target: "Claquesous", value: 2 },
|
||||
{ source: "Montparnasse", target: "Valjean", value: 1 },
|
||||
{ source: "Montparnasse", target: "Gavroche", value: 1 },
|
||||
{ source: "Montparnasse", target: "Eponine", value: 1 },
|
||||
{ source: "Montparnasse", target: "Thenardier", value: 1 },
|
||||
{ source: "Toussaint", target: "Cosette", value: 2 },
|
||||
{ source: "Toussaint", target: "Javert", value: 1 },
|
||||
{ source: "Toussaint", target: "Valjean", value: 1 },
|
||||
{ source: "Child1", target: "Gavroche", value: 2 },
|
||||
{ source: "Child2", target: "Gavroche", value: 2 },
|
||||
{ source: "Child2", target: "Child1", value: 3 },
|
||||
{ source: "Brujon", target: "Babet", value: 3 },
|
||||
{ source: "Brujon", target: "Gueulemer", value: 3 },
|
||||
{ source: "Brujon", target: "Thenardier", value: 3 },
|
||||
{ source: "Brujon", target: "Gavroche", value: 1 },
|
||||
{ source: "Brujon", target: "Eponine", value: 1 },
|
||||
{ source: "Brujon", target: "Claquesous", value: 1 },
|
||||
{ source: "Brujon", target: "Montparnasse", value: 1 },
|
||||
{ source: "Mme.Hucheloup", target: "Bossuet", value: 1 },
|
||||
{ source: "Mme.Hucheloup", target: "Joly", value: 1 },
|
||||
{ source: "Mme.Hucheloup", target: "Grantaire", value: 1 },
|
||||
{ source: "Mme.Hucheloup", target: "Bahorel", value: 1 },
|
||||
{ source: "Mme.Hucheloup", target: "Courfeyrac", value: 1 },
|
||||
{ source: "Mme.Hucheloup", target: "Gavroche", value: 1 },
|
||||
{ source: "Mme.Hucheloup", target: "Enjolras", value: 1 }
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
autoPaint: false,
|
||||
defaultNode: {
|
||||
size: [10, 10],
|
||||
color: 'steelblue'
|
||||
},
|
||||
defaultEdge: {
|
||||
size: 1
|
||||
},
|
||||
nodeStyle: {
|
||||
default: {
|
||||
lineWidth: 2,
|
||||
fill: 'steelblue'
|
||||
},
|
||||
highlight: {
|
||||
opacity: 1
|
||||
},
|
||||
dark: {
|
||||
opacity: 0.2
|
||||
}
|
||||
},
|
||||
edgeStyle: {
|
||||
default: {
|
||||
stroke: '#e2e2e2',
|
||||
lineAppendWidth: 2
|
||||
},
|
||||
highlight: {
|
||||
stroke: '#999'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function clearAllStats() {
|
||||
graph.setAutoPaint(false);
|
||||
graph.getNodes().forEach(function(node) {
|
||||
graph.clearItemStates(node);
|
||||
});
|
||||
|
||||
graph.getEdges().forEach(function(edge) {
|
||||
graph.clearItemStates(edge);
|
||||
});
|
||||
graph.paint();
|
||||
graph.setAutoPaint(true);
|
||||
}
|
||||
graph.on('node:mouseenter', function(e) {
|
||||
var item = e.item;
|
||||
|
||||
graph.setAutoPaint(false);
|
||||
|
||||
graph.getNodes().forEach(function(node) {
|
||||
graph.clearItemStates(node);
|
||||
graph.setItemState(node, 'dark', true);
|
||||
});
|
||||
|
||||
graph.setItemState(item, 'dark', false);
|
||||
graph.setItemState(item, 'highlight', true);
|
||||
graph.getEdges().forEach(function(edge) {
|
||||
if (edge.getSource() === item) {
|
||||
graph.setItemState(edge.getTarget(), 'dark', false);
|
||||
graph.setItemState(edge.getTarget(), 'highlight', true);
|
||||
graph.setItemState(edge, 'highlight', true);
|
||||
edge.toFront();
|
||||
} else if (edge.getTarget() === item) {
|
||||
graph.setItemState(edge.getSource(), 'dark', false);
|
||||
graph.setItemState(edge.getSource(), 'highlight', true);
|
||||
graph.setItemState(edge, 'highlight', true);
|
||||
edge.toFront();
|
||||
} else {
|
||||
graph.setItemState(edge, 'highlight', false);
|
||||
}
|
||||
});
|
||||
|
||||
graph.paint();
|
||||
graph.setAutoPaint(true);
|
||||
});
|
||||
graph.on('node:mouseleave', clearAllStats);
|
||||
graph.on('canvas:click', clearAllStats);
|
||||
|
||||
graph.data({
|
||||
nodes: data.nodes,
|
||||
edges: data.edges.map(function(edge, i) {
|
||||
edge.id = 'edge' + i;
|
||||
return Object.assign({}, edge);
|
||||
})
|
||||
});
|
||||
var simulation = d3.forceSimulation().force("link", d3.forceLink().id(function(d) {
|
||||
return d.id;
|
||||
}).strength(0.5)).force("charge", d3.forceManyBody()).force("center", d3.forceCenter(window.innerWidth / 2, window.innerHeight / 2));
|
||||
simulation.nodes(data.nodes).on("tick", ticked);
|
||||
simulation.force("link").links(data.edges);
|
||||
|
||||
graph.render();
|
||||
|
||||
function ticked() {
|
||||
graph.refreshPositions();
|
||||
graph.paint();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
310
demos/linePoint.html
Normal file
310
demos/linePoint.html
Normal file
@ -0,0 +1,310 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mountNode"></div>
|
||||
</body>
|
||||
<script src="./assets/dagre.js"></script>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script>
|
||||
/**
|
||||
* node 特殊属性
|
||||
*/
|
||||
const nodeExtraAttrs = {
|
||||
begin: {
|
||||
fill: "#9FD4FB"
|
||||
},
|
||||
end: {
|
||||
fill: "#C2E999"
|
||||
}
|
||||
};
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: "1",
|
||||
label: "请求回放1(开始)",
|
||||
type: "begin"
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
label: "交易创建"
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
label: "请求回放3"
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
label: "请求回放4"
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
label: "请求回放5"
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
label: "请求回放6"
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
label: "请求回放2(结束)",
|
||||
type: "end"
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: "1",
|
||||
target: "2"
|
||||
},
|
||||
{
|
||||
source: "1",
|
||||
target: "3"
|
||||
},
|
||||
{
|
||||
source: "2",
|
||||
target: "5"
|
||||
},
|
||||
{
|
||||
source: "5",
|
||||
target: "6"
|
||||
},
|
||||
{
|
||||
source: "6",
|
||||
target: "7"
|
||||
},
|
||||
{
|
||||
source: "3",
|
||||
target: "4"
|
||||
},
|
||||
{
|
||||
source: "4",
|
||||
target: "7"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* 自定义节点
|
||||
*/
|
||||
G6.registerNode(
|
||||
"node",
|
||||
{
|
||||
drawShape: function drawShape(cfg, group) {
|
||||
var rect = group.addShape("rect", {
|
||||
attrs: {
|
||||
x: -75,
|
||||
y: -25,
|
||||
width: 150,
|
||||
height: 50,
|
||||
radius: 4,
|
||||
fill: "#FFD591",
|
||||
fillOpacity: 1,
|
||||
...nodeExtraAttrs[cfg.type]
|
||||
}
|
||||
});
|
||||
return rect;
|
||||
},
|
||||
// 设置状态
|
||||
setState(name, value, item) {
|
||||
const group = item.getContainer();
|
||||
const shape = group.get("children")[0]; // 顺序根据 draw 时确定
|
||||
|
||||
if (name === "selected") {
|
||||
if (value) {
|
||||
shape.attr("fill", "#F6C277");
|
||||
} else {
|
||||
shape.attr("fill", "#FFD591");
|
||||
}
|
||||
}
|
||||
},
|
||||
getAnchorPoints: function getAnchorPoints() {
|
||||
return [[0, 0.5], [1, 0.5]];
|
||||
}
|
||||
},
|
||||
"single-shape"
|
||||
);
|
||||
|
||||
/**
|
||||
* 自定义 edge 中心关系节点
|
||||
*/
|
||||
G6.registerNode(
|
||||
"statusNode",
|
||||
{
|
||||
drawShape: function drawShape(cfg, group) {
|
||||
const circle = group.addShape("circle", {
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
r: 6,
|
||||
// fill: '#ccc',
|
||||
fill: cfg.active ? "#AB83E4" : "#ccc"
|
||||
}
|
||||
});
|
||||
// const text = group.addShape('text', {
|
||||
// attrs: {
|
||||
// x: 0,
|
||||
// y: -20,
|
||||
// textAlign: 'center',
|
||||
// text: cfg.label,
|
||||
// fill: '#444'
|
||||
// }
|
||||
// });
|
||||
return circle;
|
||||
}
|
||||
},
|
||||
"single-shape"
|
||||
);
|
||||
|
||||
/**
|
||||
* 自定义带箭头的贝塞尔曲线 edge
|
||||
*/
|
||||
G6.registerEdge("line-with-arrow", {
|
||||
itemType: "edge",
|
||||
draw: function draw(cfg, group) {
|
||||
const startPoint = cfg.startPoint;
|
||||
const endPoint = cfg.endPoint;
|
||||
const centerPoint = {
|
||||
x: (startPoint.x + endPoint.x) / 2,
|
||||
y: (startPoint.y + endPoint.y) / 2
|
||||
};
|
||||
// 控制点坐标
|
||||
const controlPoint = {
|
||||
x: (startPoint.x + centerPoint.x) / 2,
|
||||
y: startPoint.y
|
||||
};
|
||||
|
||||
console.log(cfg, startPoint, endPoint);
|
||||
|
||||
// 为了更好的展示效果, 对称贝塞尔曲线需要连到箭头根部
|
||||
const path = group.addShape("path", {
|
||||
attrs: {
|
||||
path: [
|
||||
["M", startPoint.x, startPoint.y],
|
||||
[
|
||||
"Q",
|
||||
controlPoint.x + 8,
|
||||
controlPoint.y,
|
||||
centerPoint.x,
|
||||
centerPoint.y
|
||||
],
|
||||
["T", endPoint.x - 8, endPoint.y],
|
||||
["L", endPoint.x, endPoint.y]
|
||||
],
|
||||
stroke: "#ccc",
|
||||
lineWidth: 1.6,
|
||||
endArrow: {
|
||||
path: "M 4,0 L -4,-4 L -4,4 Z",
|
||||
d: 4
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 计算贝塞尔曲线上的中心点
|
||||
// 参考资料 https://stackoverflow.com/questions/54216448/how-to-find-a-middle-point-of-a-beizer-curve
|
||||
// const lineCenterPoint = {
|
||||
// x:
|
||||
// (1 / 8) * startPoint.x +
|
||||
// (3 / 8) * (controlPoint.x + 8) +
|
||||
// (3 / 8) * centerPoint.x +
|
||||
// (1 / 8) * (endPoint.x - 8),
|
||||
// y:
|
||||
// (1 / 8) * startPoint.y +
|
||||
// (3 / 8) * controlPoint.y +
|
||||
// (3 / 8) * centerPoint.y +
|
||||
// (1 / 8) * endPoint.y
|
||||
// };
|
||||
|
||||
// 在贝塞尔曲线中心点上添加圆形
|
||||
const { source, target } = cfg;
|
||||
group.addShape("circle", {
|
||||
attrs: {
|
||||
id: `statusNode${source}-${target}`,
|
||||
r: 6,
|
||||
x: centerPoint.x,
|
||||
y: centerPoint.y,
|
||||
fill: cfg.active ? "#AB83E4" : "#ccc"
|
||||
}
|
||||
});
|
||||
|
||||
return path;
|
||||
}
|
||||
});
|
||||
|
||||
const g = new dagre.graphlib.Graph();
|
||||
g.setDefaultEdgeLabel(function() { return {}; });
|
||||
g.setGraph({ rankdir: 'LR' });
|
||||
|
||||
data.nodes.forEach(node => {
|
||||
g.setNode(node.id + '', { width: 150, height: 50 });
|
||||
});
|
||||
|
||||
data.edges.forEach(edge => {
|
||||
edge.source = edge.source + '';
|
||||
edge.target = edge.target + '';
|
||||
g.setEdge(edge.source, edge.target);
|
||||
});
|
||||
dagre.layout(g);
|
||||
|
||||
let coord;
|
||||
g.nodes().forEach((node, i) => {
|
||||
coord = g.node(node);
|
||||
data.nodes[i].x = coord.x;
|
||||
data.nodes[i].y = coord.y;
|
||||
});
|
||||
g.edges().forEach((edge, i) => {
|
||||
coord = g.edge(edge);
|
||||
const startPoint = coord.points[0];
|
||||
const endPoint = coord.points[coord.points.length - 1];
|
||||
data.edges[i].startPoint = startPoint;
|
||||
data.edges[i].endPoint = endPoint;
|
||||
data.edges[i].controlPoints = coord.points.slice(
|
||||
1,
|
||||
coord.points.length - 1
|
||||
);
|
||||
});
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: "mountNode",
|
||||
width: 1000,
|
||||
height: 800,
|
||||
defaultNode: {
|
||||
shape: "node",
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: "#fff",
|
||||
fontSize: 14
|
||||
}
|
||||
}
|
||||
},
|
||||
defaultEdge: {
|
||||
shape: "line-with-arrow"
|
||||
},
|
||||
edgeStyle: {
|
||||
default: {
|
||||
endArrow: true,
|
||||
lineWidth: 2,
|
||||
stroke: "#ccc"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
graph.on('edge:click', evt => {
|
||||
const { target } = evt
|
||||
const type = target.get('type')
|
||||
if(type === 'circle') {
|
||||
// 点击边上的circle
|
||||
alert('你点击的是边上的圆点')
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
</html>
|
91
demos/loop-line-animate.html
Normal file
91
demos/loop-line-animate.html
Normal file
@ -0,0 +1,91 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>自环边设置动画</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="mountNode"></div>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script>
|
||||
G6.registerNode('circleNode', {
|
||||
drawShape(cfg, group) {
|
||||
const keyShape = group.addShape('circle', {
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
r: 30,
|
||||
fill: '#87e8de'
|
||||
}
|
||||
});
|
||||
|
||||
return keyShape;
|
||||
}
|
||||
}, 'circle');
|
||||
|
||||
G6.registerEdge('loop-growth', {
|
||||
afterDraw(cfg, group) {
|
||||
const shape = group.get('children')[0];
|
||||
const length = shape.getTotalLength();
|
||||
shape.animate({
|
||||
onFrame(ratio) {
|
||||
const startLen = ratio * length;
|
||||
// 计算线的lineDash
|
||||
const cfg = {
|
||||
lineDash: [startLen, length - startLen]
|
||||
};
|
||||
return cfg;
|
||||
},
|
||||
repeat: true
|
||||
}, 2000);
|
||||
}
|
||||
}, 'loop');
|
||||
|
||||
const data = {
|
||||
nodes: [{
|
||||
x: 300,
|
||||
y: 300,
|
||||
shape: 'circleNode',
|
||||
label: 'rect',
|
||||
id:'node1',
|
||||
labelCfg: {
|
||||
position: 'bottom'
|
||||
},
|
||||
anchorPoints: [
|
||||
[0.5, 0, {type: 'circle', style: {stroke: 'red', fill: 'red'}}],
|
||||
[1, 0.5, {type: 'circle', style: {stroke: 'blue', fill: 'red'}}]
|
||||
]
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'node1',
|
||||
target: 'node1',
|
||||
label: 'loop',
|
||||
shape:'loop-growth',
|
||||
labelCfg: {
|
||||
refY: 10
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 1000,
|
||||
height: 600,
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
},
|
||||
modes: {
|
||||
default: ['drag-node']
|
||||
}
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
120
demos/node-bg-img.html
Normal file
120
demos/node-bg-img.html
Normal file
@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>节点背景使用图片</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="mountNode"></div>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script>
|
||||
/**
|
||||
* 本示例演示以下功能:
|
||||
* 1、如何使用图片作为节点背景;
|
||||
* 2、点击切换节点背景图片。
|
||||
*
|
||||
*/
|
||||
G6.registerNode('circleNode', {
|
||||
drawShape(cfg, group) {
|
||||
const keyShape = group.addShape('circle', {
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
r: 30,
|
||||
fill: '#87e8de'
|
||||
}
|
||||
});
|
||||
|
||||
return keyShape;
|
||||
}
|
||||
}, 'circle');
|
||||
|
||||
const img = new Image();
|
||||
img.src = 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1566553535233&di=b0b17eeea7bd7356a6f42ebfd48e9441&imgtype=0&src=http%3A%2F%2Fa2.att.hudong.com%2F64%2F29%2F01300543361379145388299988437_s.jpg';
|
||||
|
||||
// 点击图片节点,切换背景图片
|
||||
const img2 = new Image();
|
||||
img2.src = 'http://seopic.699pic.com/photo/50055/5642.jpg_wh1200.jpg';
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
x: 100,
|
||||
y: 100,
|
||||
shape: 'circleNode',
|
||||
label: 'circle',
|
||||
id:'node1',
|
||||
labelCfg: {
|
||||
position: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
x: 400,
|
||||
y: 100,
|
||||
shape: 'image',
|
||||
id:'node2',
|
||||
img: img.src ,
|
||||
label: '头像',
|
||||
style: {
|
||||
cursor: 'pointer'
|
||||
},
|
||||
labelCfg: {
|
||||
position: 'bottom'
|
||||
}
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
label: 'line',
|
||||
labelCfg: {
|
||||
refY: 10
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
// 避免拖动过程中闪烁:使用加载已经LOAD好的图片
|
||||
img.onload = function() {
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 1000,
|
||||
height: 600,
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
},
|
||||
modes: {
|
||||
default: ['drag-node', {
|
||||
type: 'drag-node',
|
||||
}],
|
||||
}
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
graph.on('node:click', evt => {
|
||||
const { target } = evt;
|
||||
const type = target.get('type');
|
||||
const hasChangeBg = target.get('hasChangeBg');
|
||||
console.log(target)
|
||||
if(type === 'image') {
|
||||
if(!hasChangeBg) {
|
||||
// 点击图片节点时,切换背景图片
|
||||
target.attr('img', img2);
|
||||
target.attr('imgSrc', 'http://seopic.699pic.com/photo/50055/5642.jpg_wh1200.jpg')
|
||||
target.set('hasChangeBg', true);
|
||||
} else {
|
||||
target.attr('img', img);
|
||||
target.attr('imgSrc', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1566553535233&di=b0b17eeea7bd7356a6f42ebfd48e9441&imgtype=0&src=http%3A%2F%2Fa2.att.hudong.com%2F64%2F29%2F01300543361379145388299988437_s.jpg')
|
||||
target.set('hasChangeBg', false);
|
||||
}
|
||||
graph.paint()
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
98
demos/pie-node.html
Normal file
98
demos/pie-node.html
Normal file
@ -0,0 +1,98 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mountNode"></div>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script>
|
||||
/**
|
||||
* 该实例演示如何使用G6自定义类似饼图的节点
|
||||
* by 十吾
|
||||
*
|
||||
*/
|
||||
const lightBlue = 'rgb(119, 243, 252)';
|
||||
const lightOrange = 'rgb(230, 100, 64)';
|
||||
|
||||
// 注册自定义名为 pie-node 的节点类型
|
||||
G6.registerNode('pie-node', {
|
||||
drawShape: (cfg, group) => {
|
||||
const radius = cfg.size / 2; // 节点半径
|
||||
const inPercentage = cfg.inDegree / cfg.degree; // 入度占总度数的比例
|
||||
const inAngle = inPercentage * Math.PI * 2; // 入度在饼图中的夹角大小
|
||||
const outAngle = Math.PI * 2 - inAngle; // 出度在饼图中的夹角大小
|
||||
const inArcEnd = [radius * Math.cos(inAngle), radius * Math.sin(inAngle)]; //入度饼图弧结束位置
|
||||
let isInBigArc = 1, isOutBigArc = 0;
|
||||
if (inAngle > Math.PI) {
|
||||
isInBigArc = 0;
|
||||
isOutBigArc = 1;
|
||||
}
|
||||
// 定义代表入度的扇形形状
|
||||
const fanIn = group.addShape('path', {
|
||||
attrs: {
|
||||
path: [
|
||||
[ 'M', radius, 0 ],
|
||||
[ 'A', radius, radius, 0, isInBigArc, 0, inArcEnd[0], inArcEnd[1] ],
|
||||
[ 'L', 0, 0 ],
|
||||
[ 'B' ]
|
||||
],
|
||||
lineWidth: 0,
|
||||
fill: lightOrange
|
||||
}
|
||||
});
|
||||
// 定义代表出度的扇形形状
|
||||
const fanOut = group.addShape('path', {
|
||||
attrs: {
|
||||
path: [
|
||||
[ 'M', inArcEnd[0], inArcEnd[1] ],
|
||||
[ 'A', radius, radius, 0, isOutBigArc, 0, radius, 0 ],
|
||||
[ 'L', 0, 0 ],
|
||||
[ 'B' ]
|
||||
],
|
||||
lineWidth: 0,
|
||||
fill: lightBlue
|
||||
}
|
||||
});
|
||||
// 返回 keyshape
|
||||
return fanIn;
|
||||
}
|
||||
},
|
||||
"single-shape"
|
||||
);
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
size: 80,
|
||||
inDegree: 80,
|
||||
degree: 360,
|
||||
x: 50,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
size: 80,
|
||||
inDegree: 280,
|
||||
degree: 360,
|
||||
x: 200,
|
||||
y: 200
|
||||
}
|
||||
]
|
||||
}
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 800,
|
||||
height: 600,
|
||||
defaultNode: {
|
||||
shape: 'pie-node'
|
||||
}
|
||||
})
|
||||
|
||||
graph.data(data)
|
||||
graph.render()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
127
demos/register-polyline.html
Normal file
127
demos/register-polyline.html
Normal file
@ -0,0 +1,127 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mountNode"></div>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script>
|
||||
/**
|
||||
* 该案例演示如何自定义折线,有两种方式:
|
||||
* 1、继承line,复写getPath和getShapeStyle方法;
|
||||
* 2、复写draw方法。
|
||||
* by siogo 提供的 issue(https://github.com/antvis/g6/issues/814)
|
||||
*
|
||||
*/
|
||||
G6.registerNode('circleNode', {
|
||||
drawShape(cfg, group) {
|
||||
const keyShape = group.addShape('circle', {
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
r: 30,
|
||||
fill: '#87e8de'
|
||||
}
|
||||
});
|
||||
|
||||
return keyShape;
|
||||
}
|
||||
}, 'circle');
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{id: '7', x: 200, y: 200, size: 40, shape: 'circleNode',anchorPoints: [[1, 0.5], [1, 0]]},
|
||||
{id: '8', x: 400, y: 400, size: 40, shape: 'circleNode',anchorPoints: [[0, 0.5], [0, 1]]},
|
||||
],
|
||||
edges: [
|
||||
{source: '7', target: '8', shape: 'line-arrow-self',sourceAnchor: 0,targetAnchor: 0,}
|
||||
]
|
||||
};
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
// 支持的 behavior
|
||||
default: [ 'drag-node'],
|
||||
}
|
||||
});
|
||||
|
||||
G6.registerEdge('line-arrow', {
|
||||
draw(cfg, group) {
|
||||
const { startPoint, endPoint } = cfg
|
||||
const keyShape = group.addShape('path', {
|
||||
attrs: {
|
||||
path: [
|
||||
['M', startPoint.x, startPoint.y],
|
||||
['L', endPoint.x / 3 + 2 / 3 * startPoint.x , startPoint.y],
|
||||
['L', endPoint.x / 3 + 2 / 3 * startPoint.x , endPoint.y],
|
||||
['L', endPoint.x, endPoint.y]
|
||||
],
|
||||
stroke: '#BBB',
|
||||
lineWidth: 1,
|
||||
startArrow: {
|
||||
path: 'M 6,0 L -6,-6 L -3,0 L -6,6 Z',
|
||||
d: 6
|
||||
},
|
||||
endArrow: {
|
||||
path: 'M 6,0 L -6,-6 L -3,0 L -6,6 Z',
|
||||
d: 6
|
||||
},
|
||||
className: 'edge-shape'
|
||||
}
|
||||
});
|
||||
return keyShape
|
||||
}
|
||||
});
|
||||
|
||||
G6.registerEdge('line-arrow-self', {
|
||||
getPath(points) {
|
||||
const startPoint = points[0]
|
||||
const endPoint = points[1]
|
||||
return [
|
||||
['M', startPoint.x, startPoint.y],
|
||||
['L', endPoint.x / 3 + 2 / 3 * startPoint.x , startPoint.y],
|
||||
['L', endPoint.x / 3 + 2 / 3 * startPoint.x , endPoint.y],
|
||||
['L', endPoint.x, endPoint.y]
|
||||
]
|
||||
},
|
||||
getShapeStyle(cfg) {
|
||||
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);
|
||||
const style = G6.Util.mix({}, G6.Global.defaultEdge.style, {
|
||||
stroke: '#BBB',
|
||||
lineWidth: 1,
|
||||
path,
|
||||
startArrow: {
|
||||
path: 'M 6,0 L -6,-6 L -3,0 L -6,6 Z',
|
||||
d: 6
|
||||
},
|
||||
endArrow: {
|
||||
path: 'M 6,0 L -6,-6 L -3,0 L -6,6 Z',
|
||||
d: 6
|
||||
},
|
||||
}, cfg.style);
|
||||
return style;
|
||||
},
|
||||
}, 'line');
|
||||
|
||||
graph.data(data)
|
||||
graph.render()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
112
demos/self-node-click.html
Normal file
112
demos/self-node-click.html
Normal file
@ -0,0 +1,112 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>自定义节点选中样式</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="mountNode"></div>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script>
|
||||
/**
|
||||
* 本案例演示如何通过交互设置元素状态:当鼠标hover到圆形节点上时,圆形节点执行动画效果
|
||||
* by 镜曦。
|
||||
*/
|
||||
G6.registerNode('animate-circle', {
|
||||
setState: function setState(name, value, item) {
|
||||
var shape = item.get('keyShape');
|
||||
const cfg = item.get('model');
|
||||
if (name === 'running') {
|
||||
if (value) {
|
||||
shape.animate({
|
||||
repeat: true,
|
||||
onFrame: function onFrame(ratio) {
|
||||
var diff = ratio <= 0.5 ? ratio * 10 : (1 - ratio) * 10;
|
||||
return {
|
||||
r: cfg.size / 2 + diff
|
||||
};
|
||||
}
|
||||
}, 1000, 'easeCubic');
|
||||
} else {
|
||||
shape.stopAnimate();
|
||||
shape.attr('lineDash', null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 'circle');
|
||||
const data = {
|
||||
nodes: [{
|
||||
x: 100,
|
||||
y: 100,
|
||||
shape: 'animate-circle',
|
||||
label: 'animate-circle',
|
||||
id: 'node1',
|
||||
size: 30,
|
||||
labelCfg: {
|
||||
position: 'bottom'
|
||||
},
|
||||
|
||||
}, {
|
||||
x: 400,
|
||||
y: 100,
|
||||
shape: 'rect',
|
||||
label: 'rect2',
|
||||
id: 'node2',
|
||||
labelCfg: {
|
||||
position: 'bottom'
|
||||
},
|
||||
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
label: 'line',
|
||||
labelCfg: {
|
||||
refY: 10
|
||||
},
|
||||
style: {
|
||||
endArrow: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 1000,
|
||||
height: 600,
|
||||
defaultNode: {
|
||||
style: {
|
||||
fill: '#87e8de'
|
||||
},
|
||||
color: '#87e8de'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
},
|
||||
modes: {
|
||||
default: ['drag-node', {
|
||||
type: 'drag-node',
|
||||
}],
|
||||
}
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
graph.on('node:mouseenter', function (ev) {
|
||||
var node = ev.item;
|
||||
graph.setItemState(node, 'running', true);
|
||||
});
|
||||
|
||||
graph.on('node:mouseleave', function (ev) {
|
||||
var node = ev.item;
|
||||
graph.setItemState(node, 'running', false);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -16,7 +16,7 @@
|
||||
<script src="assets/hierarchy.js"></script>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script>
|
||||
let currentLayout = 'dendrogram';
|
||||
let currentLayout = 'compactBox';
|
||||
const layouts = {
|
||||
dendrogram: {
|
||||
type: 'dendrogram',
|
||||
@ -28,6 +28,7 @@
|
||||
compactBox: {
|
||||
type: 'compactBox',
|
||||
direction: 'LR',
|
||||
fixedPosition: true,
|
||||
getId(d) { return d.id; },
|
||||
getHeight() { return 16 },
|
||||
getWidth() { return 16 },
|
||||
@ -52,7 +53,7 @@
|
||||
default: ['collapse-expand', 'drag-canvas']
|
||||
},
|
||||
fitView: true,
|
||||
layout: layouts.dendrogram
|
||||
layout: layouts.compactBox
|
||||
});
|
||||
graph.node(node => {
|
||||
return {
|
||||
|
@ -98,7 +98,8 @@
|
||||
"screenshot": "node ./bin/screenshot.js",
|
||||
"start": "npm run dev",
|
||||
"test": "torch --compile --renderer --opts test/mocha.opts --recursive ./test/unit",
|
||||
"test-live": "torch --compile --interactive --watch --opts test/mocha.opts --recursive ./test/unit/behavior/drag-node-spec.js",
|
||||
"test-live": "torch --compile --interactive --watch --opts test/mocha.opts --recursive ./test/unit/behavior/drag-group-spec.js",
|
||||
"test-live-tree": "torch --compile --interactive --watch --opts test/mocha.opts --recursive ./test/unit/graph/tree-graph-spec.js",
|
||||
"test-bugs": "torch --compile --renderer --recursive ./test/bugs",
|
||||
"test-bugs-live": "torch --compile --interactive --watch --recursive ./test/bugs",
|
||||
"test-all": "npm run test && npm run test-bugs",
|
||||
|
33
src/behavior/collapse-expand-group.js
Normal file
33
src/behavior/collapse-expand-group.js
Normal file
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* @Author: moyee
|
||||
* @Date: 2019-07-31 14:36:15
|
||||
* @LastEditors: moyee
|
||||
* @LastEditTime: 2019-08-22 18:43:24
|
||||
* @Description: 收起和展开群组
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
getDefaultCfg() {
|
||||
return {
|
||||
delegateShapeBBoxs: {}
|
||||
};
|
||||
},
|
||||
getEvents() {
|
||||
return {
|
||||
dblclick: 'onDblClick'
|
||||
};
|
||||
},
|
||||
onDblClick(evt) {
|
||||
const { target } = evt;
|
||||
|
||||
const groupId = target.get('groupId');
|
||||
if (!groupId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const graph = this.graph;
|
||||
|
||||
const customGroupControll = graph.get('customGroupControll');
|
||||
customGroupControll.collapseExpandGroup(groupId);
|
||||
}
|
||||
};
|
321
src/behavior/drag-group.js
Normal file
321
src/behavior/drag-group.js
Normal file
@ -0,0 +1,321 @@
|
||||
/*
|
||||
* @Author: moyee
|
||||
* @Date: 2019-07-31 14:36:15
|
||||
* @LastEditors: moyee
|
||||
* @LastEditTime: 2019-08-23 11:13:43
|
||||
* @Description: 拖动群组
|
||||
*/
|
||||
const { merge } = require('lodash');
|
||||
|
||||
const delegateStyle = {
|
||||
fill: '#F3F9FF',
|
||||
fillOpacity: 0.5,
|
||||
stroke: '#1890FF',
|
||||
strokeOpacity: 0.9,
|
||||
lineDash: [ 5, 5 ]
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getDefaultCfg() {
|
||||
return {
|
||||
delegate: true,
|
||||
delegateStyle: {},
|
||||
delegateShapes: {},
|
||||
delegateShapeBBoxs: {}
|
||||
};
|
||||
},
|
||||
getEvents() {
|
||||
return {
|
||||
dragstart: 'onDragStart',
|
||||
drag: 'onDrag',
|
||||
dragend: 'onDragEnd'
|
||||
};
|
||||
},
|
||||
onDragStart(evt) {
|
||||
const { target } = evt;
|
||||
// 获取拖动的group的ID,如果拖动的不是group,则直接return
|
||||
const groupId = target.get('groupId');
|
||||
if (!groupId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const graph = this.graph;
|
||||
|
||||
const customGroupControll = graph.get('customGroupControll');
|
||||
const customGroup = customGroupControll.customGroup;
|
||||
|
||||
const currentGroup = customGroup[groupId].nodeGroup;
|
||||
|
||||
this.targetGroup = currentGroup;
|
||||
const groupOriginBBox = customGroupControll.getGroupOriginBBox(groupId);
|
||||
const keyShape = this.targetGroup.get('keyShape');
|
||||
if (!groupOriginBBox) {
|
||||
customGroupControll.setGroupOriginBBox(groupId, keyShape.getBBox());
|
||||
}
|
||||
|
||||
this.mouseOrigin = {
|
||||
x: evt.canvasX,
|
||||
y: evt.canvasY
|
||||
};
|
||||
this.nodePoint = [];
|
||||
|
||||
// 获取groupId的父Group的ID
|
||||
const { groups } = graph.save();
|
||||
let parentGroupId = null;
|
||||
for (const group of groups) {
|
||||
if (groupId !== group.id) {
|
||||
continue;
|
||||
}
|
||||
parentGroupId = group.parentId;
|
||||
break;
|
||||
}
|
||||
|
||||
if (parentGroupId) {
|
||||
const parentGroup = customGroup[parentGroupId].nodeGroup;
|
||||
customGroupControll.setGroupStyle(parentGroup.get('keyShape'), 'hover');
|
||||
}
|
||||
},
|
||||
onDrag(evt) {
|
||||
if (!this.mouseOrigin) {
|
||||
return false;
|
||||
}
|
||||
this._updateDelegate(evt);
|
||||
},
|
||||
|
||||
onDragEnd(evt) {
|
||||
// 删除delegate shape
|
||||
const groupId = evt.target.get('groupId');
|
||||
if (this.delegateShapes[groupId]) {
|
||||
this.delegateShapeBBox = this.delegateShapes[groupId].getBBox();
|
||||
this.delegateShapes[groupId].remove();
|
||||
delete this.delegateShapes[groupId];
|
||||
}
|
||||
|
||||
const graph = this.graph;
|
||||
const autoPaint = graph.get('autoPaint');
|
||||
graph.setAutoPaint(false);
|
||||
|
||||
// 修改群组位置
|
||||
this.updatePosition(evt);
|
||||
|
||||
graph.setAutoPaint(autoPaint);
|
||||
graph.paint();
|
||||
|
||||
this.mouseOrigin = null;
|
||||
this.shapeOrigin = null;
|
||||
// 在两个节点之间连线时也会执行到这里,此时this.nodePoint值为undefined
|
||||
if (this.nodePoint) {
|
||||
this.nodePoint.length = 0;
|
||||
}
|
||||
this.delegateShapeBBox = null;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 更新群组及群组中节点和边的位置
|
||||
*
|
||||
* @param {Event} evt 事件句柄
|
||||
* @return {boolean} false/true
|
||||
*/
|
||||
updatePosition(evt) {
|
||||
if (!this.delegateShapeBBox) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const graph = this.graph;
|
||||
|
||||
// 更新群组里面节点和线的位置
|
||||
this.updateItemPosition(evt);
|
||||
|
||||
const customGroupControll = graph.get('customGroupControll');
|
||||
const customGroup = customGroupControll.customGroup;
|
||||
const groupId = evt.target.get('groupId');
|
||||
// 判断是否拖动出了parent group外面,如果拖出了parent Group外面,则更新数据,去掉group关联
|
||||
// 获取groupId的父Group的ID
|
||||
const { groups } = graph.save();
|
||||
let parentGroupId = null;
|
||||
let parentGroupData = null;
|
||||
for (const group of groups) {
|
||||
if (groupId !== group.id) {
|
||||
continue;
|
||||
}
|
||||
parentGroupId = group.parentId;
|
||||
parentGroupData = group;
|
||||
break;
|
||||
}
|
||||
|
||||
if (parentGroupId) {
|
||||
const parentGroup = customGroup[parentGroupId].nodeGroup;
|
||||
const parentKeyShape = parentGroup.get('keyShape');
|
||||
customGroupControll.setGroupStyle(parentKeyShape, 'default');
|
||||
|
||||
const parentGroupBBox = parentKeyShape.getBBox();
|
||||
const { minX, minY, maxX, maxY } = parentGroupBBox;
|
||||
|
||||
// 检查是否拖出了父Group
|
||||
const currentGroup = customGroup[groupId].nodeGroup;
|
||||
const currentKeyShape = currentGroup.get('keyShape');
|
||||
const currentKeyShapeBBox = currentKeyShape.getBBox();
|
||||
const { x, y } = currentKeyShapeBBox;
|
||||
|
||||
if (!(x < maxX && x > minX && y < maxY && y > minY)) {
|
||||
// 拖出了parent group,则取消parent group ID
|
||||
delete parentGroupData.parentId;
|
||||
// 同时删除groupID中的节点
|
||||
const groupNodes = graph.get('groupNodes');
|
||||
const currentGroupNodes = groupNodes[groupId];
|
||||
const parentGroupNodes = groupNodes[parentGroupId];
|
||||
|
||||
groupNodes[parentGroupId] = parentGroupNodes.filter(node => currentGroupNodes.indexOf(node) === -1);
|
||||
|
||||
const { x: x1, y: y1, width, height } = customGroupControll.calculationGroupPosition(groupNodes[parentGroupId]);
|
||||
const groups = graph.get('groups');
|
||||
const hasSubGroup = !!groups.filter(g => g.parentId === parentGroupId).length > 0;
|
||||
const r = width > height ? width / 2 : height / 2 + (hasSubGroup ? 20 : 0);
|
||||
|
||||
const cx = (width + 2 * x1) / 2;
|
||||
const cy = (height + 2 * y1) / 2;
|
||||
// groupKeyShape.attr('x', cx);
|
||||
// groupKeyShape.attr('y', cy);
|
||||
parentKeyShape.attr({
|
||||
r: r + groupNodes[groupId].length * 10,
|
||||
x: cx,
|
||||
y: cy
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新群组中节点、边的位置
|
||||
*
|
||||
* @param {Event} evt 事件句柄
|
||||
*/
|
||||
updateItemPosition(evt) {
|
||||
const groupId = evt.target.get('groupId');
|
||||
|
||||
const graph = this.graph;
|
||||
|
||||
// 获取群组对象
|
||||
const customGroupControll = graph.get('customGroupControll');
|
||||
|
||||
const groupNodes = graph.get('groupNodes');
|
||||
|
||||
// step 1:先修改groupId中的节点位置
|
||||
const nodeInGroup = groupNodes[groupId];
|
||||
const groupOriginBBox = customGroupControll.getGroupOriginBBox(groupId);
|
||||
const delegateShapeBBoxs = this.delegateShapeBBoxs[groupId];
|
||||
const otherGroupId = [];
|
||||
nodeInGroup.forEach((nodeId, index) => {
|
||||
|
||||
const node = graph.findById(nodeId);
|
||||
const model = node.getModel();
|
||||
if (model.groupId && !otherGroupId.includes(model.groupId)) {
|
||||
otherGroupId.push(model.groupId);
|
||||
}
|
||||
if (!this.nodePoint[index]) {
|
||||
this.nodePoint[index] = {
|
||||
x: model.x,
|
||||
y: model.y
|
||||
};
|
||||
}
|
||||
|
||||
// 群组拖动后节点的位置:deletateShape的最终位置-群组起始位置+节点位置
|
||||
const x = delegateShapeBBoxs.x - groupOriginBBox.x + this.nodePoint[index].x;
|
||||
const y = delegateShapeBBoxs.y - groupOriginBBox.y + this.nodePoint[index].y;
|
||||
|
||||
this.nodePoint[index] = {
|
||||
x, y
|
||||
};
|
||||
|
||||
graph.updateItem(node, { x, y });
|
||||
});
|
||||
// step 2:修改父group中其他节点的位置
|
||||
|
||||
// 更新完群组位置后,重新设置群组起始位置
|
||||
const customGroups = customGroupControll.customGroup;
|
||||
|
||||
// otherGroupId中是否包括当前groupId,如果不包括,则添加进去
|
||||
if (!otherGroupId.includes(groupId)) {
|
||||
otherGroupId.push(groupId);
|
||||
}
|
||||
|
||||
otherGroupId.forEach(id => {
|
||||
// 更新群组位置
|
||||
const { nodeGroup } = customGroups[id];
|
||||
const groupKeyShape = nodeGroup.get('keyShape');
|
||||
|
||||
const { x, y, width, height } = customGroupControll.calculationGroupPosition(groupNodes[id]);
|
||||
const cx = (width + 2 * x) / 2;
|
||||
const cy = (height + 2 * y) / 2;
|
||||
groupKeyShape.attr('x', cx);
|
||||
groupKeyShape.attr('y', cy);
|
||||
customGroupControll.setGroupOriginBBox(id, groupKeyShape.getBBox());
|
||||
});
|
||||
|
||||
},
|
||||
_updateDelegate(evt) {
|
||||
const self = this;
|
||||
const groupId = evt.target.get('groupId');
|
||||
const item = this.targetGroup.get('keyShape');
|
||||
const graph = this.graph;
|
||||
const autoPaint = graph.get('autoPaint');
|
||||
graph.setAutoPaint(false);
|
||||
|
||||
let delegateShape = self.delegateShapes[groupId];
|
||||
const groupBbox = item.getBBox();
|
||||
const delegateType = item.get('type');
|
||||
if (!delegateShape) {
|
||||
const delegateGroup = graph.get('delegateGroup');
|
||||
const { width, height } = groupBbox;
|
||||
const x = evt.canvasX - width / 2;
|
||||
const y = evt.canvasY - height / 2;
|
||||
const attrs = {
|
||||
width,
|
||||
height,
|
||||
x,
|
||||
y,
|
||||
...merge({}, delegateStyle, this.delegateStyle)
|
||||
};
|
||||
|
||||
// 如果delegate是circle
|
||||
if (delegateType === 'circle') {
|
||||
const cx = evt.canvasX; // (width + 2 * x) / 2;
|
||||
const cy = evt.canvasY; // (height + 2 * y) / 2;
|
||||
const r = width > height ? width / 2 : height / 2;
|
||||
|
||||
delegateShape = delegateGroup.addShape('circle', {
|
||||
attrs: {
|
||||
x: cx,
|
||||
y: cy,
|
||||
r,
|
||||
...merge({}, delegateStyle, this.delegateStyle)
|
||||
}
|
||||
});
|
||||
self.shapeOrigin = { x: cx, y: cy };
|
||||
} else {
|
||||
delegateShape = delegateGroup.addShape('rect', {
|
||||
attrs
|
||||
});
|
||||
self.shapeOrigin = { x: attrs.x, y: attrs.y };
|
||||
}
|
||||
// delegateShape.set('capture', false);
|
||||
self.delegateShapes[groupId] = delegateShape;
|
||||
self.delegateShapeBBoxs[groupId] = delegateShape.getBBox();
|
||||
} else {
|
||||
const { mouseOrigin, shapeOrigin } = self;
|
||||
const deltaX = evt.canvasX - mouseOrigin.x;
|
||||
const deltaY = evt.canvasY - mouseOrigin.y;
|
||||
const x = deltaX + shapeOrigin.x;
|
||||
const y = deltaY + shapeOrigin.y;
|
||||
|
||||
// 将Canvas坐标转成视口坐标
|
||||
const point = graph.getPointByCanvas(x, y);
|
||||
delegateShape.attr({ x: point.x, y: point.y });
|
||||
self.delegateShapeBBoxs[groupId] = delegateShape.getBBox();
|
||||
}
|
||||
|
||||
graph.paint();
|
||||
graph.setAutoPaint(autoPaint);
|
||||
}
|
||||
};
|
424
src/behavior/drag-node-with-group.js
Normal file
424
src/behavior/drag-node-with-group.js
Normal file
@ -0,0 +1,424 @@
|
||||
/*
|
||||
* @Author: moyee
|
||||
* @Date: 2019-06-27 18:12:06
|
||||
* @LastEditors: moyee
|
||||
* @LastEditTime: 2019-08-23 13:54:53
|
||||
* @Description: 有group的情况下,拖动节点的Behavior
|
||||
*/
|
||||
const { merge } = require('lodash');
|
||||
const { delegateStyle } = require('../global');
|
||||
const body = document.body;
|
||||
|
||||
module.exports = {
|
||||
getDefaultCfg() {
|
||||
return {
|
||||
updateEdge: true,
|
||||
delegate: true,
|
||||
delegateStyle: {},
|
||||
maxMultiple: 1.2,
|
||||
minMultiple: 0.8
|
||||
};
|
||||
},
|
||||
getEvents() {
|
||||
return {
|
||||
'node:dragstart': 'onDragStart',
|
||||
'node:drag': 'onDrag',
|
||||
'node:dragend': 'onDragEnd',
|
||||
'canvas:mouseleave': 'onOutOfRange',
|
||||
mouseenter: 'onMouseEnter',
|
||||
mouseout: 'onMouseOut'
|
||||
};
|
||||
},
|
||||
onMouseEnter(evt) {
|
||||
const { target } = evt;
|
||||
const groupId = target.get('groupId');
|
||||
if (groupId && this.origin) {
|
||||
const graph = this.graph;
|
||||
const customGroupControll = graph.get('customGroupControll');
|
||||
const customGroup = customGroupControll.customGroup;
|
||||
const currentGroup = customGroup[groupId].nodeGroup;
|
||||
const keyShape = currentGroup.get('keyShape');
|
||||
|
||||
this.inGroupId = groupId;
|
||||
customGroupControll.setGroupStyle(keyShape, 'hover');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 拖动节点移除Group时的事件
|
||||
* @param {Event} evt 事件句柄
|
||||
*/
|
||||
onMouseOut(evt) {
|
||||
const { target } = evt;
|
||||
const groupId = target.get('groupId');
|
||||
if (groupId && this.origin) {
|
||||
const graph = this.graph;
|
||||
const customGroupControll = graph.get('customGroupControll');
|
||||
const customGroup = customGroupControll.customGroup;
|
||||
const currentGroup = customGroup[groupId].nodeGroup;
|
||||
const keyShape = currentGroup.get('keyShape');
|
||||
|
||||
customGroupControll.setGroupStyle(keyShape, 'default');
|
||||
}
|
||||
this.inGroupId = null;
|
||||
},
|
||||
onDragStart(e) {
|
||||
if (!this.shouldBegin.call(this, e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { item } = e;
|
||||
const graph = this.graph;
|
||||
|
||||
this.targets = [];
|
||||
|
||||
// 获取所有选中的元素
|
||||
const nodes = graph.findAllByState('node', 'selected');
|
||||
|
||||
const currentNodeId = item.get('id');
|
||||
|
||||
// 当前拖动的节点是否是选中的节点
|
||||
const dragNodes = nodes.filter(node => {
|
||||
const nodeId = node.get('id');
|
||||
return currentNodeId === nodeId;
|
||||
});
|
||||
|
||||
// 只拖动当前节点
|
||||
if (dragNodes.length === 0) {
|
||||
this.target = item;
|
||||
// 拖动节点时,如果在Group中,则Group高亮
|
||||
const model = item.getModel();
|
||||
const { groupId } = model;
|
||||
if (groupId) {
|
||||
const customGroupControll = graph.get('customGroupControll');
|
||||
const customGroup = customGroupControll.customGroup;
|
||||
const currentGroup = customGroup[groupId].nodeGroup;
|
||||
customGroupControll.setGroupStyle(currentGroup.get('keyShape'), 'hover');
|
||||
}
|
||||
} else {
|
||||
// 拖动多个节点
|
||||
if (nodes.length > 1) {
|
||||
nodes.forEach(node => {
|
||||
this.targets.push(node);
|
||||
});
|
||||
} else {
|
||||
this.targets.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
this.origin = {
|
||||
x: e.x,
|
||||
y: e.y
|
||||
};
|
||||
|
||||
this.point = {};
|
||||
this.originPoint = {};
|
||||
},
|
||||
onDrag(e) {
|
||||
if (!this.origin) {
|
||||
return;
|
||||
}
|
||||
if (!this.get('shouldUpdate').call(this, e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 当targets中元素时,则说明拖动的是多个选中的元素
|
||||
if (this.targets.length > 0) {
|
||||
this._updateDelegate(e);
|
||||
} else {
|
||||
// 只拖动单个元素
|
||||
this._update(this.target, e, true);
|
||||
const { item } = e;
|
||||
const graph = this.graph;
|
||||
const model = item.getModel();
|
||||
const { groupId } = model;
|
||||
if (groupId) {
|
||||
const customGroupControll = graph.get('customGroupControll');
|
||||
const customGroup = customGroupControll.customGroup;
|
||||
const currentGroup = customGroup[groupId].nodeGroup;
|
||||
const keyShape = currentGroup.get('keyShape');
|
||||
|
||||
const currentGroupBBox = keyShape.getBBox();
|
||||
|
||||
const delegateShape = this.target.get('delegateShape');
|
||||
const { x, y } = delegateShape.getBBox();
|
||||
const { minX, minY, maxX, maxY } = currentGroupBBox;
|
||||
|
||||
if (x > maxX || x < minX || y > maxY || y < minY) {
|
||||
customGroupControll.setGroupStyle(keyShape, 'default');
|
||||
} else {
|
||||
customGroupControll.setGroupStyle(keyShape, 'hover');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onDragEnd(e) {
|
||||
if (!this.origin || !this.shouldEnd.call(this, e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.shape) {
|
||||
this.shape.remove();
|
||||
this.shape = null;
|
||||
}
|
||||
|
||||
if (this.target) {
|
||||
const delegateShape = this.target.get('delegateShape');
|
||||
if (delegateShape) {
|
||||
delegateShape.remove();
|
||||
this.target.set('delegateShape', null);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.targets.length > 0) {
|
||||
// 获取所有已经选中的节点
|
||||
this.targets.forEach(node => this._update(node, e));
|
||||
} else if (this.target) {
|
||||
this._update(this.target, e);
|
||||
}
|
||||
|
||||
this.point = {};
|
||||
this.origin = null;
|
||||
this.originPoint = {};
|
||||
this.targets.length = 0;
|
||||
this.target = null;
|
||||
// 终止时需要判断此时是否在监听画布外的 mouseup 事件,若有则解绑
|
||||
const fn = this.fn;
|
||||
if (fn) {
|
||||
body.removeEventListener('mouseup', fn, false);
|
||||
this.fn = null;
|
||||
}
|
||||
|
||||
this.setCurrentGroupStyle(e);
|
||||
},
|
||||
/**
|
||||
* @param {Event} evt 事件句柄
|
||||
* @param {Group} currentGroup 当前操作的群组
|
||||
* @param {Item} keyShape 当前操作的keyShape
|
||||
* @description 节点拖入拖出后动态改变群组大小
|
||||
*/
|
||||
dynamicChangeGroupSize(evt, currentGroup, keyShape) {
|
||||
const { item } = evt;
|
||||
|
||||
const model = item.getModel();
|
||||
// 节点所在的GroupId
|
||||
const { groupId } = model;
|
||||
|
||||
const graph = this.graph;
|
||||
const customGroupControll = graph.get('customGroupControll');
|
||||
const groupNodes = graph.get('groupNodes');
|
||||
|
||||
// 拖出节点后,根据最新的节点数量,重新计算群组大小
|
||||
// 如果只有一个节点,拖出后,则删除该组
|
||||
if (groupNodes[groupId].length === 0) {
|
||||
// step 1: 从groupNodes中删除
|
||||
delete groupNodes[groupId];
|
||||
|
||||
// step 2: 从groups数据中删除
|
||||
const groupsData = graph.get('groups');
|
||||
graph.set('groups', groupsData.filter(gdata => gdata.id !== groupId));
|
||||
|
||||
// step 3: 删除原来的群组
|
||||
currentGroup.remove();
|
||||
} else {
|
||||
const { x, y, width, height } = customGroupControll.calculationGroupPosition(groupNodes[groupId]);
|
||||
// 检测操作的群组中是否包括子群组
|
||||
const groups = graph.get('groups');
|
||||
const hasSubGroup = !!groups.filter(g => g.parentId === groupId).length > 0;
|
||||
const r = width > height ? width / 2 : height / 2 + (hasSubGroup ? 20 : 0);
|
||||
const cx = (width + 2 * x) / 2;
|
||||
const cy = (height + 2 * y) / 2;
|
||||
keyShape.attr({
|
||||
r: r + groupNodes[groupId].length * 10,
|
||||
x: cx,
|
||||
y: cy
|
||||
});
|
||||
|
||||
customGroupControll.setGroupOriginBBox(groupId, keyShape.getBBox());
|
||||
}
|
||||
customGroupControll.setGroupStyle(keyShape, 'default');
|
||||
},
|
||||
setCurrentGroupStyle(evt) {
|
||||
const { item } = evt;
|
||||
const graph = this.graph;
|
||||
const autoPaint = graph.get('autoPaint');
|
||||
graph.setAutoPaint(false);
|
||||
|
||||
const model = item.getModel();
|
||||
// 节点所在的GroupId
|
||||
const { groupId, id } = model;
|
||||
|
||||
// console.log(groupId, this.inGroupId)
|
||||
const customGroupControll = graph.get('customGroupControll');
|
||||
const customGroup = customGroupControll.customGroup;
|
||||
const groupNodes = graph.get('groupNodes');
|
||||
if (this.inGroupId && groupId) {
|
||||
const currentGroup = customGroup[groupId].nodeGroup;
|
||||
const keyShape = currentGroup.get('keyShape');
|
||||
|
||||
const itemBBox = item.getBBox();
|
||||
const currentGroupBBox = keyShape.getBBox();
|
||||
|
||||
const { x, y } = itemBBox;
|
||||
const { minX, minY, maxX, maxY } = currentGroupBBox;
|
||||
|
||||
// 在自己的group中拖动,判断是否拖出了自己的group
|
||||
if (!(x < maxX * this.maxMultiple && x > minX * this.minMultiple && y < maxY * this.maxMultiple && y > minY * this.minMultiple)) {
|
||||
// 拖出了group,则删除item中的groupId字段,同时删除group中的nodeID
|
||||
const currentGroupNodes = groupNodes[groupId];
|
||||
groupNodes[groupId] = currentGroupNodes.filter(node => node !== id);
|
||||
|
||||
this.dynamicChangeGroupSize(evt, currentGroup, keyShape);
|
||||
|
||||
// 同时删除groupID中的节点
|
||||
delete model.groupId;
|
||||
}
|
||||
// 拖动到其他的group上面
|
||||
if (this.inGroupId !== groupId) {
|
||||
const nodeInGroup = customGroup[this.inGroupId].nodeGroup;
|
||||
const keyShape = nodeInGroup.get('keyShape');
|
||||
// 将该节点添加到inGroupId中
|
||||
if (groupNodes[this.inGroupId].indexOf(id) === -1) {
|
||||
groupNodes[this.inGroupId].push(id);
|
||||
}
|
||||
// 更新节点的groupId为拖动上去的group Id
|
||||
model.groupId = this.inGroupId;
|
||||
|
||||
// 拖入节点后,根据最新的节点数量,重新计算群组大小
|
||||
this.dynamicChangeGroupSize(evt, nodeInGroup, keyShape);
|
||||
}
|
||||
customGroupControll.setGroupStyle(keyShape, 'default');
|
||||
} else if (this.inGroupId && !groupId) {
|
||||
// 将节点拖动到群组中
|
||||
const nodeInGroup = customGroup[this.inGroupId].nodeGroup;
|
||||
const keyShape = nodeInGroup.get('keyShape');
|
||||
// 将该节点添加到inGroupId中
|
||||
if (groupNodes[this.inGroupId].indexOf(id) === -1) {
|
||||
groupNodes[this.inGroupId].push(id);
|
||||
}
|
||||
// 更新节点的groupId为拖动上去的group Id
|
||||
model.groupId = this.inGroupId;
|
||||
// 拖入节点后,根据最新的节点数量,重新计算群组大小
|
||||
this.dynamicChangeGroupSize(evt, nodeInGroup, keyShape);
|
||||
} else if (!this.inGroupId && groupId) {
|
||||
// 拖出到群组之外了,则删除数据中的groupId
|
||||
for (const gnode in groupNodes) {
|
||||
const currentGroupNodes = groupNodes[gnode];
|
||||
groupNodes[gnode] = currentGroupNodes.filter(node => node !== id);
|
||||
}
|
||||
const currentGroup = customGroup[groupId].nodeGroup;
|
||||
const keyShape = currentGroup.get('keyShape');
|
||||
this.dynamicChangeGroupSize(evt, currentGroup, keyShape);
|
||||
delete model.groupId;
|
||||
}
|
||||
|
||||
this.inGroupId = null;
|
||||
|
||||
graph.paint();
|
||||
graph.setAutoPaint(autoPaint);
|
||||
},
|
||||
// 若在拖拽时,鼠标移出画布区域,此时放开鼠标无法终止 drag 行为。在画布外监听 mouseup 事件,放开则终止
|
||||
onOutOfRange(e) {
|
||||
const self = this;
|
||||
if (this.origin) {
|
||||
const canvasElement = self.graph.get('canvas').get('el');
|
||||
const fn = ev => {
|
||||
if (ev.target !== canvasElement) {
|
||||
self.onDragEnd(e);
|
||||
}
|
||||
};
|
||||
this.fn = fn;
|
||||
body.addEventListener('mouseup', fn, false);
|
||||
}
|
||||
},
|
||||
_update(item, e, force) {
|
||||
const origin = this.origin;
|
||||
const model = item.get('model');
|
||||
const nodeId = item.get('id');
|
||||
if (!this.point[nodeId]) {
|
||||
this.point[nodeId] = {
|
||||
x: model.x,
|
||||
y: model.y
|
||||
};
|
||||
}
|
||||
|
||||
const x = e.x - origin.x + this.point[nodeId].x;
|
||||
const y = e.y - origin.y + this.point[nodeId].y;
|
||||
|
||||
// 拖动单个未选中元素
|
||||
if (force) {
|
||||
this._updateDelegate(e, x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
const pos = { x, y };
|
||||
|
||||
if (this.get('updateEdge')) {
|
||||
this.graph.updateItem(item, pos);
|
||||
} else {
|
||||
item.updatePosition(pos);
|
||||
this.graph.paint();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 更新拖动元素时的delegate
|
||||
* @param {Event} e 事件句柄
|
||||
* @param {number} x 拖动单个元素时候的x坐标
|
||||
* @param {number} y 拖动单个元素时候的y坐标
|
||||
*/
|
||||
_updateDelegate(e, x, y) {
|
||||
const { item } = e;
|
||||
const graph = this.graph;
|
||||
const bbox = item.get('keyShape').getBBox();
|
||||
if (!this.shape) {
|
||||
// 拖动多个
|
||||
const parent = graph.get('group');
|
||||
const attrs = merge({}, delegateStyle, this.delegateStyle);
|
||||
if (this.targets.length > 0) {
|
||||
const nodes = graph.findAllByState('node', 'selected');
|
||||
if (nodes.length === 0) {
|
||||
nodes.push(item);
|
||||
}
|
||||
const customGroupControll = graph.get('customGroupControll');
|
||||
const { x, y, width, height, minX, minY } = customGroupControll.calculationGroupPosition(nodes);
|
||||
this.originPoint = { x, y, width, height, minX, minY };
|
||||
// model上的x, y是相对于图形中心的,delegateShape是g实例,x,y是绝对坐标
|
||||
this.shape = parent.addShape('rect', {
|
||||
attrs: {
|
||||
width,
|
||||
height,
|
||||
x,
|
||||
y,
|
||||
...attrs
|
||||
}
|
||||
});
|
||||
} else if (this.target) {
|
||||
this.shape = parent.addShape('rect', {
|
||||
attrs: {
|
||||
width: bbox.width,
|
||||
height: bbox.height,
|
||||
x: x - bbox.width / 2,
|
||||
y: y - bbox.height / 2,
|
||||
...attrs
|
||||
}
|
||||
});
|
||||
this.target.set('delegateShape', this.shape);
|
||||
}
|
||||
this.shape.set('capture', false);
|
||||
}
|
||||
|
||||
if (this.targets.length > 0) {
|
||||
const clientX = e.x - this.origin.x + this.originPoint.minX;
|
||||
const clientY = e.y - this.origin.y + this.originPoint.minY;
|
||||
this.shape.attr({
|
||||
x: clientX,
|
||||
y: clientY
|
||||
});
|
||||
} else if (this.target) {
|
||||
this.shape.attr({
|
||||
x: x - bbox.width / 2,
|
||||
y: y - bbox.height / 2
|
||||
});
|
||||
}
|
||||
this.graph.paint();
|
||||
}
|
||||
};
|
@ -1,4 +1,10 @@
|
||||
const { mix } = require('../util');
|
||||
/*
|
||||
* @Author: moyee
|
||||
* @Date: 2019-06-27 18:12:06
|
||||
* @LastEditors: moyee
|
||||
* @LastEditTime: 2019-08-22 18:41:45
|
||||
* @Description: 拖动节点的Behavior
|
||||
*/
|
||||
const { merge, isString } = require('lodash');
|
||||
const { delegateStyle } = require('../global');
|
||||
const body = document.body;
|
||||
@ -7,7 +13,6 @@ module.exports = {
|
||||
getDefaultCfg() {
|
||||
return {
|
||||
updateEdge: true,
|
||||
delegate: true,
|
||||
delegateStyle: {}
|
||||
};
|
||||
},
|
||||
@ -158,29 +163,6 @@ module.exports = {
|
||||
this.graph.paint();
|
||||
}
|
||||
},
|
||||
_updateDelegate1(item, x, y) {
|
||||
const self = this;
|
||||
let shape = item.get('delegateShape');
|
||||
const bbox = item.get('keyShape').getBBox();
|
||||
if (!shape) {
|
||||
const parent = self.graph.get('group');
|
||||
const attrs = mix({}, delegateStyle, this.delegateStyle);
|
||||
// model上的x, y是相对于图形中心的,delegateShape是g实例,x,y是绝对坐标
|
||||
shape = parent.addShape('rect', {
|
||||
attrs: {
|
||||
width: bbox.width,
|
||||
height: bbox.height,
|
||||
x: x - bbox.width / 2,
|
||||
y: y - bbox.height / 2,
|
||||
...attrs
|
||||
}
|
||||
});
|
||||
shape.set('capture', false);
|
||||
item.set('delegateShape', shape);
|
||||
}
|
||||
shape.attr({ x: x - bbox.width / 2, y: y - bbox.height / 2 });
|
||||
this.graph.paint();
|
||||
},
|
||||
/**
|
||||
* 更新拖动元素时的delegate
|
||||
* @param {Event} e 事件句柄
|
||||
|
@ -1,3 +1,10 @@
|
||||
/*
|
||||
* @Author: moyee
|
||||
* @Date: 2019-06-27 18:12:06
|
||||
* @LastEditors: moyee
|
||||
* @LastEditTime: 2019-08-22 14:50:44
|
||||
* @Description: file content
|
||||
*/
|
||||
const Util = require('../util');
|
||||
const Behavior = require('./behavior');
|
||||
const behaviors = {
|
||||
@ -9,7 +16,10 @@ const behaviors = {
|
||||
'edge-tooltip': require('./edge-tooltip'),
|
||||
'collapse-expand': require('./collapse-expand'),
|
||||
'activate-relations': require('./activate-relations'),
|
||||
'brush-select': require('./brush-select')
|
||||
'brush-select': require('./brush-select'),
|
||||
'drag-group': require('./drag-group'),
|
||||
'drag-node-with-group': require('./drag-node-with-group'),
|
||||
'collapse-expand-group': require('./collapse-expand-group')
|
||||
};
|
||||
Util.each(behaviors, (behavior, type) => {
|
||||
Behavior.registerBehavior(type, behavior);
|
||||
|
@ -7,6 +7,8 @@ module.exports = {
|
||||
rootContainerClassName: 'root-container',
|
||||
nodeContainerClassName: 'node-container',
|
||||
edgeContainerClassName: 'edge-container',
|
||||
customGroupContainerClassName: 'custom-group-container',
|
||||
delegateContainerClassName: 'delegate-container',
|
||||
defaultNode: {
|
||||
shape: 'circle',
|
||||
style: {
|
||||
|
733
src/graph/controller/customGroup.js
Normal file
733
src/graph/controller/customGroup.js
Normal file
@ -0,0 +1,733 @@
|
||||
/*
|
||||
* @Author: moyee
|
||||
* @Date: 2019-07-30 12:10:26
|
||||
* @LastEditors: moyee
|
||||
* @LastEditTime: 2019-08-23 11:44:32
|
||||
* @Description: Group Controller
|
||||
*/
|
||||
const { merge, isString } = require('lodash');
|
||||
|
||||
class CustomGroup {
|
||||
getDefaultCfg() {
|
||||
return {
|
||||
default: {
|
||||
lineWidth: 1,
|
||||
stroke: '#A3B1BF',
|
||||
radius: 10,
|
||||
lineDash: [ 5, 5 ],
|
||||
strokeOpacity: 0.9,
|
||||
fill: '#F3F9FF',
|
||||
fillOpacity: 0.8,
|
||||
opacity: 0.8
|
||||
},
|
||||
hover: {
|
||||
stroke: '#faad14',
|
||||
fill: '#ffe58f',
|
||||
fillOpacity: 0.3,
|
||||
opacity: 0.3,
|
||||
lineWidth: 3
|
||||
},
|
||||
// 收起状态样式
|
||||
collapseStyle: {
|
||||
r: 30,
|
||||
lineDash: [ 5, 5 ],
|
||||
stroke: '#ffa39e',
|
||||
lineWidth: 3,
|
||||
fill: '#ffccc7'
|
||||
},
|
||||
icon: 'https://gw.alipayobjects.com/zos/rmsportal/MXXetJAxlqrbisIuZxDO.svg',
|
||||
text: {
|
||||
text: '新建群组',
|
||||
stroke: '#444'
|
||||
},
|
||||
operatorBtn: {
|
||||
collapse: {
|
||||
img: 'https://gw.alipayobjects.com/zos/rmsportal/uZVdwjJGqDooqKLKtvGA.svg',
|
||||
width: 16,
|
||||
height: 16
|
||||
},
|
||||
expand: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
img: 'https://gw.alipayobjects.com/zos/rmsportal/MXXetJAxlqrbisIuZxDO.svg'
|
||||
}
|
||||
},
|
||||
visible: false
|
||||
};
|
||||
}
|
||||
|
||||
constructor(graph) {
|
||||
// const { cfg = {} } = options;
|
||||
this.graph = graph;
|
||||
window.graph = graph;
|
||||
this.styles = this.getDefaultCfg();
|
||||
// 创建的群组集合
|
||||
this.customGroup = {};
|
||||
// 群组初始位置集合
|
||||
this.groupOriginBBox = {};
|
||||
this.delegateInGroup = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成群组
|
||||
* @param {string} groupId 群组ID
|
||||
* @param {array} nodes 群组中的节点集合
|
||||
* @param {string} type 群组类型,默认为circle,支持rect
|
||||
* @param {number} zIndex 群组层级,默认为0
|
||||
* @memberof ItemGroup
|
||||
*/
|
||||
create(groupId, nodes, type = 'circle', zIndex = 0) {
|
||||
const graph = this.graph;
|
||||
const customGroup = graph.get('customGroup');
|
||||
const nodeGroup = customGroup.addGroup({
|
||||
id: groupId,
|
||||
zIndex
|
||||
});
|
||||
|
||||
const autoPaint = graph.get('autoPaint');
|
||||
graph.setAutoPaint(false);
|
||||
|
||||
const { default: defaultStyle } = this.styles;
|
||||
|
||||
// 计算群组左上角左边、宽度、高度及x轴方向上的最大值
|
||||
const { x, y, width, height, maxX } = this.calculationGroupPosition(nodes);
|
||||
|
||||
const groupBBox = graph.get('groupBBoxs');
|
||||
groupBBox[groupId] = { x, y, width, height, maxX };
|
||||
|
||||
// step 1:绘制群组外框
|
||||
let keyShape = null;
|
||||
|
||||
if (type === 'circle') {
|
||||
const r = width > height ? width / 2 : height / 2;
|
||||
const cx = (width + 2 * x) / 2;
|
||||
const cy = (height + 2 * y) / 2;
|
||||
keyShape = nodeGroup.addShape('circle', {
|
||||
attrs: {
|
||||
...defaultStyle,
|
||||
x: cx,
|
||||
y: cy,
|
||||
r: r + nodes.length * 10
|
||||
},
|
||||
capture: true,
|
||||
zIndex,
|
||||
groupId
|
||||
});
|
||||
// 更新群组及属性样式
|
||||
this.setDeletageGroupByStyle(groupId, nodeGroup,
|
||||
{ width, height, x: cx, y: cy, r });
|
||||
} else {
|
||||
keyShape = nodeGroup.addShape('rect', {
|
||||
attrs: {
|
||||
...defaultStyle,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height
|
||||
},
|
||||
capture: true,
|
||||
zIndex,
|
||||
groupId
|
||||
});
|
||||
// 更新群组及属性样式
|
||||
this.setDeletageGroupByStyle(groupId, nodeGroup,
|
||||
{ width, height, x, y, btnOffset: maxX - 3 });
|
||||
}
|
||||
nodeGroup.set('keyShape', keyShape);
|
||||
|
||||
this.setGroupOriginBBox(groupId, keyShape.getBBox());
|
||||
|
||||
graph.setAutoPaint(autoPaint);
|
||||
graph.paint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改Group样式
|
||||
* @param {Item} keyShape 群组的keyShape
|
||||
* @param {Object | String} style 样式
|
||||
*/
|
||||
setGroupStyle(keyShape, style) {
|
||||
if (!keyShape || keyShape.get('destroyed')) {
|
||||
return;
|
||||
}
|
||||
let styles = {};
|
||||
const { hover: hoverStyle, default: defaultStyle } = this.styles;
|
||||
if (isString(style)) {
|
||||
if (style === 'default') {
|
||||
styles = merge({}, defaultStyle);
|
||||
} else if (style === 'hover') {
|
||||
styles = merge({}, hoverStyle);
|
||||
}
|
||||
} else {
|
||||
styles = merge({}, defaultStyle, style);
|
||||
}
|
||||
for (const s in styles) {
|
||||
keyShape.attr(s, styles[s]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据GroupID计算群组位置,包括左上角左边及宽度和高度
|
||||
*
|
||||
* @param {object} nodes 符合条件的node集合:选中的node或具有同一个groupID的node
|
||||
* @return {object} 根据节点计算出来的包围盒坐标
|
||||
* @memberof ItemGroup
|
||||
*/
|
||||
calculationGroupPosition(nodes) {
|
||||
const graph = this.graph;
|
||||
|
||||
const minx = [];
|
||||
const maxx = [];
|
||||
const miny = [];
|
||||
const maxy = [];
|
||||
|
||||
// 获取已节点的所有最大最小x y值
|
||||
for (const id of nodes) {
|
||||
const element = isString(id) ? graph.findById(id) : id;
|
||||
const bbox = element.getBBox();
|
||||
const { minX, minY, maxX, maxY } = bbox;
|
||||
minx.push(minX);
|
||||
miny.push(minY);
|
||||
maxx.push(maxX);
|
||||
maxy.push(maxY);
|
||||
}
|
||||
|
||||
// 从上一步获取的数组中,筛选出最小和最大值
|
||||
const minX = Math.floor(Math.min(...minx));
|
||||
const maxX = Math.floor(Math.max(...maxx));
|
||||
const minY = Math.floor(Math.min(...miny));
|
||||
const maxY = Math.floor(Math.max(...maxy));
|
||||
|
||||
// const x = minX - 20;
|
||||
// const y = minY - 30;
|
||||
// const width = maxX - minX + 40;
|
||||
// const height = maxY - minY + 40;
|
||||
const x = minX;
|
||||
const y = minY;
|
||||
const width = maxX - minX;
|
||||
const height = maxY - minY;
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
maxX
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 拖动群组里面的节点,更新群组属性样式
|
||||
*
|
||||
* @param {string} groupId 群组ID
|
||||
* @return {boolean} null
|
||||
* @memberof ItemGroup
|
||||
*/
|
||||
updateGroupStyleByNode(groupId) {
|
||||
const graph = this.graph;
|
||||
const customGroup = graph.get('customGroup');
|
||||
const groupChild = customGroup.get('children');
|
||||
const currentGroup = groupChild.filter(child => child.get('id') === groupId);
|
||||
|
||||
const autoPaint = graph.get('autoPaint');
|
||||
graph.setAutoPaint(false);
|
||||
|
||||
// 获取所有具有同一个groupID的节点,计算群组大小
|
||||
const nodes = graph.getNodes();
|
||||
const groupNodes = nodes.filter(node => {
|
||||
const currentModel = node.getModel();
|
||||
const gId = currentModel.groupId;
|
||||
return gId === groupId;
|
||||
});
|
||||
|
||||
const { x, y, width, height, maxX } = this.calculationGroupPosition(groupNodes);
|
||||
|
||||
const current = currentGroup[0];
|
||||
if (!current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新rect属性样式
|
||||
const rect = current.getChildByIndex(0);
|
||||
rect.attr('x', x);
|
||||
rect.attr('y', y);
|
||||
rect.attr('width', width);
|
||||
rect.attr('height', height);
|
||||
|
||||
// 更新群组图标属性样式
|
||||
const logoIcon = current.getChildByIndex(1);
|
||||
logoIcon.attr('x', x + 8);
|
||||
logoIcon.attr('y', y + 8);
|
||||
|
||||
// 更新群组名称属性样式
|
||||
const text = current.getChildByIndex(2);
|
||||
text.attr('x', x + 35);
|
||||
text.attr('y', y + 21);
|
||||
|
||||
// 更新收起和展开按钮属性样式
|
||||
const operatorBtn = current.getChildByIndex(3);
|
||||
operatorBtn.attr('x', maxX - 3);
|
||||
operatorBtn.attr('y', y + 8);
|
||||
|
||||
// 更新群组及属性样式
|
||||
this.setDeletageGroupByStyle(groupId, current, { width, height, btnOffset: maxX - 3 });
|
||||
|
||||
// 更新群组初始位置的bbox
|
||||
this.setGroupOriginBBox(groupId, rect.getBBox());
|
||||
|
||||
graph.setAutoPaint(autoPaint);
|
||||
graph.paint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置群组初始位置的bbox,使用rect模拟
|
||||
*
|
||||
* @param {string} groupId 群组ID
|
||||
* @param {object} bbox 群组keyShape包围盒
|
||||
* @memberof ItemGroup
|
||||
*/
|
||||
setGroupOriginBBox(groupId, bbox) {
|
||||
this.groupOriginBBox[groupId] = bbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取群组初始位置及每次拖动后的位置
|
||||
*
|
||||
* @param {string} groupId 群组ID
|
||||
* @return {object} 指定groupId的原始BBox
|
||||
* @memberof ItemGroup
|
||||
*/
|
||||
getGroupOriginBBox(groupId) {
|
||||
return this.groupOriginBBox[groupId];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置群组对象及属性值
|
||||
*
|
||||
* @param {string} groupId 群组ID
|
||||
* @param {Group} deletage 群组元素
|
||||
* @param {object} property 属性值,里面包括width、height和maxX
|
||||
* @memberof ItemGroup
|
||||
*/
|
||||
setDeletageGroupByStyle(groupId, deletage, property) {
|
||||
const { width, height, x, y, r, btnOffset } = property;
|
||||
const customGroupStyle = this.customGroup[groupId];
|
||||
if (!customGroupStyle) {
|
||||
// 首次赋值
|
||||
this.customGroup[groupId] = {
|
||||
nodeGroup: deletage,
|
||||
groupStyle: {
|
||||
width,
|
||||
height,
|
||||
x,
|
||||
y,
|
||||
r,
|
||||
btnOffset
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// 更新时候merge配置项
|
||||
const { groupStyle } = customGroupStyle;
|
||||
const styles = merge({}, groupStyle, property);
|
||||
this.customGroup[groupId] = {
|
||||
nodeGroup: deletage,
|
||||
groupStyle: styles
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据群组ID获取群组及属性对象
|
||||
*
|
||||
* @param {string} groupId 群组ID
|
||||
* @return {Item} 群组
|
||||
* @memberof ItemGroup
|
||||
*/
|
||||
getDeletageGroupById(groupId) {
|
||||
return this.customGroup[groupId];
|
||||
}
|
||||
|
||||
/**
|
||||
* 收起和展开群组
|
||||
* @param {string} groupId 群组ID
|
||||
*/
|
||||
collapseExpandGroup(groupId) {
|
||||
const customGroup = this.getDeletageGroupById(groupId);
|
||||
const { nodeGroup } = customGroup;
|
||||
|
||||
const hasHidden = nodeGroup.get('hasHidden');
|
||||
// 该群组已经处于收起状态,需要展开
|
||||
if (hasHidden) {
|
||||
nodeGroup.set('hasHidden', false);
|
||||
this.expandGroup(groupId);
|
||||
} else {
|
||||
nodeGroup.set('hasHidden', true);
|
||||
this.collapseGroup(groupId);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 将临时节点递归地设置到groupId及父节点上
|
||||
* @param {string} groupId 群组ID
|
||||
* @param {string} tmpNodeId 临时节点ID
|
||||
*/
|
||||
setGroupTmpNode(groupId, tmpNodeId) {
|
||||
const graph = this.graph;
|
||||
const graphNodes = graph.get('groupNodes');
|
||||
const groups = graph.get('groups');
|
||||
if (graphNodes[groupId].indexOf(tmpNodeId) < 0) {
|
||||
graphNodes[groupId].push(tmpNodeId);
|
||||
}
|
||||
// 获取groupId的父群组
|
||||
const parentGroup = groups.filter(g => g.id === groupId);
|
||||
let parentId = null;
|
||||
if (parentGroup.length > 0) {
|
||||
parentId = parentGroup[0].parentId;
|
||||
}
|
||||
|
||||
// 如果存在父群组,则把临时元素也添加到父群组中
|
||||
if (parentId) {
|
||||
this.setGroupTmpNode(parentId, tmpNodeId);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 收起群组,隐藏群组中的节点及边,群组外部相邻的边都连接到群组上
|
||||
*
|
||||
* @param {string} id 群组ID
|
||||
* @memberof ItemGroup
|
||||
*/
|
||||
collapseGroup(id) {
|
||||
const self = this;
|
||||
const customGroup = this.getDeletageGroupById(id);
|
||||
const { nodeGroup, groupStyle } = customGroup;
|
||||
|
||||
// 收起群组后的默认样式
|
||||
const { collapseStyle } = this.styles;
|
||||
const graph = this.graph;
|
||||
const autoPaint = graph.get('autoPaint');
|
||||
graph.setAutoPaint(false);
|
||||
|
||||
const nodesInGroup = graph.get('groupNodes')[id];
|
||||
|
||||
// 更新Group的大小
|
||||
const keyShape = nodeGroup.get('keyShape');
|
||||
const { r, ...otherStyle } = collapseStyle;
|
||||
for (const style in otherStyle) {
|
||||
keyShape.attr(style, otherStyle[style]);
|
||||
}
|
||||
|
||||
// 收起群组时候动画
|
||||
keyShape.animate({
|
||||
onFrame(ratio) {
|
||||
if (ratio === 1) {
|
||||
self.setGroupOriginBBox(id, keyShape.getBBox());
|
||||
}
|
||||
return {
|
||||
r: groupStyle.r - ratio * (groupStyle.r - r)
|
||||
};
|
||||
}
|
||||
}, 1000, 'easeCubic');
|
||||
|
||||
const edges = graph.getEdges();
|
||||
// 获取所有source在群组外,target在群组内的边
|
||||
const sourceOutTargetInEdges = edges.filter(edge => {
|
||||
const model = edge.getModel();
|
||||
return !nodesInGroup.includes(model.source) && nodesInGroup.includes(model.target);
|
||||
});
|
||||
|
||||
// 获取所有source在群组外,target在群组内的边
|
||||
const sourceInTargetOutEdges = edges.filter(edge => {
|
||||
const model = edge.getModel();
|
||||
return nodesInGroup.includes(model.source) && !nodesInGroup.includes(model.target);
|
||||
});
|
||||
|
||||
// 群组中存在source和target其中有一个在群组内,一个在群组外的情况
|
||||
if (sourceOutTargetInEdges.length > 0 || sourceInTargetOutEdges.length > 0) {
|
||||
const options = {
|
||||
groupId: id,
|
||||
id: `${id}-custom-node`,
|
||||
x: keyShape.attr('x'),
|
||||
y: keyShape.attr('y'),
|
||||
style: {
|
||||
r: 30
|
||||
},
|
||||
shape: 'circle'
|
||||
};
|
||||
const delegateNode = graph.add('node', options);
|
||||
delegateNode.set('capture', false);
|
||||
delegateNode.hide();
|
||||
this.delegateInGroup[id] = {
|
||||
delegateNode
|
||||
};
|
||||
|
||||
// 将临时添加的节点加入到群组中,以便拖动节点时候线跟着拖动
|
||||
// nodesInGroup.push(`${id}-custom-node`);
|
||||
this.setGroupTmpNode(id, `${id}-custom-node`);
|
||||
|
||||
this.updateEdgeInGroupLinks(id, sourceOutTargetInEdges, sourceInTargetOutEdges);
|
||||
}
|
||||
|
||||
// 获取群组中节点之间的所有边
|
||||
const edgeAllInGroup = edges.filter(edge => {
|
||||
const model = edge.getModel();
|
||||
return nodesInGroup.includes(model.source) && nodesInGroup.includes(model.target);
|
||||
});
|
||||
|
||||
// 隐藏群组中的所有节点
|
||||
nodesInGroup.forEach(nodeId => {
|
||||
const node = graph.findById(nodeId);
|
||||
const model = node.getModel();
|
||||
const { groupId } = model;
|
||||
if (groupId && groupId !== id) {
|
||||
// 存在群组,则隐藏
|
||||
const currentGroup = this.getDeletageGroupById(groupId);
|
||||
const { nodeGroup } = currentGroup;
|
||||
nodeGroup.hide();
|
||||
}
|
||||
node.hide();
|
||||
});
|
||||
|
||||
edgeAllInGroup.forEach(edge => {
|
||||
const source = edge.getSource();
|
||||
const target = edge.getTarget();
|
||||
if (source.isVisible() && target.isVisible()) {
|
||||
edge.show();
|
||||
} else {
|
||||
edge.hide();
|
||||
}
|
||||
});
|
||||
|
||||
graph.paint();
|
||||
graph.setAutoPaint(autoPaint);
|
||||
}
|
||||
|
||||
/**
|
||||
* 收起群组时生成临时的节点,用于连接群组外的节点
|
||||
*
|
||||
* @param {string} groupId 群组ID
|
||||
* @param {array} sourceOutTargetInEdges 出度的边
|
||||
* @param {array} sourceInTargetOutEdges 入度的边
|
||||
* @memberof ItemGroup
|
||||
*/
|
||||
updateEdgeInGroupLinks(groupId, sourceOutTargetInEdges, sourceInTargetOutEdges) {
|
||||
const graph = this.graph;
|
||||
|
||||
// 更新source在外的节点
|
||||
const edgesOuts = {};
|
||||
sourceOutTargetInEdges.map(edge => {
|
||||
const model = edge.getModel();
|
||||
const { id, target } = model;
|
||||
edgesOuts[id] = target;
|
||||
graph.updateItem(edge, {
|
||||
target: `${groupId}-custom-node`
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
// 更新target在外的节点
|
||||
const edgesIn = {};
|
||||
sourceInTargetOutEdges.map(edge => {
|
||||
const model = edge.getModel();
|
||||
const { id, source } = model;
|
||||
edgesIn[id] = source;
|
||||
graph.updateItem(edge, {
|
||||
source: `${groupId}-custom-node`
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
// 缓存群组groupId下的edge和临时生成的node节点
|
||||
this.delegateInGroup[groupId] = merge({
|
||||
sourceOutTargetInEdges,
|
||||
sourceInTargetOutEdges,
|
||||
edgesOuts,
|
||||
edgesIn
|
||||
}, this.delegateInGroup[groupId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开群组,恢复群组中的节点及边
|
||||
*
|
||||
* @param {string} id 群组ID
|
||||
* @memberof ItemGroup
|
||||
*/
|
||||
expandGroup(id) {
|
||||
const graph = this.graph;
|
||||
const self = this;
|
||||
const autoPaint = graph.get('autoPaint');
|
||||
graph.setAutoPaint(false);
|
||||
|
||||
// 显示之前隐藏的节点和群组
|
||||
const nodesInGroup = graph.get('groupNodes')[id];
|
||||
const { nodeGroup } = this.getDeletageGroupById(id);
|
||||
const { width, height } = this.calculationGroupPosition(nodesInGroup);
|
||||
// 检测操作的群组中是否包括子群组
|
||||
const groups = graph.get('groups');
|
||||
const hasSubGroup = !!groups.filter(g => g.parentId === id).length > 0;
|
||||
const r = width > height ? width / 2 : height / 2 + (hasSubGroup ? 20 : 0);
|
||||
|
||||
// const cx = (width + 2 * x) / 2;
|
||||
// const cy = (height + 2 * y) / 2;
|
||||
const keyShape = nodeGroup.get('keyShape');
|
||||
|
||||
const { default: defaultStyle } = this.styles;
|
||||
|
||||
// const styles = merge({}, defaultStyle, { x: cx, y: cy });
|
||||
for (const style in defaultStyle) {
|
||||
keyShape.attr(style, defaultStyle[style]);
|
||||
}
|
||||
// keyShape.attr('r', groupStyle.r + nodesInGroup.length * 10);
|
||||
keyShape.animate({
|
||||
onFrame(ratio) {
|
||||
if (ratio === 1) {
|
||||
self.setGroupOriginBBox(id, keyShape.getBBox());
|
||||
}
|
||||
return {
|
||||
r: 30 + ratio * (r + nodesInGroup.length * 10 - 30)
|
||||
};
|
||||
}
|
||||
}, 1000, 'easeCubic');
|
||||
|
||||
// this.setGroupOriginBBox(id, keyShape.getBBox());
|
||||
// 群组动画一会后再显示节点和边
|
||||
setTimeout(() => {
|
||||
nodesInGroup.forEach(nodeId => {
|
||||
const node = graph.findById(nodeId);
|
||||
const model = node.getModel();
|
||||
const { groupId } = model;
|
||||
if (groupId && groupId !== id) {
|
||||
// 存在群组,则显示
|
||||
const currentGroup = this.getDeletageGroupById(groupId);
|
||||
const { nodeGroup } = currentGroup;
|
||||
nodeGroup.show();
|
||||
const hasHidden = nodeGroup.get('hasHidden');
|
||||
if (!hasHidden) {
|
||||
node.show();
|
||||
}
|
||||
} else {
|
||||
node.show();
|
||||
}
|
||||
});
|
||||
|
||||
const edges = graph.getEdges();
|
||||
// 获取群组中节点之间的所有边
|
||||
const edgeAllInGroup = edges.filter(edge => {
|
||||
const model = edge.getModel();
|
||||
return nodesInGroup.includes(model.source) || nodesInGroup.includes(model.target);
|
||||
});
|
||||
|
||||
edgeAllInGroup.forEach(edge => {
|
||||
const source = edge.getSource();
|
||||
const target = edge.getTarget();
|
||||
if (source.isVisible() && target.isVisible()) {
|
||||
edge.show();
|
||||
}
|
||||
});
|
||||
}, 800);
|
||||
|
||||
const delegates = this.delegateInGroup[id];
|
||||
if (delegates) {
|
||||
const { sourceOutTargetInEdges,
|
||||
sourceInTargetOutEdges,
|
||||
edgesOuts,
|
||||
edgesIn,
|
||||
delegateNode } = delegates;
|
||||
|
||||
// 恢复source在外的节点
|
||||
sourceOutTargetInEdges.map(edge => {
|
||||
const model = edge.getModel();
|
||||
const sourceOuts = edgesOuts[model.id];
|
||||
graph.updateItem(edge, {
|
||||
target: sourceOuts
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
// 恢复target在外的节点
|
||||
sourceInTargetOutEdges.map(edge => {
|
||||
const model = edge.getModel();
|
||||
const sourceIn = edgesIn[model.id];
|
||||
graph.updateItem(edge, {
|
||||
source: sourceIn
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
// 删除群组中的临时节点ID
|
||||
const tmpNodeModel = delegateNode.getModel();
|
||||
|
||||
this.deleteTmpNode(id, tmpNodeModel.id);
|
||||
graph.remove(delegateNode);
|
||||
delete this.delegateInGroup[id];
|
||||
}
|
||||
graph.setAutoPaint(autoPaint);
|
||||
graph.paint();
|
||||
}
|
||||
|
||||
deleteTmpNode(groupId, tmpNodeId) {
|
||||
const graph = this.graph;
|
||||
const groups = graph.get('groups');
|
||||
const nodesInGroup = graph.get('groupNodes')[groupId];
|
||||
|
||||
const index = nodesInGroup.indexOf(tmpNodeId);
|
||||
nodesInGroup.splice(index, 1);
|
||||
|
||||
// 获取groupId的父群组
|
||||
const parentGroup = groups.filter(g => g.id === groupId);
|
||||
let parentId = null;
|
||||
if (parentGroup.length > 0) {
|
||||
parentId = parentGroup[0].parentId;
|
||||
}
|
||||
|
||||
// 如果存在父群组,则把临时元素也添加到父群组中
|
||||
if (parentId) {
|
||||
this.deleteTmpNode(parentId, tmpNodeId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拆分群组
|
||||
*
|
||||
* @memberof ItemGroup
|
||||
*/
|
||||
unGroup() {
|
||||
const graph = this.graph;
|
||||
const group = graph.get('customGroup');
|
||||
const groupChild = group.get('children');
|
||||
const autoPaint = graph.get('autoPaint');
|
||||
graph.setAutoPaint(false);
|
||||
|
||||
const currentGroup = groupChild.filter(child => child.get('selected'));
|
||||
if (currentGroup.length > 0) {
|
||||
const nodes = graph.getNodes();
|
||||
for (const current of currentGroup) {
|
||||
const id = current.get('id');
|
||||
// 删除原群组中node中的groupID
|
||||
nodes.forEach(node => {
|
||||
const model = node.getModel();
|
||||
const gId = model.groupId;
|
||||
if (!gId) {
|
||||
return;
|
||||
}
|
||||
if (id === gId) {
|
||||
delete model.groupId;
|
||||
// 使用没有groupID的数据更新节点
|
||||
graph.updateItem(node, model);
|
||||
}
|
||||
});
|
||||
current.destroy();
|
||||
}
|
||||
graph.setAutoPaint(autoPaint);
|
||||
graph.paint();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.graph = null;
|
||||
this.styles = {};
|
||||
this.customGroup = {};
|
||||
this.groupOriginBBox = {};
|
||||
this.delegateInGroup = {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CustomGroup;
|
@ -3,5 +3,6 @@ module.exports = {
|
||||
Event: require('./event'),
|
||||
Mode: require('./mode'),
|
||||
Item: require('./item'),
|
||||
State: require('./state')
|
||||
State: require('./state'),
|
||||
CustomGroup: require('./customGroup')
|
||||
};
|
||||
|
@ -1,8 +1,15 @@
|
||||
/*
|
||||
* @Author: moyee
|
||||
* @Date: 2019-06-27 18:12:06
|
||||
* @LastEditors: moyee
|
||||
* @LastEditTime: 2019-08-22 11:22:16
|
||||
* @Description: file content
|
||||
*/
|
||||
/**
|
||||
* @fileOverview graph
|
||||
* @author huangtonger@aliyun.com
|
||||
*/
|
||||
|
||||
const { groupBy } = require('lodash');
|
||||
const G = require('@antv/g/lib');
|
||||
const EventEmitter = G.EventEmitter;
|
||||
const Util = require('../util');
|
||||
@ -177,7 +184,23 @@ class Graph extends EventEmitter {
|
||||
*/
|
||||
easing: 'easeLinear'
|
||||
},
|
||||
callback: null
|
||||
callback: null,
|
||||
/**
|
||||
* group类型
|
||||
*/
|
||||
groupType: 'circle',
|
||||
/**
|
||||
* 各个group的BBox
|
||||
*/
|
||||
groupBBoxs: {},
|
||||
/**
|
||||
* 每个group包含的节点,父层的包括自己的节点以及子Group的节点
|
||||
*/
|
||||
groupNodes: {},
|
||||
/**
|
||||
* 群组的原始数据
|
||||
*/
|
||||
groups: []
|
||||
};
|
||||
}
|
||||
|
||||
@ -193,7 +216,17 @@ class Graph extends EventEmitter {
|
||||
const modeController = new Controller.Mode(this);
|
||||
const itemController = new Controller.Item(this);
|
||||
const stateController = new Controller.State(this);
|
||||
this.set({ eventController, viewController, modeController, itemController, stateController });
|
||||
|
||||
// 实例化customGroup
|
||||
const customGroupControll = new Controller.CustomGroup(this);
|
||||
this.set({
|
||||
eventController,
|
||||
viewController,
|
||||
modeController,
|
||||
itemController,
|
||||
stateController,
|
||||
customGroupControll
|
||||
});
|
||||
this._initPlugins();
|
||||
}
|
||||
_initCanvas() {
|
||||
@ -222,7 +255,21 @@ class Graph extends EventEmitter {
|
||||
if (this.get('groupByTypes')) {
|
||||
const edgeGroup = group.addGroup({ id: id + '-edge', className: Global.edgeContainerClassName });
|
||||
const nodeGroup = group.addGroup({ id: id + '-node', className: Global.nodeContainerClassName });
|
||||
this.set({ nodeGroup, edgeGroup });
|
||||
|
||||
const delegateGroup = group.addGroup({
|
||||
id: id + '-delagate',
|
||||
className: Global.delegateContainerClassName
|
||||
});
|
||||
|
||||
// 用于存储自定义的群组
|
||||
const customGroup = group.addGroup({
|
||||
id: `${id}-group`,
|
||||
className: Global.customGroupContainerClassName
|
||||
});
|
||||
|
||||
customGroup.toBack();
|
||||
|
||||
this.set({ nodeGroup, edgeGroup, customGroup, delegateGroup });
|
||||
}
|
||||
this.set('group', group);
|
||||
}
|
||||
@ -445,6 +492,17 @@ class Graph extends EventEmitter {
|
||||
Util.each(data.edges, edge => {
|
||||
self.add(EDGE, edge);
|
||||
});
|
||||
|
||||
// 获取所有有groupID的node
|
||||
const nodeInGroup = data.nodes.filter(node => node.groupId);
|
||||
|
||||
// 所有node中存在groupID,则说明需要群组
|
||||
if (nodeInGroup.length > 0) {
|
||||
// 渲染群组
|
||||
const groupType = self.get('groupType');
|
||||
this.renderCustomGroup(data, groupType);
|
||||
}
|
||||
|
||||
if (self.get('fitView')) {
|
||||
self.get('viewController')._fitView();
|
||||
}
|
||||
@ -453,6 +511,48 @@ class Graph extends EventEmitter {
|
||||
self.emit('afterrender');
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据数据渲染群组
|
||||
* @param {object} data 渲染图的数据
|
||||
* @param {string} groupType group类型
|
||||
*/
|
||||
renderCustomGroup(data, groupType) {
|
||||
const { groups, nodes } = data;
|
||||
|
||||
// 第一种情况,,不存在groups,则不存在嵌套群组
|
||||
let groupIndex = 10;
|
||||
if (!groups) {
|
||||
// 存在单个群组
|
||||
// 获取所有有groupID的node
|
||||
const nodeInGroup = nodes.filter(node => node.groupId);
|
||||
|
||||
// 根据groupID分组
|
||||
const groupIds = groupBy(nodeInGroup, 'groupId');
|
||||
for (const groupId in groupIds) {
|
||||
const nodeIds = groupIds[groupId].map(node => node.id);
|
||||
this.get('groupNodes')[groupId] = nodeIds;
|
||||
this.get('customGroupControll').create(groupId, nodeIds, groupType, groupIndex);
|
||||
groupIndex--;
|
||||
}
|
||||
} else {
|
||||
// 将groups的数据存到groups中
|
||||
this.set({ groups });
|
||||
|
||||
// 第二种情况,存在嵌套的群组,数据中有groups字段
|
||||
const groupNodes = Util.getAllNodeInGroups(data);
|
||||
for (const groupId in groupNodes) {
|
||||
const tmpNodes = groupNodes[groupId];
|
||||
this.get('groupNodes')[groupId] = tmpNodes;
|
||||
this.get('customGroupControll').create(groupId, tmpNodes, groupType, groupIndex);
|
||||
groupIndex--;
|
||||
}
|
||||
|
||||
// 对所有Group排序
|
||||
const customGroup = this.get('customGroup');
|
||||
customGroup.sort();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收数据进行渲染
|
||||
* @Param {Object} data 初始化数据
|
||||
@ -554,7 +654,7 @@ class Graph extends EventEmitter {
|
||||
Util.each(this.get('edges'), edge => {
|
||||
edges.push(edge.getModel());
|
||||
});
|
||||
return { nodes, edges };
|
||||
return { nodes, edges, groups: this.get('groups') };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1006,7 +1106,8 @@ class Graph extends EventEmitter {
|
||||
const canvas = this.get('canvas');
|
||||
canvas.clear();
|
||||
this._initGroups();
|
||||
this.set({ itemMap: {}, nodes: [], edges: [] });
|
||||
// 清空画布时同时清除数据
|
||||
this.set({ itemMap: {}, nodes: [], edges: [], groups: [] });
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -1023,6 +1124,7 @@ class Graph extends EventEmitter {
|
||||
this.get('modeController').destroy();
|
||||
this.get('viewController').destroy();
|
||||
this.get('stateController').destroy();
|
||||
this.get('customGroupControll').destroy();
|
||||
this.get('canvas').destroy();
|
||||
this._cfg = null;
|
||||
this.destroyed = true;
|
||||
|
@ -316,7 +316,7 @@ class TreeGraph extends Graph {
|
||||
onFrame(ratio) {
|
||||
Util.traverseTree(data, child => {
|
||||
const node = self.findById(child.id);
|
||||
// 当存在节点时候,执行动画效果
|
||||
// 只有当存在node的时候才执行
|
||||
if (node) {
|
||||
let origin = node.get('origin');
|
||||
const model = node.get('model');
|
||||
|
145
src/util/groupData.js
Normal file
145
src/util/groupData.js
Normal file
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* @Author: moyee
|
||||
* @Date: 2019-07-30 10:39:59
|
||||
* @LastEditors: moyee
|
||||
* @LastEditTime: 2019-08-20 17:04:25
|
||||
* @Description: 群组数据格式转换
|
||||
*/
|
||||
import { cloneDeep, groupBy, merge } from 'lodash';
|
||||
const groupMapNodes = {};
|
||||
/**
|
||||
* 扁平的数据格式转成树形
|
||||
* @param {array} data 扁平结构的数据
|
||||
* @param {string} value 树状结构的唯一标识
|
||||
* @param {string} parentId 父节点的键值
|
||||
* @return {array} 转成的树形结构数据
|
||||
*/
|
||||
export const flatToTree = (data, value = 'id', parentId = 'parentId') => {
|
||||
const children = 'children';
|
||||
const valueMap = [];
|
||||
const tree = [];
|
||||
|
||||
const { groups } = data;
|
||||
|
||||
groups.forEach(v => {
|
||||
valueMap[v[value]] = v;
|
||||
});
|
||||
|
||||
groups.forEach(v => {
|
||||
const parent = valueMap[v[parentId]];
|
||||
if (parent) {
|
||||
!parent[children] && (parent[children] = []);
|
||||
parent[children].push(v);
|
||||
} else {
|
||||
tree.push(v);
|
||||
}
|
||||
});
|
||||
|
||||
return tree;
|
||||
};
|
||||
|
||||
const arr = [];
|
||||
|
||||
export const addNodesToParentNode = (originData, nodes) => {
|
||||
const calcNodes = data => {
|
||||
data.forEach(row => {
|
||||
if (row.children) {
|
||||
arr.push({
|
||||
id: row.id,
|
||||
parentId: row.parentId
|
||||
});
|
||||
addNodesToParentNode(row.children, nodes);
|
||||
} else {
|
||||
arr.push({
|
||||
id: row.id,
|
||||
parentId: row.parentId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (arr.length > 0) {
|
||||
const nodeMap = groupIds => {
|
||||
if (groupIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
// const selfIds = groupIds.map(node => node.id);
|
||||
// const parentIds = groupIds.map(node => node.parentId);
|
||||
// const ids = new Set(selfIds);
|
||||
// parentIds.forEach(pid => ids.add(pid));
|
||||
|
||||
const first = groupIds.shift();
|
||||
const x = cloneDeep(groupIds);
|
||||
groupMapNodes[first.id] = x;
|
||||
nodeMap(groupIds);
|
||||
};
|
||||
|
||||
nodeMap(arr);
|
||||
}
|
||||
arr.length = 0;
|
||||
};
|
||||
calcNodes(originData);
|
||||
return groupMapNodes;
|
||||
};
|
||||
|
||||
export const getAllNodeInGroups = data => {
|
||||
const groupById = groupBy(data.groups, 'id');
|
||||
const groupByParentId = groupBy(data.groups, 'parentId');
|
||||
|
||||
const result = {};
|
||||
for (const parentId in groupByParentId) {
|
||||
if (!parentId) {
|
||||
continue;
|
||||
}
|
||||
// 获取当前parentId的所有子group ID
|
||||
const subGroupIds = groupByParentId[parentId];
|
||||
|
||||
// 获取在parentid群组中的节点
|
||||
const nodeInParentGroup = groupById[parentId];
|
||||
|
||||
if (nodeInParentGroup) {
|
||||
// 合并
|
||||
const parentGroupNodes = [ ...subGroupIds, ...nodeInParentGroup ];
|
||||
result[parentId] = parentGroupNodes;
|
||||
} else {
|
||||
result[parentId] = subGroupIds;
|
||||
}
|
||||
}
|
||||
const allGroupsId = merge({}, groupById, result);
|
||||
|
||||
// 缓存所有group包括的groupID
|
||||
const groupIds = {};
|
||||
for (const groupId in allGroupsId) {
|
||||
if (!groupId || groupId === 'undefined') {
|
||||
continue;
|
||||
}
|
||||
const subGroupIds = allGroupsId[groupId].map(node => node.id);
|
||||
|
||||
// const nodesInGroup = data.nodes.filter(node => node.groupId === groupId).map(node => node.id);
|
||||
groupIds[groupId] = subGroupIds;
|
||||
}
|
||||
|
||||
// 缓存所有groupID对应的Node
|
||||
const groupNodes = {};
|
||||
for (const groupId in groupIds) {
|
||||
if (!groupId || groupId === 'undefined') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const subGroupIds = groupIds[groupId];
|
||||
|
||||
// const subGroupIds = allGroupsId[groupId].map(node => node.id);
|
||||
|
||||
// 解析所有子群组
|
||||
const parentSubGroupIds = [];
|
||||
|
||||
for (const subId of subGroupIds) {
|
||||
const tmpGroupId = allGroupsId[subId].map(node => node.id);
|
||||
// const tmpNodes = data.nodes.filter(node => node.groupId === subId).map(node => node.id);
|
||||
parentSubGroupIds.push(...tmpGroupId);
|
||||
}
|
||||
|
||||
const nodesInGroup = data.nodes.filter(node => parentSubGroupIds.indexOf(node.groupId) > -1).map(node => node.id);
|
||||
groupNodes[groupId] = nodesInGroup;
|
||||
}
|
||||
return groupNodes;
|
||||
};
|
@ -8,5 +8,6 @@ const MathUtil = require('./math');
|
||||
const PathUtil = require('./path');
|
||||
const BaseUtil = require('./base');
|
||||
const GraphicUtil = require('./graphic');
|
||||
BaseUtil.deepMix(Util, BaseUtil, GraphicUtil, PathUtil, MathUtil);
|
||||
const GroupUtil = require('./groupData');
|
||||
BaseUtil.deepMix(Util, BaseUtil, GraphicUtil, PathUtil, MathUtil, GroupUtil);
|
||||
module.exports = Util;
|
||||
|
741
test/unit/behavior/drag-group-spec.js
Normal file
741
test/unit/behavior/drag-group-spec.js
Normal file
@ -0,0 +1,741 @@
|
||||
/*
|
||||
* @Author: moyee
|
||||
* @Date: 2019-07-31 11:54:41
|
||||
* @LastEditors: moyee
|
||||
* @LastEditTime: 2019-08-23 14:16:27
|
||||
* @Description: Group Behavior单测文件
|
||||
*/
|
||||
const expect = require('chai').expect;
|
||||
const G6 = require('../../../src');
|
||||
const Util = G6.Util;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'drag-group-spec';
|
||||
document.body.appendChild(div);
|
||||
|
||||
G6.registerNode('circleNode', {
|
||||
drawShape(cfg, group) {
|
||||
const keyShape = group.addShape('circle', {
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
r: 30,
|
||||
fill: '#87e8de'
|
||||
}
|
||||
});
|
||||
|
||||
return keyShape;
|
||||
}
|
||||
}, 'circle');
|
||||
|
||||
describe('drag signle layer group', () => {
|
||||
|
||||
it('drag signle layer group', () => {
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'node1',
|
||||
groupId: 'group1',
|
||||
x: 100,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'node2',
|
||||
groupId: 'group1',
|
||||
x: 150,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
label: 'node3',
|
||||
groupId: 'group2',
|
||||
x: 300,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node7',
|
||||
groupId: 'p1',
|
||||
x: 200,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
id: 'node6',
|
||||
groupId: 'bym',
|
||||
label: 'rect',
|
||||
x: 100,
|
||||
y: 300,
|
||||
shape: 'rect'
|
||||
},
|
||||
{
|
||||
id: 'node9',
|
||||
label: 'noGroup',
|
||||
x: 300,
|
||||
y: 210
|
||||
}
|
||||
]
|
||||
};
|
||||
const graph = new G6.Graph({
|
||||
container: div,
|
||||
width: 1500,
|
||||
height: 1000,
|
||||
pixelRatio: 2,
|
||||
modes: {
|
||||
default: [ 'drag-group' ]
|
||||
},
|
||||
defaultNode: {
|
||||
shape: 'circleNode'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
}
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
const groupControll = graph.get('customGroupControll');
|
||||
|
||||
const { nodeGroup } = groupControll.getDeletageGroupById('group1');
|
||||
|
||||
const nodes = data.nodes.filter(node => node.groupId === 'group1');
|
||||
|
||||
expect(nodes.length).eql(2);
|
||||
|
||||
const node1 = nodes[0];
|
||||
const node2 = nodes[1];
|
||||
expect(node1.x).eql(100);
|
||||
expect(node1.y).eql(100);
|
||||
expect(node2.x).eql(150);
|
||||
expect(node2.y).eql(100);
|
||||
|
||||
const keyShape = nodeGroup.get('keyShape');
|
||||
|
||||
const { width, height } = keyShape.getBBox();
|
||||
|
||||
// 触发mousedown事件
|
||||
graph.emit('dragstart', {
|
||||
canvasX: 0,
|
||||
canvasY: 0,
|
||||
target: keyShape
|
||||
});
|
||||
|
||||
graph.emit('drag', {
|
||||
canvasX: 150,
|
||||
canvasY: 150,
|
||||
target: keyShape
|
||||
});
|
||||
|
||||
graph.emit('dragend', {
|
||||
target: keyShape
|
||||
});
|
||||
|
||||
const nodeIds = data.nodes.filter(node => node.groupId === 'group1').map(node => node.id);
|
||||
const { x, y, width: w, height: h } = groupControll.calculationGroupPosition(nodeIds);
|
||||
const r = w > h ? w / 2 : h / 2;
|
||||
const cx = (w + 2 * x) / 2;
|
||||
const cy = (h + 2 * y) / 2;
|
||||
expect(keyShape.attr('r')).eql(r + nodeIds.length * 10);
|
||||
expect(keyShape.attr('x')).eql(cx);
|
||||
expect(keyShape.attr('y')).eql(cy);
|
||||
|
||||
const bbox = keyShape.getBBox();
|
||||
|
||||
// 拖动完成以后group宽高不变
|
||||
expect(bbox.width).eql(width);
|
||||
expect(bbox.height).eql(height);
|
||||
|
||||
// 拖拽完以后,group移动到了(100, 100)位置,group中的节点也移动了相应的距离
|
||||
expect(node1.x).eql(125);
|
||||
expect(node1.y).eql(150);
|
||||
expect(node1.x).eql(125);
|
||||
expect(node2.y).eql(150);
|
||||
|
||||
// 拖动以后,节点group的matrix值
|
||||
const node = graph.findById(node1.id);
|
||||
const matrix = node.get('group').getMatrix();
|
||||
expect(matrix[6]).eql(125);
|
||||
expect(matrix[7]).eql(150);
|
||||
|
||||
graph.destroy();
|
||||
expect(graph.destroyed).to.be.true;
|
||||
});
|
||||
|
||||
it('drag group of node to out', () => {
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'node1',
|
||||
groupId: 'group1',
|
||||
x: 100,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'node2',
|
||||
groupId: 'group1',
|
||||
x: 150,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
label: 'node3',
|
||||
groupId: 'group2',
|
||||
x: 300,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node7',
|
||||
groupId: 'p1',
|
||||
x: 200,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
id: 'node6',
|
||||
groupId: 'bym',
|
||||
label: 'rect',
|
||||
x: 100,
|
||||
y: 300,
|
||||
shape: 'rect'
|
||||
},
|
||||
{
|
||||
id: 'node9',
|
||||
label: 'noGroup',
|
||||
x: 300,
|
||||
y: 210
|
||||
}
|
||||
]
|
||||
};
|
||||
const graph = new G6.Graph({
|
||||
container: div,
|
||||
width: 1500,
|
||||
height: 1000,
|
||||
pixelRatio: 2,
|
||||
modes: {
|
||||
default: [ 'drag-node-with-group' ]
|
||||
},
|
||||
defaultNode: {
|
||||
shape: 'circleNode'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
}
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
const nodes = data.nodes.filter(node => node.groupId === 'group1');
|
||||
|
||||
expect(nodes.length).eql(2);
|
||||
|
||||
const node = graph.findById('node1');
|
||||
|
||||
const matrixBefore = node.get('group').getMatrix();
|
||||
expect(matrixBefore[6]).eql(100);
|
||||
expect(matrixBefore[7]).eql(100);
|
||||
|
||||
const groupControll = graph.get('customGroupControll');
|
||||
const { nodeGroup } = groupControll.getDeletageGroupById('group1');
|
||||
const keyShape = nodeGroup.get('keyShape');
|
||||
|
||||
graph.emit('node:dragstart', {
|
||||
target: node,
|
||||
item: node,
|
||||
x: 0,
|
||||
y: 0
|
||||
});
|
||||
|
||||
graph.emit('mouseenter', {
|
||||
target: keyShape
|
||||
});
|
||||
|
||||
graph.emit('node:drag', {
|
||||
target: node,
|
||||
item: node,
|
||||
x: 200,
|
||||
y: 250
|
||||
});
|
||||
|
||||
// 拖动过程中,group中还会保留原来的node
|
||||
expect(nodes.length).eql(2);
|
||||
const matrix = node.get('group').getMatrix();
|
||||
|
||||
expect(matrix[6]).eql(100);
|
||||
expect(matrix[7]).eql(100);
|
||||
|
||||
graph.emit('node:dragend', {
|
||||
item: node,
|
||||
target: node,
|
||||
x: 200,
|
||||
y: 250
|
||||
});
|
||||
|
||||
graph.paint();
|
||||
const matrixEnd = node.get('group').getMatrix();
|
||||
expect(matrixEnd[6]).eql(300);
|
||||
expect(matrixEnd[7]).eql(350);
|
||||
|
||||
const gnodes = graph.getNodes().filter(node => {
|
||||
const model = node.getModel();
|
||||
return model.groupId === 'group1';
|
||||
});
|
||||
// 将指定节点拖出group外,group中只有一个节点
|
||||
expect(gnodes.length).eql(1);
|
||||
expect(gnodes[0].get('id')).eql('node2');
|
||||
graph.destroy();
|
||||
expect(graph.destroyed).to.be.true;
|
||||
});
|
||||
|
||||
it('drag node to group', () => {
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'node1',
|
||||
groupId: 'group1',
|
||||
x: 100,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'node2',
|
||||
groupId: 'group1',
|
||||
x: 150,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
label: 'node3',
|
||||
groupId: 'group2',
|
||||
x: 300,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node7',
|
||||
groupId: 'p1',
|
||||
x: 200,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
id: 'node6',
|
||||
groupId: 'bym',
|
||||
label: 'rect',
|
||||
x: 100,
|
||||
y: 300,
|
||||
shape: 'rect'
|
||||
},
|
||||
{
|
||||
id: 'node9',
|
||||
label: 'noGroup',
|
||||
x: 300,
|
||||
y: 210
|
||||
}
|
||||
]
|
||||
};
|
||||
const graph = new G6.Graph({
|
||||
container: div,
|
||||
width: 1500,
|
||||
height: 1000,
|
||||
pixelRatio: 2,
|
||||
modes: {
|
||||
default: [ 'drag-node-with-group' ]
|
||||
},
|
||||
defaultNode: {
|
||||
shape: 'circleNode'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
}
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
const nodes = data.nodes.filter(node => node.groupId === 'group1');
|
||||
|
||||
expect(nodes.length).eql(2);
|
||||
|
||||
// 将group2中的node3拖入到group1中
|
||||
const node = graph.findById('node3');
|
||||
const group3Nodes = data.nodes.filter(node => node.groupId === 'group2');
|
||||
expect(group3Nodes.length).eql(1);
|
||||
|
||||
const matrixBefore = node.get('group').getMatrix();
|
||||
expect(matrixBefore[6]).eql(300);
|
||||
expect(matrixBefore[7]).eql(100);
|
||||
|
||||
const groupControll = graph.get('customGroupControll');
|
||||
const { nodeGroup } = groupControll.getDeletageGroupById('group1');
|
||||
const keyShape = nodeGroup.get('keyShape');
|
||||
|
||||
graph.emit('node:dragstart', {
|
||||
target: node,
|
||||
item: node,
|
||||
x: 0,
|
||||
y: 0
|
||||
});
|
||||
|
||||
graph.emit('mouseenter', {
|
||||
target: keyShape
|
||||
});
|
||||
|
||||
graph.emit('node:drag', {
|
||||
target: node,
|
||||
item: node,
|
||||
x: -200,
|
||||
y: -60
|
||||
});
|
||||
|
||||
// 拖动过程中,group中还会保留原来的node
|
||||
expect(nodes.length).eql(2);
|
||||
const matrix = node.get('group').getMatrix();
|
||||
|
||||
expect(matrix[6]).eql(300);
|
||||
expect(matrix[7]).eql(100);
|
||||
|
||||
graph.emit('node:dragend', {
|
||||
item: node,
|
||||
target: node,
|
||||
x: -200,
|
||||
y: -60
|
||||
});
|
||||
|
||||
graph.paint();
|
||||
const matrixEnd = node.get('group').getMatrix();
|
||||
expect(matrixEnd[6]).eql(100);
|
||||
expect(matrixEnd[7]).eql(40);
|
||||
|
||||
const gnodes = graph.getNodes().filter(node => {
|
||||
const model = node.getModel();
|
||||
return model.groupId === 'group1';
|
||||
});
|
||||
// 将指定节点拖如到group1中,group中有3个节点
|
||||
expect(gnodes.length).eql(3);
|
||||
const node3GroupId = gnodes.filter(node => {
|
||||
const model = node.getModel();
|
||||
return model.id === 'node3';
|
||||
}).map(node => {
|
||||
const model = node.getModel();
|
||||
return model.groupId;
|
||||
});
|
||||
|
||||
expect(node3GroupId[0]).eql('group1');
|
||||
graph.destroy();
|
||||
expect(graph.destroyed).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('nesting layer group', () => {
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node6',
|
||||
groupId: 'group3',
|
||||
label: 'rect',
|
||||
x: 100,
|
||||
y: 300
|
||||
},
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'fck',
|
||||
groupId: 'group1',
|
||||
x: 100,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node9',
|
||||
label: 'noGroup1',
|
||||
groupId: 'p1',
|
||||
x: 300,
|
||||
y: 210
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'node2',
|
||||
groupId: 'group1',
|
||||
x: 150,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
label: 'node3',
|
||||
groupId: 'group2',
|
||||
x: 300,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node7',
|
||||
groupId: 'p1',
|
||||
label: 'node7-p1',
|
||||
x: 200,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
id: 'node10',
|
||||
label: 'noGroup',
|
||||
groupId: 'p2',
|
||||
x: 300,
|
||||
y: 210
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'node1',
|
||||
target: 'node2'
|
||||
},
|
||||
{
|
||||
source: 'node2',
|
||||
target: 'node3'
|
||||
}
|
||||
],
|
||||
groups: [
|
||||
{
|
||||
id: 'group1',
|
||||
title: '1',
|
||||
parentId: 'p1'
|
||||
},
|
||||
{
|
||||
id: 'group2',
|
||||
title: '2',
|
||||
parentId: 'p1'
|
||||
},
|
||||
{
|
||||
id: 'group3',
|
||||
title: '2',
|
||||
parentId: 'p2'
|
||||
},
|
||||
{
|
||||
id: 'p1',
|
||||
title: '3'
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
title: '3'
|
||||
}
|
||||
]
|
||||
};
|
||||
it('render nesting layer group', () => {
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: div,
|
||||
width: 1500,
|
||||
height: 1000,
|
||||
pixelRatio: 2,
|
||||
modes: {
|
||||
default: [ 'drag-group', 'drag-node-with-group', 'collapse-expand-group' ]
|
||||
},
|
||||
defaultNode: {
|
||||
shape: 'circleNode'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
}
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
expect(graph.destroyed).to.be.undefined;
|
||||
|
||||
const groupControll = graph.get('customGroupControll');
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
const { groups } = graph.save();
|
||||
expect(groups.length).equal(5);
|
||||
|
||||
// 渲染的每个group的位置和坐标是否和计算的一致
|
||||
const groupNodes = Util.getAllNodeInGroups(data);
|
||||
for (const groupId in groupNodes) {
|
||||
const nodeIds = groupNodes[groupId];
|
||||
const { x, y, width, height } = groupControll.calculationGroupPosition(nodeIds);
|
||||
const r = width > height ? width / 2 : height / 2;
|
||||
const cx = (width + 2 * x) / 2;
|
||||
const cy = (height + 2 * y) / 2;
|
||||
|
||||
const groupShape = groupControll.getDeletageGroupById(groupId);
|
||||
|
||||
const { groupStyle } = groupShape;
|
||||
expect(groupStyle.x).eql(cx);
|
||||
expect(groupStyle.y).eql(cy);
|
||||
expect(groupStyle.r).eql(r);
|
||||
}
|
||||
|
||||
// 指定groupId,验证渲染后的位置是否正确
|
||||
const shape = groupControll.getDeletageGroupById('group2');
|
||||
const shapeStyle = shape.groupStyle;
|
||||
expect(shapeStyle.r).eql(30.5);
|
||||
expect(shapeStyle.x).eql(299.5);
|
||||
expect(shapeStyle.y).eql(99.5);
|
||||
|
||||
graph.destroy();
|
||||
expect(graph.destroyed).to.be.true;
|
||||
});
|
||||
|
||||
it('drag group out from group', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: div,
|
||||
width: 1500,
|
||||
height: 1000,
|
||||
pixelRatio: 2,
|
||||
modes: {
|
||||
default: [ 'drag-group', 'drag-node-with-group', 'collapse-expand-group' ]
|
||||
},
|
||||
defaultNode: {
|
||||
shape: 'circleNode'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
}
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
// 等所有群组都渲染完以后再去做单测
|
||||
setTimeout(() => {
|
||||
const groupControll = graph.get('customGroupControll');
|
||||
const { nodeGroup } = groupControll.getDeletageGroupById('group1');
|
||||
|
||||
const groupNodes = graph.get('groupNodes');
|
||||
const p1Nodes = groupNodes.p1;
|
||||
const group1Nodes = groupNodes.group1;
|
||||
expect(p1Nodes.length).eql(5);
|
||||
expect(p1Nodes.indexOf('node1') > -1).to.be.true;
|
||||
expect(p1Nodes.indexOf('nop1') > -1).to.be.false;
|
||||
expect(group1Nodes.length).eql(2);
|
||||
|
||||
const keyShape = nodeGroup.get('keyShape');
|
||||
|
||||
graph.emit('dragstart', {
|
||||
target: keyShape,
|
||||
canvasX: 0,
|
||||
canvasY: 0
|
||||
});
|
||||
|
||||
graph.emit('drag', {
|
||||
target: keyShape,
|
||||
canvasX: 500,
|
||||
canvasY: 200
|
||||
});
|
||||
|
||||
// 还没有拖出群组,group p1中还包括group1
|
||||
expect(p1Nodes.length).eql(5);
|
||||
expect(p1Nodes.indexOf('node2') > -1).to.be.true;
|
||||
graph.emit('dragend', {
|
||||
target: keyShape,
|
||||
canvasX: 500,
|
||||
canvasY: 200
|
||||
});
|
||||
|
||||
const currentP1Nodes = groupNodes.p1;
|
||||
// 拖出群组,group p1中不包括group1
|
||||
expect(currentP1Nodes.length).eql(3);
|
||||
expect(currentP1Nodes.indexOf('node1') > -1).to.be.false;
|
||||
expect(currentP1Nodes.indexOf('node2') > -1).to.be.false;
|
||||
expect(group1Nodes.length).eql(2);
|
||||
|
||||
graph.destroy();
|
||||
expect(graph.destroyed).to.be.true;
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
it('drag node to out from nesting group', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: div,
|
||||
width: 1500,
|
||||
height: 1000,
|
||||
pixelRatio: 2,
|
||||
modes: {
|
||||
default: [ 'drag-node-with-group' ]
|
||||
},
|
||||
defaultNode: {
|
||||
shape: 'circleNode'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
}
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
const nodes = data.nodes.filter(node => node.groupId === 'group1');
|
||||
|
||||
expect(nodes.length).eql(2);
|
||||
|
||||
const node = graph.findById('node1');
|
||||
|
||||
const matrixBefore = node.get('group').getMatrix();
|
||||
expect(matrixBefore[6]).eql(100);
|
||||
expect(matrixBefore[7]).eql(100);
|
||||
|
||||
const groupControll = graph.get('customGroupControll');
|
||||
const { nodeGroup } = groupControll.getDeletageGroupById('group1');
|
||||
const groupNodes = graph.get('groupNodes');
|
||||
|
||||
setTimeout(() => {
|
||||
graph.emit('node:dragstart', {
|
||||
target: node,
|
||||
item: node,
|
||||
x: 0,
|
||||
y: 0
|
||||
});
|
||||
|
||||
graph.emit('mouseenter', {
|
||||
target: nodeGroup
|
||||
});
|
||||
|
||||
graph.emit('node:drag', {
|
||||
target: node,
|
||||
item: node,
|
||||
x: 500,
|
||||
y: 250
|
||||
});
|
||||
|
||||
// 拖动过程中,group中还会保留原来的node
|
||||
expect(nodes.length).eql(2);
|
||||
const matrix = node.get('group').getMatrix();
|
||||
|
||||
expect(matrix[6]).eql(100);
|
||||
expect(matrix[7]).eql(100);
|
||||
|
||||
const p1Nodes = groupNodes.p1;
|
||||
const g1Nodes = groupNodes.group1;
|
||||
expect(p1Nodes.indexOf('node1') > -1).to.be.true;
|
||||
expect(g1Nodes.indexOf('node1') > -1).to.be.true;
|
||||
|
||||
graph.emit('node:dragend', {
|
||||
item: node,
|
||||
target: node,
|
||||
x: 500,
|
||||
y: 250
|
||||
});
|
||||
|
||||
graph.paint();
|
||||
const matrixEnd = node.get('group').getMatrix();
|
||||
expect(matrixEnd[6]).eql(975);
|
||||
expect(matrixEnd[7]).eql(400);
|
||||
|
||||
const gnodes = graph.getNodes().filter(node => {
|
||||
const model = node.getModel();
|
||||
return model.groupId === 'group1';
|
||||
});
|
||||
// 将指定节点拖出group外,group中只有一个节点
|
||||
expect(gnodes.length).eql(1);
|
||||
expect(gnodes[0].get('id')).eql('node2');
|
||||
|
||||
// 拖出以后,p1中也只有不包括node1
|
||||
const currentP1Nodes = groupNodes.p1;
|
||||
const currentG1Nodes = groupNodes.group1;
|
||||
expect(currentG1Nodes.indexOf('node1') > -1).to.be.false;
|
||||
expect(currentP1Nodes.indexOf('node1') > -1).to.be.false;
|
||||
graph.destroy();
|
||||
expect(graph.destroyed).to.be.true;
|
||||
}, 1000);
|
||||
});
|
||||
});
|
763
test/unit/graph/controller/custom-group-spec.js
Normal file
763
test/unit/graph/controller/custom-group-spec.js
Normal file
@ -0,0 +1,763 @@
|
||||
/*
|
||||
* @Author: moyee
|
||||
* @Date: 2019-07-31 11:54:41
|
||||
* @LastEditors: moyee
|
||||
* @LastEditTime: 2019-08-23 14:16:27
|
||||
* @Description: Group单测文件
|
||||
*/
|
||||
const expect = require('chai').expect;
|
||||
const G6 = require('../../../../src');
|
||||
const Util = G6.Util;
|
||||
const { groupBy } = require('lodash');
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'graph-group-spec';
|
||||
document.body.appendChild(div);
|
||||
|
||||
G6.registerNode('circleNode', {
|
||||
drawShape(cfg, group) {
|
||||
const keyShape = group.addShape('circle', {
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
r: 30,
|
||||
fill: '#87e8de'
|
||||
}
|
||||
});
|
||||
|
||||
return keyShape;
|
||||
}
|
||||
}, 'circle');
|
||||
|
||||
describe.only('signle layer group', () => {
|
||||
|
||||
it('render signle group test', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: div,
|
||||
width: 1500,
|
||||
height: 1000,
|
||||
pixelRatio: 2,
|
||||
modes: {
|
||||
default: [ 'drag-group' ]
|
||||
},
|
||||
defaultNode: {
|
||||
shape: 'circleNode'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
}
|
||||
});
|
||||
|
||||
const nodes = [
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'node1',
|
||||
groupId: 'group1',
|
||||
x: 100,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'node2',
|
||||
groupId: 'group1',
|
||||
x: 150,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
label: 'node3',
|
||||
groupId: 'group2',
|
||||
x: 300,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node7',
|
||||
groupId: 'p1',
|
||||
x: 200,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
id: 'node6',
|
||||
groupId: 'bym',
|
||||
label: 'rect',
|
||||
x: 100,
|
||||
y: 300,
|
||||
shape: 'rect'
|
||||
},
|
||||
{
|
||||
id: 'node9',
|
||||
label: 'noGroup',
|
||||
x: 300,
|
||||
y: 210
|
||||
}
|
||||
];
|
||||
const data = {
|
||||
nodes
|
||||
};
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
const groupControll = graph.get('customGroupControll');
|
||||
|
||||
const group = graph.get('customGroup');
|
||||
const children = group.get('children');
|
||||
// group的数量
|
||||
expect(children.length).equal(4);
|
||||
|
||||
// 每个group的圆心坐标
|
||||
const nodesInGroup = groupBy(data.nodes, 'groupId');
|
||||
for (const node in nodesInGroup) {
|
||||
if (node === 'undefined') {
|
||||
continue;
|
||||
}
|
||||
const currentNodes = nodesInGroup[node];
|
||||
const nodeIds = currentNodes.map(nodeId => nodeId.id);
|
||||
const { x, y, width, height } = groupControll.calculationGroupPosition(nodeIds);
|
||||
const r = width > height ? width / 2 : height / 2;
|
||||
const cx = (width + 2 * x) / 2;
|
||||
const cy = (height + 2 * y) / 2;
|
||||
|
||||
const groupShape = groupControll.getDeletageGroupById(node);
|
||||
|
||||
const { groupStyle } = groupShape;
|
||||
expect(groupStyle.x).eql(cx);
|
||||
expect(groupStyle.y).eql(cy);
|
||||
expect(groupStyle.r).eql(r);
|
||||
}
|
||||
|
||||
graph.destroy();
|
||||
expect(graph.destroyed).to.be.true;
|
||||
});
|
||||
|
||||
it('setGroupStyle', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: div,
|
||||
width: 1500,
|
||||
height: 1000,
|
||||
pixelRatio: 2,
|
||||
modes: {
|
||||
default: [ 'drag-group' ]
|
||||
},
|
||||
defaultNode: {
|
||||
shape: 'circleNode'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
}
|
||||
});
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'node1',
|
||||
groupId: 'group1',
|
||||
x: 100,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'node2',
|
||||
groupId: 'group1',
|
||||
x: 150,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
label: 'node3',
|
||||
groupId: 'group2',
|
||||
x: 300,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node7',
|
||||
groupId: 'p1',
|
||||
x: 200,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
id: 'node6',
|
||||
groupId: 'bym',
|
||||
label: 'rect',
|
||||
x: 100,
|
||||
y: 300,
|
||||
shape: 'rect'
|
||||
},
|
||||
{
|
||||
id: 'node9',
|
||||
label: 'noGroup',
|
||||
x: 300,
|
||||
y: 210
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
const groupControll = graph.get('customGroupControll');
|
||||
|
||||
const { nodeGroup } = groupControll.getDeletageGroupById('group2');
|
||||
const keyShape = nodeGroup.get('keyShape');
|
||||
groupControll.setGroupStyle(keyShape, 'hover');
|
||||
|
||||
// 这里的hover样式和定义custom group时指定的一样
|
||||
const hover = {
|
||||
stroke: '#faad14',
|
||||
fill: '#ffe58f',
|
||||
fillOpacity: 0.3,
|
||||
opacity: 0.3,
|
||||
lineWidth: 3
|
||||
};
|
||||
|
||||
expect(keyShape.attr('stroke')).eql(hover.stroke);
|
||||
expect(keyShape.attr('fill')).eql(hover.fill);
|
||||
expect(keyShape.attr('fillOpacity')).eql(hover.fillOpacity);
|
||||
expect(keyShape.attr('opacity')).eql(hover.opacity);
|
||||
expect(keyShape.attr('lineWidth')).eql(hover.lineWidth);
|
||||
expect(keyShape.attr('customStyle')).eql(undefined);
|
||||
|
||||
// 设置自定义样式属性customStyle
|
||||
groupControll.setGroupStyle(keyShape, { customStyle: true });
|
||||
expect(keyShape.attr('customStyle')).eql(true);
|
||||
|
||||
graph.destroy();
|
||||
expect(graph.destroyed).to.be.true;
|
||||
});
|
||||
|
||||
it('setGroupOriginBBox / getGroupOriginBBox', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: div,
|
||||
width: 1500,
|
||||
height: 1000,
|
||||
pixelRatio: 2,
|
||||
modes: {
|
||||
default: [ 'drag-group' ]
|
||||
},
|
||||
defaultNode: {
|
||||
shape: 'circleNode'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
}
|
||||
});
|
||||
|
||||
const nodes = [
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'node1',
|
||||
groupId: 'group1',
|
||||
x: 100,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'node2',
|
||||
groupId: 'group1',
|
||||
x: 150,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
label: 'node3',
|
||||
groupId: 'group2',
|
||||
x: 300,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node7',
|
||||
groupId: 'p1',
|
||||
x: 200,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
id: 'node6',
|
||||
groupId: 'bym',
|
||||
label: 'rect',
|
||||
x: 100,
|
||||
y: 300,
|
||||
shape: 'rect'
|
||||
},
|
||||
{
|
||||
id: 'node9',
|
||||
label: 'noGroup',
|
||||
x: 300,
|
||||
y: 210
|
||||
}
|
||||
];
|
||||
const data = {
|
||||
nodes
|
||||
};
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
const groupControll = graph.get('customGroupControll');
|
||||
|
||||
const { nodeGroup } = groupControll.getDeletageGroupById('group1');
|
||||
const keyShape = nodeGroup.get('keyShape');
|
||||
const bbox = keyShape.getBBox();
|
||||
// 调用setGroupOriginBBox方法存储group1的bbox
|
||||
groupControll.setGroupOriginBBox('group1', bbox);
|
||||
|
||||
const groupBBox = groupControll.getGroupOriginBBox('group1');
|
||||
expect(groupBBox).eql(bbox);
|
||||
|
||||
graph.destroy();
|
||||
expect(graph.destroyed).to.be.true;
|
||||
});
|
||||
|
||||
it('setDeletageGroupByStyle / getDeletageGroupById', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: div,
|
||||
width: 1500,
|
||||
height: 1000,
|
||||
pixelRatio: 2,
|
||||
modes: {
|
||||
default: [ 'drag-group' ]
|
||||
},
|
||||
defaultNode: {
|
||||
shape: 'circleNode'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
}
|
||||
});
|
||||
|
||||
const nodes = [
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'node1',
|
||||
groupId: 'group1',
|
||||
x: 100,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'node2',
|
||||
groupId: 'group1',
|
||||
x: 150,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
label: 'node3',
|
||||
groupId: 'group2',
|
||||
x: 300,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node7',
|
||||
groupId: 'p1',
|
||||
x: 200,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
id: 'node6',
|
||||
groupId: 'bym',
|
||||
label: 'rect',
|
||||
x: 100,
|
||||
y: 300,
|
||||
shape: 'rect'
|
||||
},
|
||||
{
|
||||
id: 'node9',
|
||||
label: 'noGroup',
|
||||
x: 300,
|
||||
y: 210
|
||||
}
|
||||
];
|
||||
const data = {
|
||||
nodes
|
||||
};
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
const groupControll = graph.get('customGroupControll');
|
||||
|
||||
// setDeletageGroupByStyle方法是在创建的时候就调用的,这里测试创建时候存进去的值通过getDeletageGroupById取出来后是否相同
|
||||
const group2 = groupControll.getDeletageGroupById('group2');
|
||||
const { groupStyle, nodeGroup } = group2;
|
||||
|
||||
expect(nodeGroup).not.null;
|
||||
expect(nodeGroup.get('id')).eql('group2');
|
||||
expect(nodeGroup.get('destroyed')).to.be.false;
|
||||
|
||||
const nodeInGroup2 = data.nodes.filter(node => node.groupId === 'group2').map(node => node.id);
|
||||
const { width, height, x, y } = groupControll.calculationGroupPosition(nodeInGroup2);
|
||||
|
||||
const r = width > height ? width / 2 : height / 2;
|
||||
const cx = (width + 2 * x) / 2;
|
||||
const cy = (height + 2 * y) / 2;
|
||||
|
||||
expect(groupStyle.x).eql(cx);
|
||||
expect(groupStyle.y).eql(cy);
|
||||
expect(groupStyle.r).eql(r);
|
||||
expect(groupStyle.width).eql(width);
|
||||
expect(groupStyle.height).eql(height);
|
||||
|
||||
// 通过不存在的groupId查询
|
||||
const notGroup = groupControll.getDeletageGroupById('group5');
|
||||
expect(notGroup).to.be.undefined;
|
||||
|
||||
graph.destroy();
|
||||
expect(graph.destroyed).to.be.true;
|
||||
});
|
||||
|
||||
it('collapseExpandGroup', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: div,
|
||||
width: 1500,
|
||||
height: 1000,
|
||||
pixelRatio: 2,
|
||||
modes: {
|
||||
default: [ 'drag-group' ]
|
||||
},
|
||||
defaultNode: {
|
||||
shape: 'circleNode'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
}
|
||||
});
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'node1',
|
||||
groupId: 'group1',
|
||||
x: 100,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'node2',
|
||||
groupId: 'group1',
|
||||
x: 150,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
label: 'node3',
|
||||
groupId: 'group2',
|
||||
x: 300,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node7',
|
||||
groupId: 'p1',
|
||||
x: 200,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
id: 'node6',
|
||||
groupId: 'bym',
|
||||
label: 'rect',
|
||||
x: 100,
|
||||
y: 300,
|
||||
shape: 'rect'
|
||||
},
|
||||
{
|
||||
id: 'node9',
|
||||
label: 'noGroup',
|
||||
x: 300,
|
||||
y: 210
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
const groupControll = graph.get('customGroupControll');
|
||||
const { nodeGroup } = groupControll.getDeletageGroupById('group1');
|
||||
expect(nodeGroup.get('hasHidden')).to.be.undefined;
|
||||
|
||||
groupControll.collapseExpandGroup('group1');
|
||||
expect(nodeGroup.get('hasHidden')).to.be.true;
|
||||
|
||||
groupControll.collapseExpandGroup('group1');
|
||||
expect(nodeGroup.get('hasHidden')).to.be.false;
|
||||
|
||||
graph.destroy();
|
||||
expect(graph.destroyed).to.be.true;
|
||||
});
|
||||
|
||||
it('collapseGroup / expandGroup', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: div,
|
||||
width: 1500,
|
||||
height: 1000,
|
||||
pixelRatio: 2,
|
||||
modes: {
|
||||
default: [ 'drag-group' ]
|
||||
},
|
||||
defaultNode: {
|
||||
shape: 'circleNode'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
}
|
||||
});
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'node1',
|
||||
groupId: 'group1',
|
||||
x: 100,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'node2',
|
||||
groupId: 'group1',
|
||||
x: 150,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
label: 'node3',
|
||||
groupId: 'group2',
|
||||
x: 300,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node7',
|
||||
groupId: 'p1',
|
||||
x: 200,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
id: 'node6',
|
||||
groupId: 'bym',
|
||||
label: 'rect',
|
||||
x: 100,
|
||||
y: 300,
|
||||
shape: 'rect'
|
||||
},
|
||||
{
|
||||
id: 'node9',
|
||||
label: 'noGroup',
|
||||
x: 300,
|
||||
y: 210
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
const groupControll = graph.get('customGroupControll');
|
||||
|
||||
const nodes = graph.getNodes();
|
||||
const groupNodes = nodes.filter(node => {
|
||||
const model = node.getModel();
|
||||
return model.groupId === 'group1';
|
||||
});
|
||||
|
||||
// 没有收起群组之前,所有节点都应该是显示状态
|
||||
for (const node of groupNodes) {
|
||||
const isVisible = node.isVisible();
|
||||
expect(isVisible).to.be.true;
|
||||
}
|
||||
|
||||
groupControll.collapseGroup('group1');
|
||||
|
||||
// group收起后,隐藏所有节点
|
||||
for (const node of groupNodes) {
|
||||
const isVisible = node.isVisible();
|
||||
expect(isVisible).to.be.false;
|
||||
}
|
||||
|
||||
// 同时检测之前的group是否正确
|
||||
const { nodeGroup, groupStyle } = groupControll.getDeletageGroupById('group1');
|
||||
const keyShape = nodeGroup.get('keyShape');
|
||||
// 延迟下判断,因为收起会有一个动画效果
|
||||
setTimeout(() => {
|
||||
expect(keyShape.attr('r')).eql(30);
|
||||
expect(keyShape.attr('x')).eql(groupStyle.x);
|
||||
expect(keyShape.attr('y')).eql(groupStyle.y);
|
||||
}, 2000);
|
||||
|
||||
// group 展开
|
||||
groupControll.expandGroup('group1');
|
||||
|
||||
setTimeout(() => {
|
||||
// 展开后,所有node都是显示状态
|
||||
for (const node of groupNodes) {
|
||||
const isVisible = node.isVisible();
|
||||
expect(isVisible).to.be.true;
|
||||
}
|
||||
|
||||
expect(keyShape.attr('r')).eql(groupStyle.r);
|
||||
expect(keyShape.attr('x')).eql(groupStyle.x);
|
||||
expect(keyShape.attr('y')).eql(groupStyle.y);
|
||||
|
||||
|
||||
const nodeIds = groupNodes.map(node => {
|
||||
const model = node.getModel();
|
||||
return model.id;
|
||||
});
|
||||
|
||||
const { width, height } = groupControll.calculationGroupPosition(nodeIds);
|
||||
expect(width).eql(groupStyle.width);
|
||||
expect(height).eql(groupStyle.height);
|
||||
|
||||
graph.destroy();
|
||||
expect(graph.destroyed).to.be.true;
|
||||
}, 5500);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('nesting layer group', () => {
|
||||
it('render nesting layer group', () => {
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node6',
|
||||
groupId: 'group3',
|
||||
label: 'rect',
|
||||
x: 100,
|
||||
y: 300
|
||||
},
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'fck',
|
||||
groupId: 'group1',
|
||||
x: 100,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node9',
|
||||
label: 'noGroup1',
|
||||
groupId: 'p1',
|
||||
x: 300,
|
||||
y: 210
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'node2',
|
||||
groupId: 'group1',
|
||||
x: 150,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
label: 'node3',
|
||||
groupId: 'group2',
|
||||
x: 300,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: 'node7',
|
||||
groupId: 'p1',
|
||||
label: 'node7-p1',
|
||||
x: 200,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
id: 'node10',
|
||||
label: 'noGroup',
|
||||
groupId: 'p2',
|
||||
x: 300,
|
||||
y: 210
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'node1',
|
||||
target: 'node2'
|
||||
},
|
||||
{
|
||||
source: 'node2',
|
||||
target: 'node3'
|
||||
}
|
||||
],
|
||||
groups: [
|
||||
{
|
||||
id: 'group1',
|
||||
title: '1',
|
||||
parentId: 'p1'
|
||||
},
|
||||
{
|
||||
id: 'group2',
|
||||
title: '2',
|
||||
parentId: 'p1'
|
||||
},
|
||||
{
|
||||
id: 'group3',
|
||||
title: '2',
|
||||
parentId: 'p2'
|
||||
},
|
||||
{
|
||||
id: 'p1',
|
||||
title: '3'
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
title: '3'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: div,
|
||||
width: 1500,
|
||||
height: 1000,
|
||||
pixelRatio: 2,
|
||||
modes: {
|
||||
default: [ 'drag-group' ]
|
||||
},
|
||||
defaultNode: {
|
||||
shape: 'circleNode'
|
||||
},
|
||||
defaultEdge: {
|
||||
color: '#bae7ff'
|
||||
}
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
const groupControll = graph.get('customGroupControll');
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
const { groups } = graph.save();
|
||||
expect(groups.length).equal(5);
|
||||
|
||||
// 渲染的每个group的位置和坐标是否和计算的一致
|
||||
const groupNodes = Util.getAllNodeInGroups(data);
|
||||
for (const groupId in groupNodes) {
|
||||
const nodeIds = groupNodes[groupId];
|
||||
const { x, y, width, height } = groupControll.calculationGroupPosition(nodeIds);
|
||||
const r = width > height ? width / 2 : height / 2;
|
||||
const cx = (width + 2 * x) / 2;
|
||||
const cy = (height + 2 * y) / 2;
|
||||
|
||||
const groupShape = groupControll.getDeletageGroupById(groupId);
|
||||
|
||||
const { groupStyle } = groupShape;
|
||||
expect(groupStyle.x).eql(cx);
|
||||
expect(groupStyle.y).eql(cy);
|
||||
expect(groupStyle.r).eql(r);
|
||||
}
|
||||
|
||||
// 指定groupId,验证渲染后的位置是否正确
|
||||
const shape = groupControll.getDeletageGroupById('group2');
|
||||
const shapeStyle = shape.groupStyle;
|
||||
expect(shapeStyle.r).eql(30.5);
|
||||
expect(shapeStyle.x).eql(299.5);
|
||||
expect(shapeStyle.y).eql(99.5);
|
||||
|
||||
graph.destroy();
|
||||
expect(graph.destroyed).to.be.true;
|
||||
});
|
||||
});
|
@ -30,7 +30,7 @@ describe('graph', () => {
|
||||
expect(inst.get('group').get('className')).to.equal('root-container');
|
||||
expect(inst.get('group').get('id').endsWith('-root')).to.be.true;
|
||||
const children = inst.get('group').get('children');
|
||||
expect(children.length).to.equal(2);
|
||||
expect(children.length).to.equal(4);
|
||||
expect(children[1].get('className')).to.equal('node-container');
|
||||
expect(children[0].get('className')).to.equal('edge-container');
|
||||
const nodes = inst.getNodes();
|
||||
|
211
test/unit/util/groupdata-spec.js
Normal file
211
test/unit/util/groupdata-spec.js
Normal file
@ -0,0 +1,211 @@
|
||||
/*
|
||||
* @Author: moyee
|
||||
* @Date: 2019-07-30 10:57:38
|
||||
* @LastEditors: moyee
|
||||
* @LastEditTime: 2019-08-20 17:03:00
|
||||
* @Description: file content
|
||||
*/
|
||||
const expect = require('chai').expect;
|
||||
const groupDataUtil = require('../../../src/util/groupData');
|
||||
|
||||
describe('group data transform util test', () => {
|
||||
it('flat transform to tree', () => {
|
||||
const flatData = [
|
||||
{
|
||||
id: 'group1',
|
||||
title: '1',
|
||||
parentId: 'p1'
|
||||
},
|
||||
{
|
||||
id: 'group2',
|
||||
title: '2',
|
||||
parentId: 'p1'
|
||||
},
|
||||
{
|
||||
id: 'p1',
|
||||
title: '3',
|
||||
parentId: 'p0'
|
||||
},
|
||||
{
|
||||
id: 'p0',
|
||||
parentId: 'p'
|
||||
},
|
||||
{
|
||||
id: 'p'
|
||||
},
|
||||
{
|
||||
id: 'bzn',
|
||||
title: 'bzn'
|
||||
},
|
||||
{
|
||||
id: 'bym',
|
||||
title: 'bym',
|
||||
parentId: 'bzn'
|
||||
},
|
||||
{
|
||||
id: 'yxl',
|
||||
title: 'yxl'
|
||||
}
|
||||
];
|
||||
|
||||
const originData = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'node1',
|
||||
groupId: 'group1'
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'node2',
|
||||
groupId: 'group1'
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
label: 'node3',
|
||||
groupId: 'group2'
|
||||
},
|
||||
{
|
||||
id: 'node6',
|
||||
groupId: 'p1'
|
||||
},
|
||||
{
|
||||
id: 'node6',
|
||||
groupId: 'bym'
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'node1',
|
||||
target: 'node2'
|
||||
}
|
||||
],
|
||||
groups: flatData
|
||||
};
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: 'p',
|
||||
children: [
|
||||
{
|
||||
id: 'p0',
|
||||
parentId: 'p',
|
||||
children: [
|
||||
{
|
||||
id: 'p1',
|
||||
parentId: 'p0',
|
||||
title: '3',
|
||||
nodes: [ 'node6' ],
|
||||
children: [
|
||||
{
|
||||
id: 'group1',
|
||||
title: '1',
|
||||
parentId: 'p1',
|
||||
nodes: [ 'node1', 'node2' ]
|
||||
},
|
||||
{
|
||||
id: 'group2',
|
||||
title: '2',
|
||||
parentId: 'p1',
|
||||
nodes: [ 'node3' ]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'bzn',
|
||||
title: 'bzn',
|
||||
children: [
|
||||
{
|
||||
id: 'bym',
|
||||
title: 'bym',
|
||||
parentId: 'bzn'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'yxl',
|
||||
title: 'yxl'
|
||||
}
|
||||
];
|
||||
|
||||
const treeData = groupDataUtil.flatToTree(originData);
|
||||
expect(treeData.length).equal(3);
|
||||
|
||||
const tree1 = treeData.filter(td => td.id === data[0].id);
|
||||
const tree2 = treeData.filter(td => td.id === data[1].id);
|
||||
const tree3 = treeData.filter(td => td.id === data[2].id);
|
||||
|
||||
expect(tree1.length).equal(1);
|
||||
expect(tree1[0].id).equal(data[0].id);
|
||||
expect(tree1[0].children.length).equal(1);
|
||||
|
||||
expect(tree2[0].id).equal(data[1].id);
|
||||
expect(tree2[0].children.length).equal(1);
|
||||
expect(tree2[0].children[0].parentId).equal(data[1].id);
|
||||
|
||||
expect(tree3.children).equal(undefined);
|
||||
|
||||
const groupByIdData = groupDataUtil.addNodesToParentNode(treeData, originData.nodes);
|
||||
expect(groupByIdData.p.length).equal(4);
|
||||
expect(groupByIdData.group2.length).equal(0);
|
||||
expect(groupByIdData.p1.length).equal(2);
|
||||
expect(groupByIdData.p1[0].id).equal('group1');
|
||||
expect(groupByIdData.p1[0].parentId).equal('p1');
|
||||
});
|
||||
|
||||
it('groupBy groupId test', () => {
|
||||
const nodes = [
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'node1',
|
||||
groupId: 'group1'
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'node2',
|
||||
groupId: 'group1'
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
label: 'node3',
|
||||
groupId: 'group2'
|
||||
},
|
||||
{
|
||||
id: 'node6',
|
||||
groupId: 'p1'
|
||||
},
|
||||
{
|
||||
id: 'node6',
|
||||
groupId: 'bym'
|
||||
}
|
||||
];
|
||||
|
||||
const nodesNoGroup = [
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'node1'
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'node2'
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
label: 'node3'
|
||||
},
|
||||
{
|
||||
id: 'node6'
|
||||
}
|
||||
];
|
||||
|
||||
const nodeHasGroupId = nodes.filter(node => node.groupId);
|
||||
expect(nodeHasGroupId.length).equal(5);
|
||||
|
||||
const nodeNoGroupId = nodesNoGroup.filter(node => node.groupId);
|
||||
expect(nodeNoGroupId.length).equal(0);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user