feat: support drag group when zoom change

This commit is contained in:
zhanning.bzn 2019-08-23 19:42:09 +08:00
parent 26fda9ba5c
commit 8883dd9966
11 changed files with 441 additions and 71 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>

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>

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

View File

@ -1,10 +1,3 @@
<!--
* @Author: moyee
* @Date: 2019-08-20 08:50:04
* @LastEditors: moyee
* @LastEditTime: 2019-08-20 16:57:14
* @Description: file content
-->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>

View File

@ -2,11 +2,10 @@
* @Author: moyee * @Author: moyee
* @Date: 2019-07-31 14:36:15 * @Date: 2019-07-31 14:36:15
* @LastEditors: moyee * @LastEditors: moyee
* @LastEditTime: 2019-08-22 15:06:01 * @LastEditTime: 2019-08-23 11:13:43
* @Description: file content * @Description: file content
*/ */
const { merge } = require('lodash');
import { merge } from 'lodash';
const delegateStyle = { const delegateStyle = {
fill: '#F3F9FF', fill: '#F3F9FF',
@ -119,7 +118,7 @@ module.exports = {
* @return {boolean} false/true * @return {boolean} false/true
*/ */
updatePosition(evt) { updatePosition(evt) {
if (!this.rectPos || !this.delegateShapeBBox) { if (!this.delegateShapeBBox) {
return false; return false;
} }
@ -189,7 +188,8 @@ module.exports = {
// step 1先修改groupId中的节点位置 // step 1先修改groupId中的节点位置
const nodeInGroup = groupNodes[groupId]; const nodeInGroup = groupNodes[groupId];
const groupOriginBBox = customGroupControll.getGroupOriginBBox(groupId);
const delegateShapeBBoxs = this.delegateShapeBBoxs[groupId];
const otherGroupId = []; const otherGroupId = [];
nodeInGroup.forEach((nodeId, index) => { nodeInGroup.forEach((nodeId, index) => {
@ -205,11 +205,9 @@ module.exports = {
}; };
} }
const groupOriginBBox = customGroupControll.getGroupOriginBBox(groupId);
// 群组拖动后节点的位置deletateShape的最终位置-群组起始位置+节点位置 // 群组拖动后节点的位置deletateShape的最终位置-群组起始位置+节点位置
const x = this.delegateShapeBBoxs[groupId].x - groupOriginBBox.x + this.nodePoint[index].x; const x = delegateShapeBBoxs.x - groupOriginBBox.x + this.nodePoint[index].x;
const y = this.delegateShapeBBoxs[groupId].y - groupOriginBBox.y + this.nodePoint[index].y; const y = delegateShapeBBoxs.y - groupOriginBBox.y + this.nodePoint[index].y;
this.nodePoint[index] = { this.nodePoint[index] = {
x, y x, y
@ -238,7 +236,6 @@ module.exports = {
const cy = (height + 2 * y) / 2; const cy = (height + 2 * y) / 2;
groupKeyShape.attr('x', cx); groupKeyShape.attr('x', cx);
groupKeyShape.attr('y', cy); groupKeyShape.attr('y', cy);
customGroupControll.setGroupOriginBBox(id, groupKeyShape.getBBox()); customGroupControll.setGroupOriginBBox(id, groupKeyShape.getBBox());
}); });
@ -288,8 +285,7 @@ module.exports = {
}); });
self.shapeOrigin = { x: attrs.x, y: attrs.y }; self.shapeOrigin = { x: attrs.x, y: attrs.y };
} }
delegateShape.set('capture', false); // delegateShape.set('capture', false);
this.rectPos = { ...self.shapeOrigin };
self.delegateShapes[groupId] = delegateShape; self.delegateShapes[groupId] = delegateShape;
self.delegateShapeBBoxs[groupId] = delegateShape.getBBox(); self.delegateShapeBBoxs[groupId] = delegateShape.getBBox();
} else { } else {
@ -299,9 +295,9 @@ module.exports = {
const x = deltaX + shapeOrigin.x; const x = deltaX + shapeOrigin.x;
const y = deltaY + shapeOrigin.y; const y = deltaY + shapeOrigin.y;
delegateShape.attr({ x, y }); // 将Canvas坐标转成视口坐标
this.rectPos = { x, y }; const point = graph.getPointByCanvas(x, y);
delegateShape.attr({ x: point.x, y: point.y });
self.delegateShapeBBoxs[groupId] = delegateShape.getBBox(); self.delegateShapeBBoxs[groupId] = delegateShape.getBBox();
} }

View File

