mirror of
https://gitee.com/antv/g6.git
synced 2024-12-04 20:59:15 +08:00
Merge branch 'dev3.1.0' of https://github.com/antvis/g6 into dev3.1.0
This commit is contained in:
commit
cff5d62242
4641
demos/assets/data/concentric-data.json
Normal file
4641
demos/assets/data/concentric-data.json
Normal file
File diff suppressed because one or more lines are too long
221
demos/group-circle-fruchterman.html
Normal file
221
demos/group-circle-fruchterman.html
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Circle节点分组 fruchterman 布局</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="mountNode"></div>
|
||||||
|
<script src="../build/g6.js"></script>
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* 该案例演示以下功能:
|
||||||
|
* 1、渲染群组所需要的数据结构;
|
||||||
|
* 2、如何拖动一个群组;
|
||||||
|
* 3、将节点从群组中拖出;
|
||||||
|
* 4、将节点拖入到某个群组中;
|
||||||
|
* 5、拖出拖入节点后动态改变群组大小。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const graph = new G6.Graph({
|
||||||
|
container: 'mountNode',
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
layout: {
|
||||||
|
type: 'fruchtermanGroup'
|
||||||
|
},
|
||||||
|
defaultNode: {
|
||||||
|
shape: 'circle',
|
||||||
|
size: 10
|
||||||
|
},
|
||||||
|
defaultEdge: {
|
||||||
|
color: '#bae7ff'
|
||||||
|
},
|
||||||
|
modes: {
|
||||||
|
default: [ 'drag-canvas', 'drag-group', 'drag-node-with-group', 'collapse-expand-group' ]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const oneNodes = []
|
||||||
|
for(let i = 0; i < 50; i++) {
|
||||||
|
oneNodes.push({
|
||||||
|
id: `node-1-${i}`,
|
||||||
|
groupId: 'group3',
|
||||||
|
label: `node-1-${i}-group3`,
|
||||||
|
// ox: 100 + 5 * i,
|
||||||
|
// oy: 300 + 5 * i,
|
||||||
|
shape: 'circle'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
nodes: [
|
||||||
|
// ...oneNodes,
|
||||||
|
{
|
||||||
|
id: 'node6',
|
||||||
|
groupId: 'group3',
|
||||||
|
label: 'node6-group3',
|
||||||
|
ox: 100,
|
||||||
|
oy: 300,
|
||||||
|
shape: 'rect',
|
||||||
|
size: [40, 20]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node6-1',
|
||||||
|
groupId: 'group3',
|
||||||
|
label: 'node6-1-group3',
|
||||||
|
ox: 110,
|
||||||
|
oy: 300,
|
||||||
|
shape: 'rect',
|
||||||
|
size: [40, 20]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node6-2',
|
||||||
|
groupId: 'group3',
|
||||||
|
label: 'node6-2-group3',
|
||||||
|
ox: 120,
|
||||||
|
oy: 300,
|
||||||
|
shape: 'rect',
|
||||||
|
size: [40, 20]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node6-3',
|
||||||
|
groupId: 'group3',
|
||||||
|
label: 'node6-3-group3',
|
||||||
|
ox: 140,
|
||||||
|
oy: 300,
|
||||||
|
shape: 'rect',
|
||||||
|
size: [40, 20]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node6-4',
|
||||||
|
groupId: 'group3',
|
||||||
|
label: 'node6-4-group3',
|
||||||
|
ox: 150,
|
||||||
|
oy: 300,
|
||||||
|
shape: 'rect',
|
||||||
|
size: [40, 20]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node6-5',
|
||||||
|
groupId: 'group3',
|
||||||
|
label: 'node6-5-group3',
|
||||||
|
ox: 160,
|
||||||
|
oy: 300,
|
||||||
|
shape: 'rect',
|
||||||
|
size: [40, 20]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node1',
|
||||||
|
label: 'node1-group1',
|
||||||
|
groupId: 'group1',
|
||||||
|
ox: 100,
|
||||||
|
oy: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node9',
|
||||||
|
label: 'node9-p1',
|
||||||
|
groupId: 'p1',
|
||||||
|
ox: 300,
|
||||||
|
oy: 210
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node2',
|
||||||
|
label: 'node2-group1',
|
||||||
|
groupId: 'group1',
|
||||||
|
ox: 150,
|
||||||
|
oy: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node3',
|
||||||
|
label: 'node3-group2',
|
||||||
|
groupId: 'group2',
|
||||||
|
ox: 300,
|
||||||
|
oy: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node7',
|
||||||
|
groupId: 'p1',
|
||||||
|
label: 'node7-p1',
|
||||||
|
ox: 200,
|
||||||
|
oy: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node10',
|
||||||
|
label: 'node10-p2',
|
||||||
|
groupId: 'p2',
|
||||||
|
ox: 300,
|
||||||
|
oy: 210
|
||||||
|
}
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
source: 'node1',
|
||||||
|
target: 'node2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: 'node2',
|
||||||
|
target: 'node3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: 'node1',
|
||||||
|
target: 'node3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: 'node6',
|
||||||
|
target: 'node1'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
id: 'group1',
|
||||||
|
title: {
|
||||||
|
text: 'group1',
|
||||||
|
stroke: '#444',
|
||||||
|
offsetox: -20,
|
||||||
|
offsetoy: 30
|
||||||
|
},
|
||||||
|
parentId: 'p1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'group2',
|
||||||
|
title: {
|
||||||
|
text: 'group2'
|
||||||
|
},
|
||||||
|
title: '2',
|
||||||
|
parentId: 'p1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'group3',
|
||||||
|
title: {
|
||||||
|
text: 'group3',
|
||||||
|
stroke: '#444',
|
||||||
|
offsetox: -20,
|
||||||
|
offsetoy: 30
|
||||||
|
},
|
||||||
|
parentId: 'p2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p1',
|
||||||
|
title: {
|
||||||
|
text: 'p1'
|
||||||
|
},
|
||||||
|
title: '3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p2',
|
||||||
|
title: {
|
||||||
|
text: 'p2'
|
||||||
|
},
|
||||||
|
title: '3'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
graph.data(data)
|
||||||
|
graph.render()
|
||||||
|
console.log(graph);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
222
demos/group-rect-fruchterman.html
Normal file
222
demos/group-rect-fruchterman.html
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Circle节点分组 fruchterman 布局</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="mountNode"></div>
|
||||||
|
<script src="../build/g6.js"></script>
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* 该案例演示以下功能:
|
||||||
|
* 1、渲染群组所需要的数据结构;
|
||||||
|
* 2、如何拖动一个群组;
|
||||||
|
* 3、将节点从群组中拖出;
|
||||||
|
* 4、将节点拖入到某个群组中;
|
||||||
|
* 5、拖出拖入节点后动态改变群组大小。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const graph = new G6.Graph({
|
||||||
|
container: 'mountNode',
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
layout: {
|
||||||
|
type: 'fruchtermanGroup'
|
||||||
|
},
|
||||||
|
defaultNode: {
|
||||||
|
shape: 'circle',
|
||||||
|
size: 10
|
||||||
|
},
|
||||||
|
defaultEdge: {
|
||||||
|
color: '#bae7ff'
|
||||||
|
},
|
||||||
|
modes: {
|
||||||
|
default: [ 'drag-canvas', 'drag-group', 'drag-node-with-group', 'collapse-expand-group' ]
|
||||||
|
},
|
||||||
|
groupType: 'rect'
|
||||||
|
});
|
||||||
|
|
||||||
|
const oneNodes = []
|
||||||
|
for(let i = 0; i < 50; i++) {
|
||||||
|
oneNodes.push({
|
||||||
|
id: `node-1-${i}`,
|
||||||
|
groupId: 'group3',
|
||||||
|
label: `node-1-${i}-group3`,
|
||||||
|
// ox: 100 + 5 * i,
|
||||||
|
// oy: 300 + 5 * i,
|
||||||
|
shape: 'circle'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
nodes: [
|
||||||
|
// ...oneNodes,
|
||||||
|
{
|
||||||
|
id: 'node6',
|
||||||
|
groupId: 'group3',
|
||||||
|
label: 'node6-group3',
|
||||||
|
ox: 100,
|
||||||
|
oy: 300,
|
||||||
|
shape: 'rect',
|
||||||
|
size: [40, 20]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node6-1',
|
||||||
|
groupId: 'group3',
|
||||||
|
label: 'node6-1-group3',
|
||||||
|
ox: 110,
|
||||||
|
oy: 300,
|
||||||
|
shape: 'rect',
|
||||||
|
size: [40, 20]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node6-2',
|
||||||
|
groupId: 'group3',
|
||||||
|
label: 'node6-2-group3',
|
||||||
|
ox: 120,
|
||||||
|
oy: 300,
|
||||||
|
shape: 'rect',
|
||||||
|
size: [40, 20]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node6-3',
|
||||||
|
groupId: 'group3',
|
||||||
|
label: 'node6-3-group3',
|
||||||
|
ox: 140,
|
||||||
|
oy: 300,
|
||||||
|
shape: 'rect',
|
||||||
|
size: [40, 20]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node6-4',
|
||||||
|
groupId: 'group3',
|
||||||
|
label: 'node6-4-group3',
|
||||||
|
ox: 150,
|
||||||
|
oy: 300,
|
||||||
|
shape: 'rect',
|
||||||
|
size: [40, 20]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node6-5',
|
||||||
|
groupId: 'group3',
|
||||||
|
label: 'node6-5-group3',
|
||||||
|
ox: 160,
|
||||||
|
oy: 300,
|
||||||
|
shape: 'rect',
|
||||||
|
size: [40, 20]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node1',
|
||||||
|
label: 'node1-group1',
|
||||||
|
groupId: 'group1',
|
||||||
|
ox: 100,
|
||||||
|
oy: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node9',
|
||||||
|
label: 'node9-p1',
|
||||||
|
groupId: 'p1',
|
||||||
|
ox: 300,
|
||||||
|
oy: 210
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node2',
|
||||||
|
label: 'node2-group1',
|
||||||
|
groupId: 'group1',
|
||||||
|
ox: 150,
|
||||||
|
oy: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node3',
|
||||||
|
label: 'node3-group2',
|
||||||
|
groupId: 'group2',
|
||||||
|
ox: 300,
|
||||||
|
oy: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node7',
|
||||||
|
groupId: 'p1',
|
||||||
|
label: 'node7-p1',
|
||||||
|
ox: 200,
|
||||||
|
oy: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node10',
|
||||||
|
label: 'node10-p2',
|
||||||
|
groupId: 'p2',
|
||||||
|
ox: 300,
|
||||||
|
oy: 210
|
||||||
|
}
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
source: 'node1',
|
||||||
|
target: 'node2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: 'node2',
|
||||||
|
target: 'node3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: 'node1',
|
||||||
|
target: 'node3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: 'node6',
|
||||||
|
target: 'node1'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
id: 'group1',
|
||||||
|
title: {
|
||||||
|
text: 'group1',
|
||||||
|
stroke: '#444',
|
||||||
|
offsetox: -20,
|
||||||
|
offsetoy: 30
|
||||||
|
},
|
||||||
|
parentId: 'p1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'group2',
|
||||||
|
title: {
|
||||||
|
text: 'group2'
|
||||||
|
},
|
||||||
|
title: '2',
|
||||||
|
parentId: 'p1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'group3',
|
||||||
|
title: {
|
||||||
|
text: 'group3',
|
||||||
|
stroke: '#444',
|
||||||
|
offsetox: -20,
|
||||||
|
offsetoy: 30
|
||||||
|
},
|
||||||
|
parentId: 'p2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p1',
|
||||||
|
title: {
|
||||||
|
text: 'p1'
|
||||||
|
},
|
||||||
|
title: '3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p2',
|
||||||
|
title: {
|
||||||
|
text: 'p2'
|
||||||
|
},
|
||||||
|
title: '3'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
graph.data(data)
|
||||||
|
graph.render()
|
||||||
|
console.log(graph);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -7,64 +7,122 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<button id='changeView'>前置边</button>
|
<div id='description'>hover 节点或边,前置相关元素</button>
|
||||||
<div id="mountNode"></div>
|
<div id="mountNode"></div>
|
||||||
<script src="../build/g6.js"></script>
|
<script src="../build/g6.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const data = {
|
const data = {
|
||||||
nodes: [{
|
nodes: [{
|
||||||
id: 'node1',
|
id: 'node0',
|
||||||
x: 100,
|
x: 100,
|
||||||
y: 100,
|
y: 100,
|
||||||
shape: 'rect',
|
size: 20
|
||||||
style: {
|
},{
|
||||||
fill: '#fff'
|
id: 'node1',
|
||||||
}
|
|
||||||
}, {
|
|
||||||
id: 'node2',
|
|
||||||
x: 200,
|
x: 200,
|
||||||
y: 200,
|
y: 200,
|
||||||
shape: 'rect',
|
size: 20
|
||||||
style: {
|
},{
|
||||||
fill: '#fff'
|
id: 'node2',
|
||||||
}
|
x: 150,
|
||||||
}, {
|
y: 150,
|
||||||
|
size: 20
|
||||||
|
},{
|
||||||
id: 'node3',
|
id: 'node3',
|
||||||
x: 300,
|
x: 150,
|
||||||
y: 300,
|
y: 250,
|
||||||
shape: 'rect',
|
size: 20
|
||||||
style: {
|
},{
|
||||||
fill: '#fff'
|
id: 'node4',
|
||||||
}
|
x: 150,
|
||||||
}],
|
y: 200,
|
||||||
edges: [{
|
size: 20
|
||||||
id: 'edge1',
|
}],
|
||||||
target: 'node3',
|
edges: [{
|
||||||
source: 'node1',
|
id: 'edge0',
|
||||||
style: {
|
source: 'node0',
|
||||||
endArrow: true
|
target: 'node1'
|
||||||
},
|
},{
|
||||||
labelCfg: {
|
id: 'edge1',
|
||||||
style: { stroke: 'white', lineWidth: 5 } // 加白边框
|
source: 'node2',
|
||||||
}
|
target: 'node3'
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
const graph = new G6.Graph({
|
const graph = new G6.Graph({
|
||||||
container: 'mountNode',
|
container: 'mountNode',
|
||||||
width: 500,
|
width: 800,
|
||||||
height: 500
|
height: 600,
|
||||||
|
groupByTypes: false,
|
||||||
|
defaultEdge: {
|
||||||
|
style: {
|
||||||
|
lineWidth: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
graph.data(data);
|
graph.data(data);
|
||||||
graph.render();
|
graph.render();
|
||||||
graph.fitView()
|
// 获取图上的所有边实例
|
||||||
document.getElementById('changeView').addEventListener('click', (evt) => {
|
const nodes = graph.getNodes();
|
||||||
const edge=graph.findById('edge1')
|
// 遍历边实例,将所有边提前。
|
||||||
const nodeGroup = graph.get('nodeGroup')
|
nodes.forEach(node => {
|
||||||
const edge1G = edge.get('group')
|
node.toFront();
|
||||||
edge1G.toFront()
|
});
|
||||||
nodeGroup.toBack();
|
// 更改层级后需要重新绘制图
|
||||||
graph.paint()
|
graph.paint();
|
||||||
})
|
|
||||||
|
// 鼠标进入节点事件
|
||||||
|
graph.on('edge:mouseenter', ev => {
|
||||||
|
// 获得鼠标当前目标边
|
||||||
|
const edge = ev.item;
|
||||||
|
// 该边的起始点
|
||||||
|
const source = edge.getSource();
|
||||||
|
// 该边的结束点
|
||||||
|
const target = edge.getTarget();
|
||||||
|
// 先将边提前,再将端点提前。这样该边两个端点还是在该边上层,较符合常规。
|
||||||
|
edge.toFront();
|
||||||
|
source.toFront();
|
||||||
|
target.toFront();
|
||||||
|
// 注意:必须调用以根据新的层级顺序重绘
|
||||||
|
graph.paint();
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.on('edge:mouseleave', ev => {
|
||||||
|
// 获得图上所有边实例
|
||||||
|
const edges = graph.getEdges();
|
||||||
|
// 遍历边,将所有边的层级放置在后方,以恢复原样
|
||||||
|
edges.forEach(edge => {
|
||||||
|
edge.toBack();
|
||||||
|
});
|
||||||
|
// 注意:必须调用以根据新的层级顺序重绘
|
||||||
|
graph.paint();
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.on('node:mouseenter', ev => {
|
||||||
|
// 获得鼠标当前目标节点
|
||||||
|
const node = ev.item;
|
||||||
|
// 获取该节点的所有相关边
|
||||||
|
const edges = node.getEdges();
|
||||||
|
// 遍历相关边,将所有相关边提前,再将相关边的两个端点提前,以保证相关边的端点在边的上方常规效果
|
||||||
|
edges.forEach(edge => {
|
||||||
|
edge.toFront();
|
||||||
|
edge.getSource().toFront();
|
||||||
|
edge.getTarget().toFront();
|
||||||
|
});
|
||||||
|
// 注意:必须调用以根据新的层级顺序重绘
|
||||||
|
graph.paint();
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.on('node:mouseleave', ev => {
|
||||||
|
// 获得图上所有边实例
|
||||||
|
const edges = graph.getEdges();
|
||||||
|
// 遍历边,将所有边的层级放置在后方,以恢复原样
|
||||||
|
edges.forEach(edge => {
|
||||||
|
edge.toBack();
|
||||||
|
});
|
||||||
|
// 注意:必须调用以根据新的层级顺序重绘
|
||||||
|
graph.paint();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
46
demos/layout-concentric.html
Normal file
46
demos/layout-concentric.html
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Concentric Layout</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="mountNode"></div>
|
||||||
|
<script src="../build/g6.js"></script>
|
||||||
|
<script src="./assets/jquery-3.2.1.min.js"></script>
|
||||||
|
<script>
|
||||||
|
const radius = 100;
|
||||||
|
const graphSize = [1000, 600];
|
||||||
|
const graph = new G6.Graph({
|
||||||
|
container: 'mountNode',
|
||||||
|
width: graphSize[0],
|
||||||
|
height: graphSize[1],
|
||||||
|
fitView: true,
|
||||||
|
modes: {
|
||||||
|
default: ['zoom-canvas', 'drag-canvas', 'drag-node']
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: 'concentric',
|
||||||
|
center: [500, 300],
|
||||||
|
maxLevelDiff: 0.5,
|
||||||
|
sortBy: 'degree'
|
||||||
|
},
|
||||||
|
animate: true,
|
||||||
|
defaultNode: {
|
||||||
|
size: [5, 5]
|
||||||
|
},
|
||||||
|
defaultEdge: {
|
||||||
|
size: 1,
|
||||||
|
color: '#e2e2e2'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$.getJSON('./assets/data/concentric-data.json', data1 => {
|
||||||
|
graph.data(data1);
|
||||||
|
graph.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
118
demos/layout-dagre-add-node.html
Normal file
118
demos/layout-dagre-add-node.html
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>addItem on Dagre Layout and Update layout</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="description">Dagre 布局,点击节点新增一个节点,点击空白区域重新布局</div>
|
||||||
|
<div id="mountNode"></div>
|
||||||
|
<script src="../build/g6.js"></script>
|
||||||
|
<script>
|
||||||
|
const data = {
|
||||||
|
"nodes": [{
|
||||||
|
"id": "0",
|
||||||
|
"label": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"label": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"label": "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"label": "3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"label": "4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"label": "5"
|
||||||
|
}],
|
||||||
|
"edges": [{
|
||||||
|
"source": "0",
|
||||||
|
"target": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "0",
|
||||||
|
"target": "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "1",
|
||||||
|
"target": "3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "1",
|
||||||
|
"target": "4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "2",
|
||||||
|
"target": "5"
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
const graph = new G6.Graph({
|
||||||
|
container: 'mountNode',
|
||||||
|
width: 1000,
|
||||||
|
height: 600,
|
||||||
|
modes: {
|
||||||
|
default: ['drag-canvas', 'drag-node'],
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: 'dagre',
|
||||||
|
center: [500, 300],
|
||||||
|
nodeSize: [40, 20],
|
||||||
|
nodesep: 50,
|
||||||
|
ranksep: 50,
|
||||||
|
rankdir: 'TB',
|
||||||
|
},
|
||||||
|
animate: true,
|
||||||
|
defaultNode: {
|
||||||
|
size: [40, 20],
|
||||||
|
color: 'steelblue',
|
||||||
|
shape: 'rect',
|
||||||
|
style: {
|
||||||
|
lineWidth: 2,
|
||||||
|
fill: '#fff'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultEdge: {
|
||||||
|
size: 1,
|
||||||
|
color: '#e2e2e2',
|
||||||
|
style: {
|
||||||
|
endArrow: {
|
||||||
|
path: 'M 4,0 L -4,-4 L -4,4 Z',
|
||||||
|
d: 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
graph.data(data);
|
||||||
|
graph.render();
|
||||||
|
|
||||||
|
graph.on('node:click', evt => {
|
||||||
|
graph.addItem('node', {
|
||||||
|
id: 'newnode',
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
style: {
|
||||||
|
fill: 'red'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.on('canvas:click', evt => {
|
||||||
|
graph.updateLayout({
|
||||||
|
ranksep: 10,
|
||||||
|
rankdir: 'LR'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Fruchterman Layout with Changing Configurations</title>
|
<title>Dagre Layout with Changing Configurations</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -230,7 +230,7 @@
|
|||||||
|
|
||||||
const graph = new G6.Graph({
|
const graph = new G6.Graph({
|
||||||
container: 'mountNode',
|
container: 'mountNode',
|
||||||
width: 500,
|
width: 1000,
|
||||||
height: 500,
|
height: 500,
|
||||||
layout: {
|
layout: {
|
||||||
type: 'dagre',
|
type: 'dagre',
|
||||||
|
3591
demos/layout-grid.html
Normal file
3591
demos/layout-grid.html
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,8 @@
|
|||||||
<script src="../build/g6.js"></script>
|
<script src="../build/g6.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const data = {
|
const data = {
|
||||||
"nodes": [{
|
"nodes": [
|
||||||
|
{
|
||||||
"id": "0",
|
"id": "0",
|
||||||
"label": "0"
|
"label": "0"
|
||||||
},
|
},
|
||||||
@ -396,6 +397,7 @@
|
|||||||
container: 'mountNode',
|
container: 'mountNode',
|
||||||
width: 1000,
|
width: 1000,
|
||||||
height: 600,
|
height: 600,
|
||||||
|
fitView: true,
|
||||||
layout: {
|
layout: {
|
||||||
type: 'mds',
|
type: 'mds',
|
||||||
linkDistance: 100
|
linkDistance: 100
|
||||||
|
@ -3888,7 +3888,7 @@
|
|||||||
const maxDegree = 4;
|
const maxDegree = 4;
|
||||||
// the max degree about foces(clicked) node in the original data
|
// the max degree about foces(clicked) node in the original data
|
||||||
const oMaxDegree = 3;
|
const oMaxDegree = 3;
|
||||||
const unitRadius = 40;
|
const unitRadius = 100;
|
||||||
const focusNodeId = "2";
|
const focusNodeId = "2";
|
||||||
// re-place the clicked node far away the exisiting items
|
// re-place the clicked node far away the exisiting items
|
||||||
// along the radius from center node to it
|
// along the radius from center node to it
|
||||||
@ -3905,7 +3905,9 @@
|
|||||||
focusNode: "2",
|
focusNode: "2",
|
||||||
unitRadius,
|
unitRadius,
|
||||||
linkDistance: 180,
|
linkDistance: 180,
|
||||||
preventOverlap: true
|
preventOverlap: true,
|
||||||
|
strictRadial: false,
|
||||||
|
nodeSize: 50
|
||||||
});
|
});
|
||||||
subRadialLayout.init({
|
subRadialLayout.init({
|
||||||
'nodes': newNodeModels,
|
'nodes': newNodeModels,
|
||||||
@ -3921,7 +3923,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const focusNode = data_m.nodes[20];
|
const focusNode = data_m.nodes[20];
|
||||||
const mainUnitRadius = 120;
|
const mainUnitRadius = 220;
|
||||||
const graph = new G6.Graph({
|
const graph = new G6.Graph({
|
||||||
container: 'mountNode',
|
container: 'mountNode',
|
||||||
width: 1000,
|
width: 1000,
|
||||||
@ -3932,16 +3934,21 @@
|
|||||||
maxIteration: 200,
|
maxIteration: 200,
|
||||||
focusNode,
|
focusNode,
|
||||||
unitRadius: mainUnitRadius,
|
unitRadius: mainUnitRadius,
|
||||||
linkDistance: 100,
|
linkDistance: 200,
|
||||||
preventOverlap: true,
|
preventOverlap: true,
|
||||||
nodeSize: 20
|
nodeSize: 50,
|
||||||
|
strictRadial: true,
|
||||||
|
maxPreventOverlapIteration: 500
|
||||||
},
|
},
|
||||||
animate: true,
|
animate: true,
|
||||||
modes: {
|
modes: {
|
||||||
default: ['drag-node', 'click-select', 'click-add-node', 'drag-canvas']
|
default: ['drag-node', 'click-select', 'click-add-node', 'drag-canvas']
|
||||||
},
|
},
|
||||||
defaultNode: {
|
defaultNode: {
|
||||||
size: [20, 20]
|
size: 50,
|
||||||
|
style: {
|
||||||
|
stroke: '#000'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
defaultEdge: {
|
defaultEdge: {
|
||||||
size: 1,
|
size: 1,
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
size: [40, 40]
|
size: [40, 40]
|
||||||
},
|
},
|
||||||
modes: {
|
modes: {
|
||||||
default: [ 'brush-select', 'drag-node' ]
|
default: [ 'brush-select', 'drag-canvas', 'zoom-canvas' ]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -137,8 +137,9 @@
|
|||||||
|
|
||||||
const menu = document.getElementById('contextMenu')
|
const menu = document.getElementById('contextMenu')
|
||||||
graph.on('node:contextmenu', evt => {
|
graph.on('node:contextmenu', evt => {
|
||||||
menu.style.left = evt.x + 'px'
|
console.log(evt);
|
||||||
menu.style.top = evt.y + 'px'
|
menu.style.left = evt.clientX + 'px'
|
||||||
|
menu.style.top = evt.clientY + 'px'
|
||||||
})
|
})
|
||||||
|
|
||||||
graph.on('node:mouseleave', evt => {
|
graph.on('node:mouseleave', evt => {
|
||||||
|
@ -18,20 +18,9 @@ class LayoutController {
|
|||||||
let layoutType = self.layoutType;
|
let layoutType = self.layoutType;
|
||||||
const graph = self.graph;
|
const graph = self.graph;
|
||||||
// const data = graph.get('data');
|
// const data = graph.get('data');
|
||||||
const nodes = [];
|
|
||||||
const edges = [];
|
self.data = self.setDataFromGraph();
|
||||||
const nodeItems = graph.getNodes();
|
const nodes = self.data.nodes;
|
||||||
const edgeItems = graph.getEdges();
|
|
||||||
nodeItems.forEach(nodeItem => {
|
|
||||||
const model = nodeItem.getModel();
|
|
||||||
nodes.push(model);
|
|
||||||
});
|
|
||||||
edgeItems.forEach(edgeItem => {
|
|
||||||
const model = edgeItem.getModel();
|
|
||||||
edges.push(model);
|
|
||||||
});
|
|
||||||
const data = { nodes, edges };
|
|
||||||
self.data = data;
|
|
||||||
|
|
||||||
if (!nodes) {
|
if (!nodes) {
|
||||||
return;
|
return;
|
||||||
@ -84,7 +73,7 @@ class LayoutController {
|
|||||||
console.warn('The layout method: ' + layoutCfg + ' does not exist! Please specify it first.');
|
console.warn('The layout method: ' + layoutCfg + ' does not exist! Please specify it first.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
layoutMethod.init(data);
|
layoutMethod.init(self.data);
|
||||||
graph.emit('beforelayout');
|
graph.emit('beforelayout');
|
||||||
layoutMethod.execute();
|
layoutMethod.execute();
|
||||||
if (layoutType !== 'force') {
|
if (layoutType !== 'force') {
|
||||||
@ -110,6 +99,8 @@ class LayoutController {
|
|||||||
const graph = self.graph;
|
const graph = self.graph;
|
||||||
self.layoutType = cfg.type;
|
self.layoutType = cfg.type;
|
||||||
const layoutMethod = self.layoutMethod;
|
const layoutMethod = self.layoutMethod;
|
||||||
|
self.data = self.setDataFromGraph();
|
||||||
|
layoutMethod.init(self.data);
|
||||||
layoutMethod.updateCfg(cfg);
|
layoutMethod.updateCfg(cfg);
|
||||||
graph.emit('beforelayout');
|
graph.emit('beforelayout');
|
||||||
layoutMethod.execute();
|
layoutMethod.execute();
|
||||||
@ -139,6 +130,34 @@ class LayoutController {
|
|||||||
self.layout();
|
self.layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从 this.graph 获取数据
|
||||||
|
setDataFromGraph() {
|
||||||
|
const self = this;
|
||||||
|
const nodes = [];
|
||||||
|
const edges = [];
|
||||||
|
const nodeItems = self.graph.getNodes();
|
||||||
|
const edgeItems = self.graph.getEdges();
|
||||||
|
nodeItems.forEach(nodeItem => {
|
||||||
|
const model = nodeItem.getModel();
|
||||||
|
nodes.push(model);
|
||||||
|
});
|
||||||
|
edgeItems.forEach(edgeItem => {
|
||||||
|
const model = edgeItem.getModel();
|
||||||
|
edges.push(model);
|
||||||
|
});
|
||||||
|
const data = { nodes, edges };
|
||||||
|
if (self.layoutType === 'fruchtermanGroup') {
|
||||||
|
// const groupsData = self.graph.get('groups');
|
||||||
|
// const customGroup = self.graph.get('customGroup');
|
||||||
|
// const groupController = self.graph.get('customGroupControll');
|
||||||
|
// data.groupsData = groupsData;
|
||||||
|
// data.customGroup = customGroup;
|
||||||
|
// data.groupController = groupController;
|
||||||
|
data.graph = self.graph;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
// 重新布局
|
// 重新布局
|
||||||
relayout() {
|
relayout() {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
@ -512,10 +512,6 @@ class Graph extends EventEmitter {
|
|||||||
Util.each(data.edges, edge => {
|
Util.each(data.edges, edge => {
|
||||||
self.add(EDGE, edge);
|
self.add(EDGE, edge);
|
||||||
});
|
});
|
||||||
// layout
|
|
||||||
const layoutController = self.get('layoutController');
|
|
||||||
layoutController.layout();
|
|
||||||
self.refreshPositions();
|
|
||||||
|
|
||||||
// 防止传入的数据不存在nodes
|
// 防止传入的数据不存在nodes
|
||||||
if (data.nodes) {
|
if (data.nodes) {
|
||||||
@ -530,6 +526,29 @@ class Graph extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.get('groupByTypes')) {
|
||||||
|
// 为提升性能,选择数量少的进行操作
|
||||||
|
if (data.nodes.length < data.edges.length) {
|
||||||
|
const nodes = this.getNodes();
|
||||||
|
// 遍历节点实例,将所有节点提前。
|
||||||
|
nodes.forEach(node => {
|
||||||
|
node.toFront();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const edges = this.getEdges();
|
||||||
|
// 遍历节点实例,将所有节点提前。
|
||||||
|
edges.forEach(edge => {
|
||||||
|
edge.toBack();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// layout
|
||||||
|
const layoutController = self.get('layoutController');
|
||||||
|
layoutController.layout();
|
||||||
|
self.refreshPositions();
|
||||||
|
|
||||||
if (self.get('fitView')) {
|
if (self.get('fitView')) {
|
||||||
self.get('viewController')._fitView();
|
self.get('viewController')._fitView();
|
||||||
}
|
}
|
||||||
|
177
src/layout/concentric.js
Normal file
177
src/layout/concentric.js
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
/**
|
||||||
|
* @fileOverview concentric layout
|
||||||
|
* @author shiwu.wyy@antfin.com
|
||||||
|
* this algorithm refers to <cytoscape.js> - https://github.com/cytoscape/cytoscape.js/
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Layout = require('./layout');
|
||||||
|
const isString = require('@antv/util/lib/type/is-string');
|
||||||
|
|
||||||
|
function getDegree(n, nodeIdxMap, edges) {
|
||||||
|
const degrees = [];
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
degrees[i] = 0;
|
||||||
|
}
|
||||||
|
edges.forEach(e => {
|
||||||
|
degrees[nodeIdxMap.get(e.source)] += 1;
|
||||||
|
degrees[nodeIdxMap.get(e.target)] += 1;
|
||||||
|
});
|
||||||
|
return degrees;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 同心圆布局
|
||||||
|
*/
|
||||||
|
Layout.registerLayout('concentric', {
|
||||||
|
getDefaultCfg() {
|
||||||
|
return {
|
||||||
|
center: [ 0, 0 ], // 布局中心
|
||||||
|
nodeSize: 30,
|
||||||
|
minNodeSpacing: 10, // min spacing between outside of nodes (used for radius adjustment)
|
||||||
|
preventOverlap: false, // prevents node overlap, may overflow boundingBox if not enough space
|
||||||
|
sweep: undefined, // how many radians should be between the first and last node (defaults to full circle)
|
||||||
|
equidistant: false, // whether levels have an equal radial distance betwen them, may cause bounding box overflow
|
||||||
|
startAngle: 3 / 2 * Math.PI, // where nodes start in radians
|
||||||
|
clockwise: true, // whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false)
|
||||||
|
maxLevelDiff: undefined, // the letiation of concentric values in each level
|
||||||
|
sortBy: 'degree' // 根据 sortBy 指定的属性进行排布,数值高的放在中心。如果是 sortBy 则会计算节点度数,度数最高的放在中心。
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 执行布局
|
||||||
|
*/
|
||||||
|
execute() {
|
||||||
|
const self = this;
|
||||||
|
const nodes = self.nodes;
|
||||||
|
const edges = self.edges;
|
||||||
|
const n = nodes.length;
|
||||||
|
const center = self.center;
|
||||||
|
if (n === 0) {
|
||||||
|
return;
|
||||||
|
} else if (n === 1) {
|
||||||
|
nodes[0].x = center[0];
|
||||||
|
nodes[0].y = center[1];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const layoutNodes = [];
|
||||||
|
let maxNodeSize;
|
||||||
|
if (isNaN(self.nodeSize)) {
|
||||||
|
maxNodeSize = Math.max(self.nodeSize[0], self.nodeSize[1]);
|
||||||
|
} else {
|
||||||
|
maxNodeSize = self.nodeSize;
|
||||||
|
}
|
||||||
|
nodes.forEach(node => {
|
||||||
|
layoutNodes.push(node);
|
||||||
|
let nodeSize;
|
||||||
|
if (isNaN(node.size)) {
|
||||||
|
nodeSize = Math.max(node.size[0], node.size[1]);
|
||||||
|
} else {
|
||||||
|
nodeSize = node.size;
|
||||||
|
}
|
||||||
|
maxNodeSize = Math.max(maxNodeSize, nodeSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.width = self.width || window.innerHeight;
|
||||||
|
self.height = self.height || window.innerWidth;
|
||||||
|
self.clockwise = self.counterclockwise !== undefined ? !self.counterclockwise : self.clockwise;
|
||||||
|
|
||||||
|
// layout
|
||||||
|
const nodeMap = new Map();
|
||||||
|
const nodeIdxMap = new Map();
|
||||||
|
layoutNodes.forEach((node, i) => {
|
||||||
|
nodeMap.set(node.id, node);
|
||||||
|
nodeIdxMap.set(node.id, i);
|
||||||
|
});
|
||||||
|
self.nodeMap = nodeMap;
|
||||||
|
|
||||||
|
// get the node degrees
|
||||||
|
if (self.sortBy === 'degree' || !isString(self.sortBy) || layoutNodes[0][self.sortBy] === undefined) {
|
||||||
|
self.sortBy = 'degree';
|
||||||
|
if (isNaN(nodes[0].degree)) {
|
||||||
|
const values = getDegree(nodes.length, nodeIdxMap, edges);
|
||||||
|
layoutNodes.forEach((node, i) => {
|
||||||
|
node.degree = values[i];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// sort nodes by value
|
||||||
|
layoutNodes.sort((n1, n2) => {
|
||||||
|
return n2[self.sortBy] - n1[self.sortBy];
|
||||||
|
});
|
||||||
|
|
||||||
|
self.maxValueNode = layoutNodes[0];
|
||||||
|
|
||||||
|
self.maxLevelDiff = self.maxLevelDiff || self.maxValueNode[self.sortBy] / 4; // 0.5;
|
||||||
|
|
||||||
|
// put the values into levels
|
||||||
|
const levels = [[]];
|
||||||
|
let currentLevel = levels[0];
|
||||||
|
layoutNodes.forEach(node => {
|
||||||
|
if (currentLevel.length > 0) {
|
||||||
|
const diff = Math.abs(currentLevel[0][self.sortBy] - node[self.sortBy]);
|
||||||
|
if (diff >= self.maxLevelDiff) {
|
||||||
|
currentLevel = [];
|
||||||
|
levels.push(currentLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentLevel.push(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
// create positions for levels
|
||||||
|
let minDist = maxNodeSize + self.minNodeSpacing; // min dist between nodes
|
||||||
|
if (!self.preventOverlap) { // then strictly constrain to bb
|
||||||
|
const firstLvlHasMulti = levels.length > 0 && levels[0].length > 1;
|
||||||
|
const maxR = (Math.min(self.width, self.height) / 2 - minDist);
|
||||||
|
const rStep = maxR / (levels.length + firstLvlHasMulti ? 1 : 0);
|
||||||
|
|
||||||
|
minDist = Math.min(minDist, rStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the metrics for each level
|
||||||
|
let r = 0;
|
||||||
|
levels.forEach(level => {
|
||||||
|
const sweep = self.sweep === undefined ? 2 * Math.PI - 2 * Math.PI / level.length : self.sweep;
|
||||||
|
const dTheta = level.dTheta = sweep / (Math.max(1, level.length - 1));
|
||||||
|
|
||||||
|
// calculate the radius
|
||||||
|
if (level.length > 1 && self.preventOverlap) { // but only if more than one node (can't overlap)
|
||||||
|
const dcos = Math.cos(dTheta) - Math.cos(0);
|
||||||
|
const dsin = Math.sin(dTheta) - Math.sin(0);
|
||||||
|
const rMin = Math.sqrt(minDist * minDist / (dcos * dcos + dsin * dsin)); // s.t. no nodes overlapping
|
||||||
|
|
||||||
|
r = Math.max(rMin, r);
|
||||||
|
}
|
||||||
|
level.r = r;
|
||||||
|
r += minDist;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (self.equidistant) {
|
||||||
|
let rDeltaMax = 0;
|
||||||
|
let r = 0;
|
||||||
|
for (let i = 0; i < levels.length; i++) {
|
||||||
|
const level = levels[ i ];
|
||||||
|
const rDelta = level.r - r;
|
||||||
|
rDeltaMax = Math.max(rDeltaMax, rDelta);
|
||||||
|
}
|
||||||
|
r = 0;
|
||||||
|
levels.forEach((level, i) => {
|
||||||
|
if (i === 0) {
|
||||||
|
r = level.r;
|
||||||
|
}
|
||||||
|
level.r = r;
|
||||||
|
r += rDeltaMax;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate the node positions
|
||||||
|
levels.forEach(level => {
|
||||||
|
const dTheta = level.dTheta;
|
||||||
|
const r = level.r;
|
||||||
|
level.forEach((node, j) => {
|
||||||
|
const theta = self.startAngle + (self.clockwise ? 1 : -1) * dTheta * j;
|
||||||
|
node.x = center[0] + r * Math.cos(theta);
|
||||||
|
node.y = center[1] + r * Math.sin(theta);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
443
src/layout/fruchterman-group.js
Normal file
443
src/layout/fruchterman-group.js
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
/**
|
||||||
|
* @fileOverview random layout
|
||||||
|
* @author shiwu.wyy@antfin.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
const d3Force = require('d3-force');
|
||||||
|
const Layout = require('./layout');
|
||||||
|
|
||||||
|
const SPEED_DIVISOR = 800;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fruchterman 布局
|
||||||
|
*/
|
||||||
|
Layout.registerLayout('fruchtermanGroup', {
|
||||||
|
getDefaultCfg() {
|
||||||
|
return {
|
||||||
|
maxIteration: 1000, // 停止迭代的最大迭代数
|
||||||
|
center: [ 0, 0 ], // 布局中心
|
||||||
|
gravity: 1, // 重力大小,影响图的紧凑程度
|
||||||
|
speed: 1, // 速度
|
||||||
|
groupGravity: 1, // 聚类力大小
|
||||||
|
nodeRepulsiveCoefficient: 50,
|
||||||
|
groupRepulsiveCoefficient: 10,
|
||||||
|
nodeAttractiveCoefficient: 1,
|
||||||
|
groupAttractiveCoefficient: 1,
|
||||||
|
preventGroupOverlap: true,
|
||||||
|
groupCollideStrength: 0.7 // 防止重叠的力强度
|
||||||
|
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 初始化
|
||||||
|
* @param {object} data 数据
|
||||||
|
*/
|
||||||
|
init(data) {
|
||||||
|
const self = this;
|
||||||
|
self.nodes = data.nodes;
|
||||||
|
self.edges = data.edges;
|
||||||
|
self.graph = data.graph;
|
||||||
|
self.groupsData = self.graph.get('groups'); // group data
|
||||||
|
self.customGroup = self.graph.get('customGroup'); // shape group
|
||||||
|
self.groupController = self.graph.get('customGroupControll'); // controller
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 执行布局
|
||||||
|
*/
|
||||||
|
execute() {
|
||||||
|
const self = this;
|
||||||
|
const nodes = self.nodes;
|
||||||
|
const center = self.center;
|
||||||
|
|
||||||
|
if (nodes.length === 0) {
|
||||||
|
return;
|
||||||
|
} else if (nodes.length === 1) {
|
||||||
|
nodes[0].x = center[0];
|
||||||
|
nodes[0].y = center[1];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nodeMap = new Map();
|
||||||
|
const nodeIndexMap = new Map();
|
||||||
|
nodes.forEach((node, i) => {
|
||||||
|
nodeMap.set(node.id, node);
|
||||||
|
nodeIndexMap.set(node.id, i);
|
||||||
|
});
|
||||||
|
self.nodeMap = nodeMap;
|
||||||
|
self.nodeIndexMap = nodeIndexMap;
|
||||||
|
// layout
|
||||||
|
self.run();
|
||||||
|
|
||||||
|
self.graph.refreshPositions();
|
||||||
|
|
||||||
|
// refresh groups' positions
|
||||||
|
const customGroup = self.customGroup;
|
||||||
|
const groupItems = customGroup.get('children');
|
||||||
|
const groupController = self.groupController;
|
||||||
|
const groupType = self.graph.get('groupType');
|
||||||
|
|
||||||
|
groupItems.forEach(gItem => {
|
||||||
|
const gid = gItem.get('id');
|
||||||
|
const group = self.groupMap.get(gid);
|
||||||
|
group.item = gItem;
|
||||||
|
|
||||||
|
const paddingValue = groupController.getGroupPadding(gid);
|
||||||
|
|
||||||
|
const { x: x1, y: y1, width, height } = groupController.calculationGroupPosition(group.nodeIds);
|
||||||
|
const groupTitleShape = gItem.findByClassName('group-title');
|
||||||
|
const gItemKeyShape = gItem.get('children')[0];
|
||||||
|
|
||||||
|
let titleX = 0;
|
||||||
|
let titleY = 0;
|
||||||
|
if (groupType === 'circle') {
|
||||||
|
const r = width > height ? width / 2 : height / 2;
|
||||||
|
const x = (width + 2 * x1) / 2;
|
||||||
|
const y = (height + 2 * y1) / 2;
|
||||||
|
gItemKeyShape.attr({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
r: r + paddingValue
|
||||||
|
});
|
||||||
|
group.x = x;
|
||||||
|
group.y = y;
|
||||||
|
group.size = (r + paddingValue) * 2;
|
||||||
|
titleX = x;
|
||||||
|
titleY = y - r - paddingValue;
|
||||||
|
} else if (groupType === 'rect') {
|
||||||
|
const { default: defaultStyle } = groupController.styles;
|
||||||
|
const rectPadding = paddingValue * defaultStyle.disCoefficient;
|
||||||
|
const rectWidth = width + rectPadding * 2;
|
||||||
|
const rectHeight = height + rectPadding * 2;
|
||||||
|
const x = x1 - rectPadding;
|
||||||
|
const y = y1 - rectPadding;
|
||||||
|
gItemKeyShape.attr({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width: rectWidth,
|
||||||
|
height: rectHeight
|
||||||
|
});
|
||||||
|
group.x = x;
|
||||||
|
group.y = y;
|
||||||
|
group.size = [ rectWidth, rectHeight ];
|
||||||
|
titleX = x1;
|
||||||
|
titleY = y1;// - rectHeight / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupTitleShape) {
|
||||||
|
const titleConfig = group.groupData.title;
|
||||||
|
let offsetX = 0;
|
||||||
|
let offsetY = 0;
|
||||||
|
if (titleConfig) {
|
||||||
|
offsetX = titleConfig.offsetox || 0;
|
||||||
|
offsetY = titleConfig.offsetoy || 0;
|
||||||
|
titleConfig.offsetX = offsetX;
|
||||||
|
titleConfig.offsetY = offsetY;
|
||||||
|
if (groupType === 'rect') {
|
||||||
|
titleConfig.offsetX = 0;
|
||||||
|
titleConfig.offsetY = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let x = titleX + offsetX;
|
||||||
|
let y = titleY + offsetY;
|
||||||
|
if (groupType === 'rect') {
|
||||||
|
x = titleX;
|
||||||
|
y = titleY;
|
||||||
|
}
|
||||||
|
groupTitleShape.attr({ x, y });
|
||||||
|
group.titlePos = [ x, y ];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// // find the levels of groups
|
||||||
|
// const roots = [];
|
||||||
|
// const groupMarks = {};
|
||||||
|
// self.groupsData.forEach(gd => {
|
||||||
|
// const group = self.groupMap.get(gd.id);
|
||||||
|
// if (!gd.parentId) {
|
||||||
|
// const groupNodes = [];
|
||||||
|
// group.nodeIds.forEach(nid => {
|
||||||
|
// groupNodes.push(nodeMap.get(nid));
|
||||||
|
// });
|
||||||
|
// roots.push({
|
||||||
|
// id: gd.id,
|
||||||
|
// children: [],
|
||||||
|
// x: group.cx,
|
||||||
|
// y: group.cy,
|
||||||
|
// ox: group.cx,
|
||||||
|
// oy: group.cy,
|
||||||
|
// nodes: groupNodes,
|
||||||
|
// item: group.item,
|
||||||
|
// size: group.size
|
||||||
|
// });
|
||||||
|
// groupMarks[gd.id] = 1;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// const graphWidth = self.graph.get('width');
|
||||||
|
// const graphHeight = self.graph.get('height');
|
||||||
|
// self.BFSDivide(graphWidth, graphHeight, roots);
|
||||||
|
|
||||||
|
// according to group's size to divide the canvas
|
||||||
|
|
||||||
|
self.graph.paint();
|
||||||
|
},
|
||||||
|
run() {
|
||||||
|
const self = this;
|
||||||
|
const nodes = self.nodes;
|
||||||
|
const groups = self.groupsData;
|
||||||
|
const edges = self.edges;
|
||||||
|
const maxIteration = self.maxIteration;
|
||||||
|
const width = self.width || window.innerHeight;
|
||||||
|
const height = self.height || window.innerWidth;
|
||||||
|
const center = self.center;
|
||||||
|
const nodeMap = self.nodeMap;
|
||||||
|
const nodeIndexMap = self.nodeIndexMap;
|
||||||
|
const maxDisplace = width / 10;
|
||||||
|
const k = Math.sqrt(width * height / (nodes.length + 1));
|
||||||
|
const gravity = self.gravity;
|
||||||
|
const speed = self.speed;
|
||||||
|
const groupMap = new Map();
|
||||||
|
self.groupMap = groupMap;
|
||||||
|
nodes.forEach(n => {
|
||||||
|
if (groupMap.get(n.groupId) === undefined) {
|
||||||
|
let parentId;
|
||||||
|
let groupData;
|
||||||
|
groups.forEach(g => {
|
||||||
|
if (g.id === n.groupId) {
|
||||||
|
parentId = g.parentId;
|
||||||
|
groupData = g;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const group = {
|
||||||
|
name: n.groupId,
|
||||||
|
cx: 0,
|
||||||
|
cy: 0,
|
||||||
|
count: 0,
|
||||||
|
parentId,
|
||||||
|
nodeIds: [],
|
||||||
|
groupData
|
||||||
|
};
|
||||||
|
groupMap.set(n.groupId, group);
|
||||||
|
}
|
||||||
|
const c = groupMap.get(n.groupId);
|
||||||
|
c.nodeIds.push(n.id);
|
||||||
|
c.cx += n.x;
|
||||||
|
c.cy += n.y;
|
||||||
|
c.count++;
|
||||||
|
});
|
||||||
|
|
||||||
|
groupMap.forEach(c => {
|
||||||
|
c.cx /= c.count;
|
||||||
|
c.cy /= c.count;
|
||||||
|
});
|
||||||
|
|
||||||
|
self.DFSSetGroups();
|
||||||
|
|
||||||
|
for (let i = 0; i < maxIteration; i++) {
|
||||||
|
const disp = [];
|
||||||
|
nodes.forEach((n, i) => {
|
||||||
|
disp[i] = { x: 0, y: 0 };
|
||||||
|
});
|
||||||
|
self.getDisp(nodes, edges, nodeMap, nodeIndexMap, disp, k);
|
||||||
|
|
||||||
|
// gravity for one group
|
||||||
|
const groupGravity = self.groupGravity || gravity;
|
||||||
|
nodes.forEach((n, i) => {
|
||||||
|
const c = groupMap.get(n.groupId);
|
||||||
|
const distLength = Math.sqrt((n.x - c.cx) * (n.x - c.cx) + (n.y - c.cy) * (n.y - c.cy));
|
||||||
|
const gravityForce = self.groupAttractiveCoefficient * k * groupGravity;
|
||||||
|
disp[i].x -= gravityForce * (n.x - c.cx) / distLength;
|
||||||
|
disp[i].y -= gravityForce * (n.y - c.cy) / distLength;
|
||||||
|
});
|
||||||
|
|
||||||
|
groupMap.forEach(c => {
|
||||||
|
c.cx = 0;
|
||||||
|
c.cy = 0;
|
||||||
|
c.count = 0;
|
||||||
|
});
|
||||||
|
nodes.forEach(n => {
|
||||||
|
const c = groupMap.get(n.groupId);
|
||||||
|
c.cx += n.x;
|
||||||
|
c.cy += n.y;
|
||||||
|
c.count++;
|
||||||
|
});
|
||||||
|
groupMap.forEach(c => {
|
||||||
|
c.cx /= c.count;
|
||||||
|
c.cy /= c.count;
|
||||||
|
});
|
||||||
|
|
||||||
|
// gravity
|
||||||
|
nodes.forEach((n, i) => {
|
||||||
|
const gravityForce = 0.01 * k * gravity;
|
||||||
|
disp[i].x -= gravityForce * (n.x - center[0]);
|
||||||
|
disp[i].y -= gravityForce * (n.y - center[1]);
|
||||||
|
});
|
||||||
|
// speed
|
||||||
|
nodes.forEach((n, i) => {
|
||||||
|
disp[i].dx *= speed / SPEED_DIVISOR;
|
||||||
|
disp[i].dy *= speed / SPEED_DIVISOR;
|
||||||
|
});
|
||||||
|
|
||||||
|
// move
|
||||||
|
nodes.forEach((n, i) => {
|
||||||
|
const distLength = Math.sqrt(disp[i].x * disp[i].x + disp[i].y * disp[i].y);
|
||||||
|
if (distLength > 0) { // && !n.isFixed()
|
||||||
|
const limitedDist = Math.min(maxDisplace * (speed / SPEED_DIVISOR), distLength);
|
||||||
|
n.x += disp[i].x / distLength * limitedDist;
|
||||||
|
n.y += disp[i].y / distLength * limitedDist;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getDisp(nodes, edges, nodeMap, nodeIndexMap, disp, k) {
|
||||||
|
const self = this;
|
||||||
|
self.calRepulsive(nodes, disp, k);
|
||||||
|
self.calAttractive(edges, nodeMap, nodeIndexMap, disp, k);
|
||||||
|
self.calGroupRepulsive(disp, k);
|
||||||
|
},
|
||||||
|
calRepulsive(nodes, disp, k) {
|
||||||
|
const self = this;
|
||||||
|
nodes.forEach((v, i) => {
|
||||||
|
disp[i] = { x: 0, y: 0 };
|
||||||
|
nodes.forEach((u, j) => {
|
||||||
|
if (i === j) return;
|
||||||
|
const vecx = v.x - u.x;
|
||||||
|
const vecy = v.y - u.y;
|
||||||
|
let vecLengthSqr = vecx * vecx + vecy * vecy;
|
||||||
|
if (vecLengthSqr === 0) vecLengthSqr = 1;
|
||||||
|
const common = self.nodeRepulsiveCoefficient * (k * k) / vecLengthSqr;
|
||||||
|
disp[i].x += vecx * common;
|
||||||
|
disp[i].y += vecy * common;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
calAttractive(edges, nodeMap, nodeIndexMap, disp, k) {
|
||||||
|
const self = this;
|
||||||
|
edges.forEach(e => {
|
||||||
|
const uIndex = nodeIndexMap.get(e.source);
|
||||||
|
const vIndex = nodeIndexMap.get(e.target);
|
||||||
|
const u = nodeMap.get(e.source);
|
||||||
|
const v = nodeMap.get(e.target);
|
||||||
|
const vecx = v.x - u.x;
|
||||||
|
const vecy = v.y - u.y;
|
||||||
|
const vecLength = Math.sqrt(vecx * vecx + vecy * vecy);
|
||||||
|
const common = self.nodeAttractiveCoefficient * vecLength * vecLength / k;
|
||||||
|
disp[vIndex].x -= vecx / vecLength * common;
|
||||||
|
disp[vIndex].y -= vecy / vecLength * common;
|
||||||
|
disp[uIndex].x += vecx / vecLength * common;
|
||||||
|
disp[uIndex].y += vecy / vecLength * common;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
calGroupRepulsive(disp, k) {
|
||||||
|
const self = this;
|
||||||
|
const groupMap = self.groupMap;
|
||||||
|
const nodeIndexMap = self.nodeIndexMap;
|
||||||
|
groupMap.forEach((gv, i) => {
|
||||||
|
const gDisp = { x: 0, y: 0 };
|
||||||
|
groupMap.forEach((gu, j) => {
|
||||||
|
if (i === j) return;
|
||||||
|
const vecx = gv.cx - gu.cx;
|
||||||
|
const vecy = gv.cy - gu.cy;
|
||||||
|
let vecLengthSqr = vecx * vecx + vecy * vecy;
|
||||||
|
if (vecLengthSqr === 0) vecLengthSqr = 1;
|
||||||
|
const common = self.groupRepulsiveCoefficient * (k * k) / vecLengthSqr;
|
||||||
|
gDisp.x += vecx * common;
|
||||||
|
gDisp.y += vecy * common;
|
||||||
|
});
|
||||||
|
// apply group disp to the group's nodes
|
||||||
|
const groupNodeIds = gv.nodeIds;
|
||||||
|
groupNodeIds.forEach(gnid => {
|
||||||
|
const nodeIdx = nodeIndexMap.get(gnid);
|
||||||
|
disp[nodeIdx].x += gDisp.x;
|
||||||
|
disp[nodeIdx].y += gDisp.y;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
DFSSetGroups() {
|
||||||
|
const self = this;
|
||||||
|
const groupMap = self.groupMap;
|
||||||
|
groupMap.forEach(group => {
|
||||||
|
const parentGroupId = group.parentId;
|
||||||
|
|
||||||
|
if (parentGroupId) {
|
||||||
|
let parentParentId;
|
||||||
|
self.groupsData.forEach(g => {
|
||||||
|
if (g.id === group.groupId) {
|
||||||
|
parentParentId = g.parentId;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const parentGroup = groupMap.get(parentGroupId);
|
||||||
|
if (!parentGroup) {
|
||||||
|
const pgroup = {
|
||||||
|
name: parentGroupId,
|
||||||
|
cx: 0,
|
||||||
|
cy: 0,
|
||||||
|
count: 0,
|
||||||
|
parentId: parentParentId,
|
||||||
|
nodeIds: group.nodeIds
|
||||||
|
};
|
||||||
|
groupMap.set(parentGroupId, pgroup);
|
||||||
|
} else {
|
||||||
|
group.nodeIds.forEach(n => {
|
||||||
|
parentGroup.nodeIds.push(n);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
BFSDivide(width, height, children) {
|
||||||
|
const self = this;
|
||||||
|
const nodeForce = d3Force.forceManyBody();
|
||||||
|
nodeForce.strength(30);
|
||||||
|
|
||||||
|
const simulation = d3Force.forceSimulation()
|
||||||
|
.nodes(children)
|
||||||
|
.force('center', d3Force.forceCenter(width / 2, height / 2))
|
||||||
|
.force('charge', nodeForce)
|
||||||
|
.alpha(0.3)
|
||||||
|
.alphaDecay(0.01)
|
||||||
|
.alphaMin(0.001)
|
||||||
|
.on('tick', () => {
|
||||||
|
children.forEach(child => {
|
||||||
|
const groupNodes = child.nodes;
|
||||||
|
groupNodes.forEach(gn => {
|
||||||
|
gn.x += (child.x - child.ox);
|
||||||
|
gn.y += (child.y - child.oy);
|
||||||
|
});
|
||||||
|
child.ox = child.x;
|
||||||
|
child.oy = child.y;
|
||||||
|
const gItem = child.item;
|
||||||
|
const gItemKeyShape = gItem.get('children')[0];
|
||||||
|
gItemKeyShape.attr({
|
||||||
|
x: child.x,
|
||||||
|
y: child.y
|
||||||
|
});
|
||||||
|
});
|
||||||
|
self.graph.refreshPositions();
|
||||||
|
})
|
||||||
|
.on('end', () => {
|
||||||
|
});
|
||||||
|
self.groupOverlapProcess(simulation);
|
||||||
|
},
|
||||||
|
groupOverlapProcess(simulation) {
|
||||||
|
const self = this;
|
||||||
|
let nodeSize = self.nodeSize;
|
||||||
|
const groupCollideStrength = self.groupCollideStrength;
|
||||||
|
if (!nodeSize) {
|
||||||
|
nodeSize = d => {
|
||||||
|
if (d.size) {
|
||||||
|
if (Array.isArray(d.size)) {
|
||||||
|
return d.size[0] / 2;
|
||||||
|
}
|
||||||
|
return d.size / 2;
|
||||||
|
}
|
||||||
|
return 10;
|
||||||
|
};
|
||||||
|
} else if (!isNaN(nodeSize)) {
|
||||||
|
nodeSize /= 2;
|
||||||
|
} else if (nodeSize.length === 2) {
|
||||||
|
const larger = nodeSize[0] > nodeSize[1] ? nodeSize[0] : nodeSize[1];
|
||||||
|
nodeSize = larger / 2;
|
||||||
|
}
|
||||||
|
// forceCollide's parameter is a radius
|
||||||
|
simulation.force('collisionForce', d3Force.forceCollide(nodeSize).strength(groupCollideStrength));
|
||||||
|
}
|
||||||
|
});
|
286
src/layout/grid.js
Normal file
286
src/layout/grid.js
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
/**
|
||||||
|
* @fileOverview grid layout
|
||||||
|
* @author shiwu.wyy@antfin.com
|
||||||
|
* this algorithm refers to <cytoscape.js> - https://github.com/cytoscape/cytoscape.js/
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Layout = require('./layout');
|
||||||
|
const isString = require('@antv/util/lib/type/is-string');
|
||||||
|
|
||||||
|
|
||||||
|
function getDegree(n, nodeIdxMap, edges) {
|
||||||
|
const degrees = [];
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
degrees[i] = 0;
|
||||||
|
}
|
||||||
|
edges.forEach(e => {
|
||||||
|
degrees[nodeIdxMap.get(e.source)] += 1;
|
||||||
|
degrees[nodeIdxMap.get(e.target)] += 1;
|
||||||
|
});
|
||||||
|
return degrees;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网格布局
|
||||||
|
*/
|
||||||
|
Layout.registerLayout('grid', {
|
||||||
|
getDefaultCfg() {
|
||||||
|
return {
|
||||||
|
begin: [ 0, 0 ], // 布局起始点
|
||||||
|
preventOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
|
||||||
|
preventOverlapPadding: 10, // extra spacing around nodes when preventOverlap: true
|
||||||
|
condense: false, // uses all available space on false, uses minimal space on true
|
||||||
|
rows: undefined, // force num of rows in the grid
|
||||||
|
cols: undefined, // force num of columns in the grid
|
||||||
|
position() {}, // returns { row, col } for element
|
||||||
|
sortBy: 'degree', // a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
|
||||||
|
nodeSize: 30
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 执行布局
|
||||||
|
*/
|
||||||
|
execute() {
|
||||||
|
const self = this;
|
||||||
|
const nodes = self.nodes;
|
||||||
|
// const edges = self.edges;
|
||||||
|
const n = nodes.length;
|
||||||
|
const center = self.center;
|
||||||
|
if (n === 0) {
|
||||||
|
return;
|
||||||
|
} else if (n === 1) {
|
||||||
|
nodes[0].x = center[0];
|
||||||
|
nodes[0].y = center[1];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const edges = self.edges;
|
||||||
|
const layoutNodes = [];
|
||||||
|
nodes.forEach(node => {
|
||||||
|
layoutNodes.push(node);
|
||||||
|
});
|
||||||
|
const nodeIdxMap = new Map();
|
||||||
|
layoutNodes.forEach((node, i) => {
|
||||||
|
nodeIdxMap.set(node.id, i);
|
||||||
|
});
|
||||||
|
if (self.sortBy === 'degree' || !isString(self.sortBy) || layoutNodes[0][self.sortBy] === undefined) {
|
||||||
|
self.sortBy = 'degree';
|
||||||
|
if (isNaN(nodes[0].degree)) {
|
||||||
|
const values = getDegree(layoutNodes.length, nodeIdxMap, edges);
|
||||||
|
layoutNodes.forEach((node, i) => {
|
||||||
|
node.degree = values[i];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// sort nodes by value
|
||||||
|
layoutNodes.sort((n1, n2) => {
|
||||||
|
return n2[self.sortBy] - n1[self.sortBy];
|
||||||
|
});
|
||||||
|
|
||||||
|
self.width = self.width || window.innerHeight;
|
||||||
|
self.height = self.height || window.innerWidth;
|
||||||
|
// width/height * splits^2 = cells where splits is number of times to split width
|
||||||
|
self.cells = n;
|
||||||
|
self.splits = Math.sqrt(self.cells * self.height / self.width);
|
||||||
|
self.rows = Math.round(self.splits);
|
||||||
|
self.cols = Math.round(self.width / self.height * self.splits);
|
||||||
|
|
||||||
|
const oRows = self.rows;
|
||||||
|
const oCols = self.cols != null ? self.cols : self.columns;
|
||||||
|
|
||||||
|
// if rows or columns were set in self, use those values
|
||||||
|
if (oRows != null && oCols != null) {
|
||||||
|
self.rows = oRows;
|
||||||
|
self.cols = oCols;
|
||||||
|
} else if (oRows != null && oCols == null) {
|
||||||
|
self.rows = oRows;
|
||||||
|
self.cols = Math.ceil(self.cells / self.rows);
|
||||||
|
} else if (oRows == null && oCols != null) {
|
||||||
|
self.cols = oCols;
|
||||||
|
self.rows = Math.ceil(self.cells / self.cols);
|
||||||
|
} else if (self.cols * self.rows > self.cells) {
|
||||||
|
// otherwise use the automatic values and adjust accordingly
|
||||||
|
// if rounding was up, see if we can reduce rows or columns
|
||||||
|
const sm = self.small();
|
||||||
|
const lg = self.large();
|
||||||
|
|
||||||
|
// reducing the small side takes away the most cells, so try it first
|
||||||
|
if ((sm - 1) * lg >= self.cells) {
|
||||||
|
self.small(sm - 1);
|
||||||
|
} else if ((lg - 1) * sm >= self.cells) {
|
||||||
|
self.large(lg - 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// if rounding was too low, add rows or columns
|
||||||
|
while (self.cols * self.rows < self.cells) {
|
||||||
|
const sm = self.small();
|
||||||
|
const lg = self.large();
|
||||||
|
|
||||||
|
// try to add to larger side first (adds less in multiplication)
|
||||||
|
if ((lg + 1) * sm >= self.cells) {
|
||||||
|
self.large(lg + 1);
|
||||||
|
} else {
|
||||||
|
self.small(sm + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cellWidth = self.width / self.cols;
|
||||||
|
self.cellHeight = self.height / self.rows;
|
||||||
|
|
||||||
|
if (self.condense) {
|
||||||
|
self.cellWidth = 0;
|
||||||
|
self.cellHeight = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.preventOverlap) {
|
||||||
|
layoutNodes.forEach(node => {
|
||||||
|
if (node.x == null || node.y == null) { // for bb
|
||||||
|
node.x = 0;
|
||||||
|
node.y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nodew;
|
||||||
|
let nodeh;
|
||||||
|
if (isNaN(node.size)) {
|
||||||
|
nodew = node.size[0];
|
||||||
|
nodeh = node.size[1];
|
||||||
|
} else {
|
||||||
|
nodew = node.size;
|
||||||
|
nodeh = node.size;
|
||||||
|
}
|
||||||
|
if (isNaN(nodew) || isNaN(nodeh)) {
|
||||||
|
if (isNaN(self.nodeSize)) {
|
||||||
|
nodew = self.nodeSize[0];
|
||||||
|
nodeh = self.nodeSize[1];
|
||||||
|
} else {
|
||||||
|
nodew = self.nodeSize;
|
||||||
|
nodeh = self.nodeSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = self.preventOverlapPadding;
|
||||||
|
|
||||||
|
const w = nodew + p;
|
||||||
|
const h = nodeh + p;
|
||||||
|
|
||||||
|
self.cellWidth = Math.max(self.cellWidth, w);
|
||||||
|
self.cellHeight = Math.max(self.cellHeight, h);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cellUsed = {}; // e.g. 'c-0-2' => true
|
||||||
|
|
||||||
|
// to keep track of current cell position
|
||||||
|
self.row = 0;
|
||||||
|
self.col = 0;
|
||||||
|
|
||||||
|
// get a cache of all the manual positions
|
||||||
|
self.id2manPos = {};
|
||||||
|
for (let i = 0; i < layoutNodes.length; i++) {
|
||||||
|
const node = layoutNodes[i];
|
||||||
|
const rcPos = self.position(node);
|
||||||
|
|
||||||
|
if (rcPos && (rcPos.row !== undefined || rcPos.col !== undefined)) { // must have at least row or col def'd
|
||||||
|
const pos = {
|
||||||
|
row: rcPos.row,
|
||||||
|
col: rcPos.col
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pos.col === undefined) { // find unused col
|
||||||
|
pos.col = 0;
|
||||||
|
|
||||||
|
while (self.used(pos.row, pos.col)) {
|
||||||
|
pos.col++;
|
||||||
|
}
|
||||||
|
} else if (pos.row === undefined) { // find unused row
|
||||||
|
pos.row = 0;
|
||||||
|
|
||||||
|
while (self.used(pos.row, pos.col)) {
|
||||||
|
pos.row++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.id2manPos[ node.id ] = pos;
|
||||||
|
self.use(pos.row, pos.col);
|
||||||
|
}
|
||||||
|
self.getPos(node);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
small(val) {
|
||||||
|
const self = this;
|
||||||
|
let res;
|
||||||
|
if (val == null) {
|
||||||
|
res = Math.min(self.rows, self.cols);
|
||||||
|
} else {
|
||||||
|
const min = Math.min(self.rows, self.cols);
|
||||||
|
if (min === self.rows) {
|
||||||
|
self.rows = val;
|
||||||
|
} else {
|
||||||
|
self.cols = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
large(val) {
|
||||||
|
const self = this;
|
||||||
|
let res;
|
||||||
|
if (val == null) {
|
||||||
|
res = Math.max(self.rows, self.cols);
|
||||||
|
} else {
|
||||||
|
const max = Math.max(self.rows, self.cols);
|
||||||
|
if (max === self.rows) {
|
||||||
|
self.rows = val;
|
||||||
|
} else {
|
||||||
|
self.cols = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
used(row, col) {
|
||||||
|
const self = this;
|
||||||
|
return self.cellUsed[ 'c-' + row + '-' + col ] || false;
|
||||||
|
},
|
||||||
|
use(row, col) {
|
||||||
|
const self = this;
|
||||||
|
self.cellUsed[ 'c-' + row + '-' + col ] = true;
|
||||||
|
},
|
||||||
|
moveToNextCell() {
|
||||||
|
const self = this;
|
||||||
|
self.col++;
|
||||||
|
if (self.col >= self.cols) {
|
||||||
|
self.col = 0;
|
||||||
|
self.row++;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getPos(node) {
|
||||||
|
const self = this;
|
||||||
|
const begin = self.begin;
|
||||||
|
const cellWidth = self.cellWidth;
|
||||||
|
const cellHeight = self.cellHeight;
|
||||||
|
let x;
|
||||||
|
let y;
|
||||||
|
|
||||||
|
// see if we have a manual position set
|
||||||
|
const rcPos = self.id2manPos[ node.id ];
|
||||||
|
if (rcPos) {
|
||||||
|
x = rcPos.col * cellWidth + cellWidth / 2 + begin[0];
|
||||||
|
y = rcPos.row * cellHeight + cellHeight / 2 + begin[1];
|
||||||
|
|
||||||
|
} else { // otherwise set automatically
|
||||||
|
|
||||||
|
while (self.used(self.row, self.col)) {
|
||||||
|
self.moveToNextCell();
|
||||||
|
}
|
||||||
|
|
||||||
|
x = self.col * cellWidth + cellWidth / 2 + begin[0];
|
||||||
|
y = self.row * cellHeight + cellHeight / 2 + begin[1];
|
||||||
|
self.use(self.row, self.col);
|
||||||
|
|
||||||
|
self.moveToNextCell();
|
||||||
|
}
|
||||||
|
node.x = x;
|
||||||
|
node.y = y;
|
||||||
|
}
|
||||||
|
});
|
@ -11,6 +11,9 @@ module.exports = {
|
|||||||
Fruchterman: require('./fruchterman'),
|
Fruchterman: require('./fruchterman'),
|
||||||
Radial: require('./radial/radial'),
|
Radial: require('./radial/radial'),
|
||||||
Force: require('./force'),
|
Force: require('./force'),
|
||||||
Dagre: require('./dagre')
|
Dagre: require('./dagre'),
|
||||||
|
Concentric: require('./concentric'),
|
||||||
|
Grid: require('./grid'),
|
||||||
|
FruchtermanGroup: require('./fruchterman-group')
|
||||||
};
|
};
|
||||||
module.exports = Layout;
|
module.exports = Layout;
|
||||||
|
@ -34,6 +34,7 @@ Layout.registerLayout('mds', {
|
|||||||
// the graph-theoretic distance (shortest path distance) matrix
|
// the graph-theoretic distance (shortest path distance) matrix
|
||||||
const adjMatrix = Util.getAdjMatrix({ nodes, edges }, false);
|
const adjMatrix = Util.getAdjMatrix({ nodes, edges }, false);
|
||||||
const distances = Util.floydWarshall(adjMatrix);
|
const distances = Util.floydWarshall(adjMatrix);
|
||||||
|
self.handleInfinity(distances);
|
||||||
self.distances = distances;
|
self.distances = distances;
|
||||||
|
|
||||||
// scale the ideal edge length acoording to linkDistance
|
// scale the ideal edge length acoording to linkDistance
|
||||||
@ -75,5 +76,25 @@ Layout.registerLayout('mds', {
|
|||||||
return ret.U.map(function(row) {
|
return ret.U.map(function(row) {
|
||||||
return Numeric.mul(row, eigenValues).splice(0, dimension);
|
return Numeric.mul(row, eigenValues).splice(0, dimension);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
handleInfinity(distances) {
|
||||||
|
let maxDistance = -999999;
|
||||||
|
distances.forEach(row => {
|
||||||
|
row.forEach(value => {
|
||||||
|
if (value === Infinity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (maxDistance < value) {
|
||||||
|
maxDistance = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
distances.forEach((row, i) => {
|
||||||
|
row.forEach((value, j) => {
|
||||||
|
if (value === Infinity) {
|
||||||
|
distances[i][j] = maxDistance;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -45,8 +45,10 @@ Layout.registerLayout('radial', {
|
|||||||
focusNode: null, // 中心点,默认为数据中第一个点
|
focusNode: null, // 中心点,默认为数据中第一个点
|
||||||
unitRadius: null, // 每一圈半径
|
unitRadius: null, // 每一圈半径
|
||||||
linkDistance: 50, // 默认边长度
|
linkDistance: 50, // 默认边长度
|
||||||
preventOverlap: false, // 是否防止重叠
|
preventOverlap: false, // 是否防止重叠
|
||||||
nodeSize: 10 // 节点直径
|
nodeSize: 10, // 节点直径
|
||||||
|
strictRadial: true, // 是否必须是严格的 radial 布局,即每一层的节点严格布局在一个环上。preventOverlap 为 true 时生效。
|
||||||
|
maxPreventOverlapIteration: 200 // 防止重叠步骤的最大迭代次数
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -150,12 +152,13 @@ Layout.registerLayout('radial', {
|
|||||||
self.run();
|
self.run();
|
||||||
const preventOverlap = self.preventOverlap;
|
const preventOverlap = self.preventOverlap;
|
||||||
const nodeSize = self.nodeSize;
|
const nodeSize = self.nodeSize;
|
||||||
|
const strictRadial = self.strictRadial;
|
||||||
// stagger the overlapped nodes
|
// stagger the overlapped nodes
|
||||||
if (preventOverlap) {
|
if (preventOverlap) {
|
||||||
const nonoverlapForce = new RadialNonoverlapForce({
|
const nonoverlapForce = new RadialNonoverlapForce({
|
||||||
nodeSize, adjMatrix, positions, radii, height, width,
|
nodeSize, adjMatrix, positions, radii, height, width, strictRadial,
|
||||||
focusID: focusIndex,
|
focusID: focusIndex,
|
||||||
iterations: 200,
|
iterations: self.maxPreventOverlapIteration || 200,
|
||||||
k: positions.length / 4.5
|
k: positions.length / 4.5
|
||||||
});
|
});
|
||||||
positions = nonoverlapForce.layout();
|
positions = nonoverlapForce.layout();
|
||||||
|
@ -57,6 +57,11 @@ class RadialNonoverlapForce {
|
|||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
this.k = params.k || 5;
|
this.k = params.k || 5;
|
||||||
|
/**
|
||||||
|
* if each circle can be separated into subcircles to avoid overlappings
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.strictRadial = params.strictRadial;
|
||||||
}
|
}
|
||||||
layout() {
|
layout() {
|
||||||
const self = this;
|
const self = this;
|
||||||
@ -107,25 +112,28 @@ class RadialNonoverlapForce {
|
|||||||
const positions = self.positions;
|
const positions = self.positions;
|
||||||
const disp = self.disp;
|
const disp = self.disp;
|
||||||
const speed = self.speed;
|
const speed = self.speed;
|
||||||
|
const strictRadial = self.strictRadial;
|
||||||
const f = self.focusID;
|
const f = self.focusID;
|
||||||
|
|
||||||
disp.forEach((di, i) => {
|
if (strictRadial) {
|
||||||
const vx = positions[i][0] - positions[f][0];
|
disp.forEach((di, i) => {
|
||||||
const vy = positions[i][1] - positions[f][1];
|
const vx = positions[i][0] - positions[f][0];
|
||||||
const vLength = Math.sqrt(vx * vx + vy * vy);
|
const vy = positions[i][1] - positions[f][1];
|
||||||
let vpx = vy / vLength;
|
const vLength = Math.sqrt(vx * vx + vy * vy);
|
||||||
let vpy = -vx / vLength;
|
let vpx = vy / vLength;
|
||||||
const diLength = Math.sqrt(di.x * di.x + di.y * di.y);
|
let vpy = -vx / vLength;
|
||||||
let alpha = Math.acos((vpx * di.x + vpy * di.y) / diLength);
|
const diLength = Math.sqrt(di.x * di.x + di.y * di.y);
|
||||||
if (alpha > Math.PI / 2) {
|
let alpha = Math.acos((vpx * di.x + vpy * di.y) / diLength);
|
||||||
alpha -= Math.PI / 2;
|
if (alpha > Math.PI / 2) {
|
||||||
vpx *= -1;
|
alpha -= Math.PI / 2;
|
||||||
vpy *= -1;
|
vpx *= -1;
|
||||||
}
|
vpy *= -1;
|
||||||
const tdispLength = Math.cos(alpha) * diLength;
|
}
|
||||||
di.x = vpx * tdispLength;
|
const tdispLength = Math.cos(alpha) * diLength;
|
||||||
di.y = vpy * tdispLength;
|
di.x = vpx * tdispLength;
|
||||||
});
|
di.y = vpy * tdispLength;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// speed
|
// speed
|
||||||
positions.forEach((n, i) => {
|
positions.forEach((n, i) => {
|
||||||
@ -142,13 +150,15 @@ class RadialNonoverlapForce {
|
|||||||
const limitedDist = Math.min(self.maxDisplace * (speed / SPEED_DIVISOR), distLength);
|
const limitedDist = Math.min(self.maxDisplace * (speed / SPEED_DIVISOR), distLength);
|
||||||
n[0] += disp[i].x / distLength * limitedDist;
|
n[0] += disp[i].x / distLength * limitedDist;
|
||||||
n[1] += disp[i].y / distLength * limitedDist;
|
n[1] += disp[i].y / distLength * limitedDist;
|
||||||
let vx = n[0] - positions[f][0];
|
if (strictRadial) {
|
||||||
let vy = n[1] - positions[f][1];
|
let vx = n[0] - positions[f][0];
|
||||||
const nfDis = Math.sqrt(vx * vx + vy * vy);
|
let vy = n[1] - positions[f][1];
|
||||||
vx = vx / nfDis * radii[i];
|
const nfDis = Math.sqrt(vx * vx + vy * vy);
|
||||||
vy = vy / nfDis * radii[i];
|
vx = vx / nfDis * radii[i];
|
||||||
n[0] = positions[f][0] + vx;
|
vy = vy / nfDis * radii[i];
|
||||||
n[1] = positions[f][1] + vy;
|
n[0] = positions[f][0] + vx;
|
||||||
|
n[1] = positions[f][1] + vy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user