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>
|
||||
|
||||
<body>
|
||||
<button id='changeView'>前置边</button>
|
||||
<div id='description'>hover 节点或边,前置相关元素</button>
|
||||
<div id="mountNode"></div>
|
||||
<script src="../build/g6.js"></script>
|
||||
<script>
|
||||
const data = {
|
||||
nodes: [{
|
||||
id: 'node1',
|
||||
id: 'node0',
|
||||
x: 100,
|
||||
y: 100,
|
||||
shape: 'rect',
|
||||
style: {
|
||||
fill: '#fff'
|
||||
}
|
||||
}, {
|
||||
id: 'node2',
|
||||
size: 20
|
||||
},{
|
||||
id: 'node1',
|
||||
x: 200,
|
||||
y: 200,
|
||||
shape: 'rect',
|
||||
style: {
|
||||
fill: '#fff'
|
||||
}
|
||||
}, {
|
||||
size: 20
|
||||
},{
|
||||
id: 'node2',
|
||||
x: 150,
|
||||
y: 150,
|
||||
size: 20
|
||||
},{
|
||||
id: 'node3',
|
||||
x: 300,
|
||||
y: 300,
|
||||
shape: 'rect',
|
||||
style: {
|
||||
fill: '#fff'
|
||||
}
|
||||
x: 150,
|
||||
y: 250,
|
||||
size: 20
|
||||
},{
|
||||
id: 'node4',
|
||||
x: 150,
|
||||
y: 200,
|
||||
size: 20
|
||||
}],
|
||||
edges: [{
|
||||
id: 'edge0',
|
||||
source: 'node0',
|
||||
target: 'node1'
|
||||
},{
|
||||
id: 'edge1',
|
||||
target: 'node3',
|
||||
source: 'node1',
|
||||
style: {
|
||||
endArrow: true
|
||||
},
|
||||
labelCfg: {
|
||||
style: { stroke: 'white', lineWidth: 5 } // 加白边框
|
||||
}
|
||||
source: 'node2',
|
||||
target: 'node3'
|
||||
}]
|
||||
};
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 500,
|
||||
height: 500
|
||||
width: 800,
|
||||
height: 600,
|
||||
groupByTypes: false,
|
||||
defaultEdge: {
|
||||
style: {
|
||||
lineWidth: 2
|
||||
}
|
||||
}
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
graph.fitView()
|
||||
document.getElementById('changeView').addEventListener('click', (evt) => {
|
||||
const edge=graph.findById('edge1')
|
||||
const nodeGroup = graph.get('nodeGroup')
|
||||
const edge1G = edge.get('group')
|
||||
edge1G.toFront()
|
||||
nodeGroup.toBack();
|
||||
graph.paint()
|
||||
})
|
||||
// 获取图上的所有边实例
|
||||
const nodes = graph.getNodes();
|
||||
// 遍历边实例,将所有边提前。
|
||||
nodes.forEach(node => {
|
||||
node.toFront();
|
||||
});
|
||||
// 更改层级后需要重新绘制图
|
||||
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>
|
||||
</body>
|
||||
</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>
|
||||
<meta charset="UTF-8">
|
||||
<title>Fruchterman Layout with Changing Configurations</title>
|
||||
<title>Dagre Layout with Changing Configurations</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -230,7 +230,7 @@
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 500,
|
||||
width: 1000,
|
||||
height: 500,
|
||||
layout: {
|
||||
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>
|
||||
const data = {
|
||||
"nodes": [{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "0",
|
||||
"label": "0"
|
||||
},
|
||||
@ -396,6 +397,7 @@
|
||||
container: 'mountNode',
|
||||
width: 1000,
|
||||
height: 600,
|
||||
fitView: true,
|
||||
layout: {
|
||||
type: 'mds',
|
||||
linkDistance: 100
|
||||
|
@ -3888,7 +3888,7 @@
|
||||
const maxDegree = 4;
|
||||
// the max degree about foces(clicked) node in the original data
|
||||
const oMaxDegree = 3;
|
||||
const unitRadius = 40;
|
||||
const unitRadius = 100;
|
||||
const focusNodeId = "2";
|
||||
// re-place the clicked node far away the exisiting items
|
||||
// along the radius from center node to it
|
||||
@ -3905,7 +3905,9 @@
|
||||
focusNode: "2",
|
||||
unitRadius,
|
||||
linkDistance: 180,
|
||||
preventOverlap: true
|
||||
preventOverlap: true,
|
||||
strictRadial: false,
|
||||
nodeSize: 50
|
||||
});
|
||||
subRadialLayout.init({
|
||||
'nodes': newNodeModels,
|
||||
@ -3921,7 +3923,7 @@
|
||||
});
|
||||
|
||||
const focusNode = data_m.nodes[20];
|
||||
const mainUnitRadius = 120;
|
||||
const mainUnitRadius = 220;
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 1000,
|
||||
@ -3932,16 +3934,21 @@
|
||||
maxIteration: 200,
|
||||
focusNode,
|
||||
unitRadius: mainUnitRadius,
|
||||
linkDistance: 100,
|
||||
linkDistance: 200,
|
||||
preventOverlap: true,
|
||||
nodeSize: 20
|
||||
nodeSize: 50,
|
||||
strictRadial: true,
|
||||
maxPreventOverlapIteration: 500
|
||||
},
|
||||
animate: true,
|
||||
modes: {
|
||||
default: ['drag-node', 'click-select', 'click-add-node', 'drag-canvas']
|
||||
},
|
||||
defaultNode: {
|
||||
size: [20, 20]
|
||||
size: 50,
|
||||
style: {
|
||||
stroke: '#000'
|
||||
}
|
||||
},
|
||||
defaultEdge: {
|
||||
size: 1,
|
||||
|
@ -34,7 +34,7 @@
|
||||
size: [40, 40]
|
||||
},
|
||||
modes: {
|
||||
default: [ 'brush-select', 'drag-node' ]
|
||||
default: [ 'brush-select', 'drag-canvas', 'zoom-canvas' ]
|
||||
}
|
||||
});
|
||||
|
||||
@ -137,8 +137,9 @@
|
||||
|
||||
const menu = document.getElementById('contextMenu')
|
||||
graph.on('node:contextmenu', evt => {
|
||||
menu.style.left = evt.x + 'px'
|
||||
menu.style.top = evt.y + 'px'
|
||||
console.log(evt);
|
||||
menu.style.left = evt.clientX + 'px'
|
||||
menu.style.top = evt.clientY + 'px'
|
||||
})
|
||||
|
||||
graph.on('node:mouseleave', evt => {
|
||||
|
@ -18,20 +18,9 @@ class LayoutController {
|
||||
let layoutType = self.layoutType;
|
||||
const graph = self.graph;
|
||||
// const data = graph.get('data');
|
||||
const nodes = [];
|
||||
const edges = [];
|
||||
const nodeItems = graph.getNodes();
|
||||
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;
|
||||
|
||||
self.data = self.setDataFromGraph();
|
||||
const nodes = self.data.nodes;
|
||||
|
||||
if (!nodes) {
|
||||
return;
|
||||
@ -84,7 +73,7 @@ class LayoutController {
|
||||
console.warn('The layout method: ' + layoutCfg + ' does not exist! Please specify it first.');
|
||||
return;
|
||||
}
|
||||
layoutMethod.init(data);
|
||||
layoutMethod.init(self.data);
|
||||
graph.emit('beforelayout');
|
||||
layoutMethod.execute();
|
||||
if (layoutType !== 'force') {
|
||||
@ -110,6 +99,8 @@ class LayoutController {
|
||||
const graph = self.graph;
|
||||
self.layoutType = cfg.type;
|
||||
const layoutMethod = self.layoutMethod;
|
||||
self.data = self.setDataFromGraph();
|
||||
layoutMethod.init(self.data);
|
||||
layoutMethod.updateCfg(cfg);
|
||||
graph.emit('beforelayout');
|
||||
layoutMethod.execute();
|
||||
@ -139,6 +130,34 @@ class LayoutController {
|
||||
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() {
|
||||
const self = this;
|
||||
|
@ -512,10 +512,6 @@ class Graph extends EventEmitter {
|
||||
Util.each(data.edges, edge => {
|
||||
self.add(EDGE, edge);
|
||||
});
|
||||
// layout
|
||||
const layoutController = self.get('layoutController');
|
||||
layoutController.layout();
|
||||
self.refreshPositions();
|
||||
|
||||
// 防止传入的数据不存在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')) {
|
||||
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'),
|
||||
Radial: require('./radial/radial'),
|
||||
Force: require('./force'),
|
||||
Dagre: require('./dagre')
|
||||
Dagre: require('./dagre'),
|
||||
Concentric: require('./concentric'),
|
||||
Grid: require('./grid'),
|
||||
FruchtermanGroup: require('./fruchterman-group')
|
||||
};
|
||||
module.exports = Layout;
|
||||
|
@ -34,6 +34,7 @@ Layout.registerLayout('mds', {
|
||||
// the graph-theoretic distance (shortest path distance) matrix
|
||||
const adjMatrix = Util.getAdjMatrix({ nodes, edges }, false);
|
||||
const distances = Util.floydWarshall(adjMatrix);
|
||||
self.handleInfinity(distances);
|
||||
self.distances = distances;
|
||||
|
||||
// scale the ideal edge length acoording to linkDistance
|
||||
@ -75,5 +76,25 @@ Layout.registerLayout('mds', {
|
||||
return ret.U.map(function(row) {
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -46,7 +46,9 @@ Layout.registerLayout('radial', {
|
||||
unitRadius: null, // 每一圈半径
|
||||
linkDistance: 50, // 默认边长度
|
||||
preventOverlap: false, // 是否防止重叠
|
||||
nodeSize: 10 // 节点直径
|
||||
nodeSize: 10, // 节点直径
|
||||
strictRadial: true, // 是否必须是严格的 radial 布局,即每一层的节点严格布局在一个环上。preventOverlap 为 true 时生效。
|
||||
maxPreventOverlapIteration: 200 // 防止重叠步骤的最大迭代次数
|
||||
};
|
||||
},
|
||||
/**
|
||||
@ -150,12 +152,13 @@ Layout.registerLayout('radial', {
|
||||
self.run();
|
||||
const preventOverlap = self.preventOverlap;
|
||||
const nodeSize = self.nodeSize;
|
||||
const strictRadial = self.strictRadial;
|
||||
// stagger the overlapped nodes
|
||||
if (preventOverlap) {
|
||||
const nonoverlapForce = new RadialNonoverlapForce({
|
||||
nodeSize, adjMatrix, positions, radii, height, width,
|
||||
nodeSize, adjMatrix, positions, radii, height, width, strictRadial,
|
||||
focusID: focusIndex,
|
||||
iterations: 200,
|
||||
iterations: self.maxPreventOverlapIteration || 200,
|
||||
k: positions.length / 4.5
|
||||
});
|
||||
positions = nonoverlapForce.layout();
|
||||
|
@ -57,6 +57,11 @@ class RadialNonoverlapForce {
|
||||
* @type {number}
|
||||
*/
|
||||
this.k = params.k || 5;
|
||||
/**
|
||||
* if each circle can be separated into subcircles to avoid overlappings
|
||||
* @type {number}
|
||||
*/
|
||||
this.strictRadial = params.strictRadial;
|
||||
}
|
||||
layout() {
|
||||
const self = this;
|
||||
@ -107,8 +112,10 @@ class RadialNonoverlapForce {
|
||||
const positions = self.positions;
|
||||
const disp = self.disp;
|
||||
const speed = self.speed;
|
||||
const strictRadial = self.strictRadial;
|
||||
const f = self.focusID;
|
||||
|
||||
if (strictRadial) {
|
||||
disp.forEach((di, i) => {
|
||||
const vx = positions[i][0] - positions[f][0];
|
||||
const vy = positions[i][1] - positions[f][1];
|
||||
@ -126,6 +133,7 @@ class RadialNonoverlapForce {
|
||||
di.x = vpx * tdispLength;
|
||||
di.y = vpy * tdispLength;
|
||||
});
|
||||
}
|
||||
|
||||
// speed
|
||||
positions.forEach((n, i) => {
|
||||
@ -142,6 +150,7 @@ class RadialNonoverlapForce {
|
||||
const limitedDist = Math.min(self.maxDisplace * (speed / SPEED_DIVISOR), distLength);
|
||||
n[0] += disp[i].x / distLength * limitedDist;
|
||||
n[1] += disp[i].y / distLength * limitedDist;
|
||||
if (strictRadial) {
|
||||
let vx = n[0] - positions[f][0];
|
||||
let vy = n[1] - positions[f][1];
|
||||
const nfDis = Math.sqrt(vx * vx + vy * vy);
|
||||
@ -150,6 +159,7 @@ class RadialNonoverlapForce {
|
||||
n[0] = positions[f][0] + vx;
|
||||
n[1] = positions[f][1] + vy;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user