@ -2,10 +2,9 @@
* @Author: moyee * @Author: moyee
* @Date: 2019-06-27 18:12:06 * @Date: 2019-06-27 18:12:06
* @LastEditors: moyee * @LastEditors: moyee
* @LastEditTime: 2019-08-22 18:43:16 * @LastEditTime: 2019-08-23 13:54:53
* @Description: file content * @Description: 有group的情况下拖动节点的Behavior
*/ */
const { mix } = require('../util');
const { merge } = require('lodash'); const { merge } = require('lodash');
const { delegateStyle } = require('../global'); const { delegateStyle } = require('../global');
const body = document.body; const body = document.body;
@ -228,6 +227,8 @@ module.exports = {
x: cx, x: cx,
y: cy y: cy
}); });
customGroupControll.setGroupOriginBBox(groupId, keyShape.getBBox());
} }
if (keyShape) { if (keyShape) {
@ -344,29 +345,6 @@ module.exports = {
this.graph.paint(); 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 * 更新拖动元素时的delegate
* @param {Event} e 事件句柄 * @param {Event} e 事件句柄

View File

@ -19,7 +19,7 @@ const behaviors = {
'brush-select': require('./brush-select'), 'brush-select': require('./brush-select'),
'drag-group': require('./drag-group'), 'drag-group': require('./drag-group'),
'drag-node-with-group': require('./drag-node-with-group'), 'drag-node-with-group': require('./drag-node-with-group'),
'collspse-expand-group': require('./collapse-expand-group') 'collapse-expand-group': require('./collapse-expand-group')
}; };
Util.each(behaviors, (behavior, type) => { Util.each(behaviors, (behavior, type) => {
Behavior.registerBehavior(type, behavior); Behavior.registerBehavior(type, behavior);

View File

@ -2,12 +2,12 @@
* @Author: moyee * @Author: moyee
* @Date: 2019-07-30 12:10:26 * @Date: 2019-07-30 12:10:26
* @LastEditors: moyee * @LastEditors: moyee
* @LastEditTime: 2019-08-22 18:41:33 * @LastEditTime: 2019-08-23 11:44:32
* @Description: file content * @Description: file content
*/ */
import { merge, isString } from 'lodash'; const { merge, isString } = require('lodash');
export default class CustomGroup { class CustomGroup {
getDefaultCfg() { getDefaultCfg() {
return { return {
default: { default: {
@ -15,7 +15,7 @@ export default class CustomGroup {
stroke: '#A3B1BF', stroke: '#A3B1BF',
radius: 10, radius: 10,
lineDash: [ 5, 5 ], lineDash: [ 5, 5 ],
strokeOpacity: 0.92, strokeOpacity: 0.9,
fill: '#F3F9FF', fill: '#F3F9FF',
fillOpacity: 0.8, fillOpacity: 0.8,
opacity: 0.8 opacity: 0.8
@ -367,15 +367,22 @@ export default class CustomGroup {
* @param {string} tmpNodeId 临时节点ID * @param {string} tmpNodeId 临时节点ID
*/ */
setGroupTmpNode(groupId, tmpNodeId) { setGroupTmpNode(groupId, tmpNodeId) {
// TODO 需要调整
const graph = this.graph; const graph = this.graph;
const graphNodes = graph.get('groupNodes'); const graphNodes = graph.get('groupNodes');
const groups = graph.get('groups'); const groups = graph.get('groups');
for (const data of groups) { if (graphNodes[groupId].indexOf(tmpNodeId) < 0) {
if (data.parentId === groupId) { graphNodes[groupId].push(tmpNodeId);
graphNodes[groupId].push(tmpNodeId); }
this.setGroupTmpNode(data.parentId); // 获取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);
} }
} }
/** /**
@ -385,6 +392,7 @@ export default class CustomGroup {
* @memberof ItemGroup * @memberof ItemGroup
*/ */
collapseGroup(id) { collapseGroup(id) {
const self = this;
const customGroup = this.getDeletageGroupById(id); const customGroup = this.getDeletageGroupById(id);
const { nodeGroup, groupStyle } = customGroup; const { nodeGroup, groupStyle } = customGroup;
@ -406,6 +414,9 @@ export default class CustomGroup {
// 收起群组时候动画 // 收起群组时候动画
keyShape.animate({ keyShape.animate({
onFrame(ratio) { onFrame(ratio) {
if (ratio === 1) {
self.setGroupOriginBBox(id, keyShape.getBBox());
}
return { return {
r: groupStyle.r - ratio * (groupStyle.r - r) r: groupStyle.r - ratio * (groupStyle.r - r)
}; };
@ -445,13 +456,12 @@ export default class CustomGroup {
}; };
// 将临时添加的节点加入到群组中,以便拖动节点时候线跟着拖动 // 将临时添加的节点加入到群组中,以便拖动节点时候线跟着拖动
nodesInGroup.push(`${id}-custom-node`); // nodesInGroup.push(`${id}-custom-node`);
// this.setGroupTmpNode(id, `${id}-custom-node`); this.setGroupTmpNode(id, `${id}-custom-node`);
this.updateEdgeInGroupLinks(id, sourceOutTargetInEdges, sourceInTargetOutEdges); this.updateEdgeInGroupLinks(id, sourceOutTargetInEdges, sourceInTargetOutEdges);
} }
// 获取群组中节点之间的所有边 // 获取群组中节点之间的所有边
const edgeAllInGroup = edges.filter(edge => { const edgeAllInGroup = edges.filter(edge => {
const model = edge.getModel(); const model = edge.getModel();
@ -538,6 +548,7 @@ export default class CustomGroup {
*/ */
expandGroup(id) { expandGroup(id) {
const graph = this.graph; const graph = this.graph;
const self = this;
const autoPaint = graph.get('autoPaint'); const autoPaint = graph.get('autoPaint');
graph.setAutoPaint(false); graph.setAutoPaint(false);
@ -560,12 +571,16 @@ export default class CustomGroup {
// keyShape.attr('r', groupStyle.r + nodesInGroup.length * 10); // keyShape.attr('r', groupStyle.r + nodesInGroup.length * 10);
keyShape.animate({ keyShape.animate({
onFrame(ratio) { onFrame(ratio) {
if (ratio === 1) {
self.setGroupOriginBBox(id, keyShape.getBBox());
}
return { return {
r: 30 + ratio * (groupStyle.r + nodesInGroup.length * 10 - 30) r: 30 + ratio * (groupStyle.r + nodesInGroup.length * 10 - 30)
}; };
} }
}, 1000, 'easeCubic'); }, 1000, 'easeCubic');
// this.setGroupOriginBBox(id, keyShape.getBBox());
// 群组动画一会后再显示节点和边 // 群组动画一会后再显示节点和边
setTimeout(() => { setTimeout(() => {
nodesInGroup.forEach(nodeId => { nodesInGroup.forEach(nodeId => {
@ -590,7 +605,7 @@ export default class CustomGroup {
// 获取群组中节点之间的所有边 // 获取群组中节点之间的所有边
const edgeAllInGroup = edges.filter(edge => { const edgeAllInGroup = edges.filter(edge => {
const model = edge.getModel(); const model = edge.getModel();
return nodesInGroup.includes(model.source) && nodesInGroup.includes(model.target); return nodesInGroup.includes(model.source) || nodesInGroup.includes(model.target);
}); });
edgeAllInGroup.forEach(edge => { edgeAllInGroup.forEach(edge => {
@ -632,9 +647,8 @@ export default class CustomGroup {
// 删除群组中的临时节点ID // 删除群组中的临时节点ID
const tmpNodeModel = delegateNode.getModel(); const tmpNodeModel = delegateNode.getModel();
const index = nodesInGroup.indexOf(tmpNodeModel.id);
nodesInGroup.splice(index, 1);
this.deleteTmpNode(id, tmpNodeModel.id);
graph.remove(delegateNode); graph.remove(delegateNode);
delete this.delegateInGroup[id]; delete this.delegateInGroup[id];
} }
@ -642,6 +656,27 @@ export default class CustomGroup {
graph.paint(); 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);
}
}
/** /**
* 拆分群组 * 拆分群组
* *
@ -687,3 +722,5 @@ export default class CustomGroup {
this.delegateInGroup = {}; this.delegateInGroup = {};
} }
} }
module.exports = CustomGroup;

View File

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

View File

@ -9,7 +9,7 @@
* @fileOverview graph * @fileOverview graph
* @author huangtonger@aliyun.com * @author huangtonger@aliyun.com
*/ */
import { groupBy } from 'lodash'; const { groupBy } = require('lodash');
const G = require('@antv/g/lib'); const G = require('@antv/g/lib');
const EventEmitter = G.EventEmitter; const EventEmitter = G.EventEmitter;
const Util = require('../util'); const Util = require('../util');

View File

@ -2,7 +2,7 @@
* @Author: moyee * @Author: moyee
* @Date: 2019-07-31 11:54:41 * @Date: 2019-07-31 11:54:41
* @LastEditors: moyee * @LastEditors: moyee
* @LastEditTime: 2019-08-22 15:47:52 * @LastEditTime: 2019-08-23 14:16:27
* @Description: file content * @Description: file content
*/ */
const expect = require('chai').expect; const expect = require('chai').expect;
@ -12,6 +12,21 @@ const div = document.createElement('div');
div.id = 'graph-group-spec'; div.id = 'graph-group-spec';
document.body.appendChild(div); 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('graph group', () => { describe('graph group', () => {
const graph = new G6.Graph({ const graph = new G6.Graph({
container: div, container: div,
@ -19,7 +34,13 @@ describe('graph group', () => {
height: 1000, height: 1000,
pixelRatio: 2, pixelRatio: 2,
modes: { modes: {
default: [ 'drag-group', 'click-select', 'drag-node-with-group', 'collspse-expand-group' ] default: [ 'zoom-canvas', 'drag-group', 'click-select', 'drag-node-with-group', 'collapse-expand-group' ]
},
defaultNode: {
shape: 'circleNode'
},
defaultEdge: {
color: '#bae7ff'
} }
}); });
@ -87,8 +108,7 @@ describe('graph group', () => {
groupId: 'group3', groupId: 'group3',
label: 'rect', label: 'rect',
x: 100, x: 100,
y: 300, y: 300
shape: 'rect'
}, },
{ {
id: 'node1', id: 'node1',
@ -121,6 +141,7 @@ describe('graph group', () => {
{ {
id: 'node7', id: 'node7',
groupId: 'p1', groupId: 'p1',
label: 'node7-p1',
x: 200, x: 200,
y: 200 y: 200
}, },