Merge pull request #818 from antvis/dev3.0.5

feat: group feature
This commit is contained in:
Moyee 2019-08-30 17:30:20 +08:00 committed by GitHub
commit 5e4dba8c3f
40 changed files with 6551 additions and 169 deletions

60
demos/arc-circle.html Normal file
View 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
View 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>

View File

@ -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'

View File

@ -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,9 +833,9 @@
stroke: "#A3B1BF"
}
},
layout: data => {
const result = Hierarchy.compactBox(data, {
direction: "LR",
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";

View 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>

View File

@ -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';

View File

@ -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
View 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
View 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>

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>

View 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
View 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
View 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>

View 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 提供的 issuehttps://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
View 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>

View File

@ -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 {

View File

@ -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",

View 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
View 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);
}
};

View 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();
}
};

View File

@ -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 事件句柄

View File

@ -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);

View File

@ -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: {

View 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 属性值里面包括widthheight和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;

View File

@ -3,5 +3,6 @@ module.exports = {
Event: require('./event'),
Mode: require('./mode'),
Item: require('./item'),
State: require('./state')
State: require('./state'),
CustomGroup: require('./customGroup')
};

View File

@ -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;

View File

@ -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
View 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;
};

View File

@ -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;

View 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);
});
});

View 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;
});
});

View File

@ -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();

View 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);
});
});