mirror of
https://gitee.com/antv/g6.git
synced 2024-12-04 12:49:04 +08:00
Merge pull request #931 from antvis/layout-web-worker
Feat: support layout using web worker
This commit is contained in:
commit
aecfb14f99
@ -17,6 +17,9 @@
|
||||
padding: 10px 8px;
|
||||
box-shadow: rgb(174, 174, 174) 0px 0px 10px;
|
||||
}
|
||||
canvas {
|
||||
border: 1px solid red;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div id="tip">布局中,请稍候......</div>
|
||||
@ -29,52 +32,6 @@
|
||||
'#a0d911', '#13c2c2', '#1890ff', '#b37feb', '#eb2f96' ];
|
||||
const beginColor = '#5b8c00'; // green
|
||||
const endColor = '#ff4d4f'; // red
|
||||
const testData = {
|
||||
nodes: [
|
||||
{
|
||||
id: '0',
|
||||
cluster: 1,
|
||||
exportValue: 10000,
|
||||
x: 100,
|
||||
y: 200,
|
||||
// label: 0
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
cluster: 1,
|
||||
exportValue: 10000,
|
||||
x: 200,
|
||||
y: 200,
|
||||
// label: 1
|
||||
},
|
||||
// {
|
||||
// id: '2',
|
||||
// region: '1'
|
||||
// },
|
||||
// {
|
||||
// id: '3',
|
||||
// region: '2'
|
||||
// }
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: '0',
|
||||
target: '1',
|
||||
},
|
||||
{
|
||||
source: '1',
|
||||
target: '0',
|
||||
},
|
||||
// {
|
||||
// source: '2',
|
||||
// target: '3',
|
||||
// },
|
||||
// {
|
||||
// source: '3',
|
||||
// target: '0',
|
||||
// },
|
||||
]
|
||||
}
|
||||
d3.json("./assets/data/filtered-trade.json", function(data) {
|
||||
const nodes = data.nodes;
|
||||
const edges = data.edges;
|
||||
@ -122,8 +79,11 @@
|
||||
maxIteration: 8000,
|
||||
gravity: 10,
|
||||
clustering: true,
|
||||
clusterGravity: 30
|
||||
clusterGravity: 30,
|
||||
workerEnabled: true
|
||||
},
|
||||
padding: 0,
|
||||
fitViewPadding: 0,
|
||||
fitView: true,
|
||||
linkCenter: true,
|
||||
defaultNode: {
|
||||
@ -134,7 +94,7 @@
|
||||
shape: 'quadratic'
|
||||
},
|
||||
modes: {
|
||||
default: [ 'drag-node', 'drag-canvas', {
|
||||
default: [ 'drag-node', 'zoom-canvas', 'drag-canvas', {
|
||||
type: 'tooltip',
|
||||
formatText(model) {
|
||||
let name = '';
|
||||
@ -155,34 +115,40 @@
|
||||
},
|
||||
});
|
||||
|
||||
graph.on('beforelayout', () => {
|
||||
console.log(data.nodes[0].x, data.nodes[0].y, data.nodes[10].x, data.nodes[10].y);
|
||||
});
|
||||
graph.on('afterlayout', () => {
|
||||
const tipDiv = document.getElementById('tip');
|
||||
tipDiv.innerHTML = '布局完成!';
|
||||
|
||||
const edgeItems = graph.getEdges();
|
||||
edgeItems.forEach(e => {
|
||||
const lineWidth = 0.4;
|
||||
const strokeOpacity = 0.2;
|
||||
let stroke = 'l(0) 0:' + beginColor + ' 1:' + endColor;
|
||||
const sourceModel = e.getSource().getModel();
|
||||
const targetModel = e.getTarget().getModel();
|
||||
if (sourceModel.x > targetModel.x) {
|
||||
stroke = 'l(0) 0:' + endColor + ' 1:' + beginColor;
|
||||
}
|
||||
e.update({
|
||||
style: {
|
||||
lineWidth,
|
||||
strokeOpacity,
|
||||
stroke
|
||||
}
|
||||
})
|
||||
});
|
||||
// 等布局完以后再绘制
|
||||
graph.paint();
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
// 如果使用web worker,graph.render是异步的,立即返回,在web worker里执行布局。
|
||||
// 而如果不使用web worker,graph.render是同步的,意味着只有等布局执行完以后,才会执行graph.render后面的代码。
|
||||
graph.render();
|
||||
|
||||
const edgeItems = graph.getEdges();
|
||||
edgeItems.forEach(e => {
|
||||
const lineWidth = 0.4;
|
||||
const strokeOpacity = 0.2;
|
||||
let stroke = 'l(0) 0:' + beginColor + ' 1:' + endColor;
|
||||
const sourceModel = e.getSource().getModel();
|
||||
const targetModel = e.getTarget().getModel();
|
||||
if (sourceModel.x > targetModel.x) {
|
||||
stroke = 'l(0) 0:' + endColor + ' 1:' + beginColor;
|
||||
}
|
||||
e.update({
|
||||
style: {
|
||||
lineWidth,
|
||||
strokeOpacity,
|
||||
stroke
|
||||
}
|
||||
})
|
||||
});
|
||||
graph.paint();
|
||||
|
||||
graph.on('node:click', e => {
|
||||
const targetItem = e.item;
|
||||
const model = targetItem.getModel();
|
||||
|
424
demos/layout-circular-web-worker.html
Normal file
424
demos/layout-circular-web-worker.html
Normal file
@ -0,0 +1,424 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Circular Layout using web worker</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<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"
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"label": "6"
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"label": "7"
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"label": "8"
|
||||
},
|
||||
{
|
||||
"id": "9",
|
||||
"label": "9"
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
"label": "10"
|
||||
},
|
||||
{
|
||||
"id": "11",
|
||||
"label": "11"
|
||||
},
|
||||
{
|
||||
"id": "12",
|
||||
"label": "12"
|
||||
},
|
||||
{
|
||||
"id": "13",
|
||||
"label": "13"
|
||||
},
|
||||
{
|
||||
"id": "14",
|
||||
"label": "14"
|
||||
},
|
||||
{
|
||||
"id": "15",
|
||||
"label": "15"
|
||||
},
|
||||
{
|
||||
"id": "16",
|
||||
"label": "16"
|
||||
},
|
||||
{
|
||||
"id": "17",
|
||||
"label": "17"
|
||||
},
|
||||
{
|
||||
"id": "18",
|
||||
"label": "18"
|
||||
},
|
||||
{
|
||||
"id": "19",
|
||||
"label": "19"
|
||||
},
|
||||
{
|
||||
"id": "20",
|
||||
"label": "20"
|
||||
},
|
||||
{
|
||||
"id": "21",
|
||||
"label": "21"
|
||||
},
|
||||
{
|
||||
"id": "22",
|
||||
"label": "22"
|
||||
},
|
||||
{
|
||||
"id": "23",
|
||||
"label": "23"
|
||||
},
|
||||
{
|
||||
"id": "24",
|
||||
"label": "24"
|
||||
},
|
||||
{
|
||||
"id": "25",
|
||||
"label": "25"
|
||||
},
|
||||
{
|
||||
"id": "26",
|
||||
"label": "26"
|
||||
},
|
||||
{
|
||||
"id": "27",
|
||||
"label": "27"
|
||||
},
|
||||
{
|
||||
"id": "28",
|
||||
"label": "28"
|
||||
},
|
||||
{
|
||||
"id": "29",
|
||||
"label": "29"
|
||||
},
|
||||
{
|
||||
"id": "30",
|
||||
"label": "30"
|
||||
},
|
||||
{
|
||||
"id": "31",
|
||||
"label": "31"
|
||||
},
|
||||
{
|
||||
"id": "32",
|
||||
"label": "32"
|
||||
},
|
||||
{
|
||||
"id": "33",
|
||||
"label": "33"
|
||||
}],
|
||||
"edges": [{
|
||||
"source": "0",
|
||||
"target": "1"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "2"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "3"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "4"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "5"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "7"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "8"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "9"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "10"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "11"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "13"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "14"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "15"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "16"
|
||||
},
|
||||
{
|
||||
"source": "2",
|
||||
"target": "3"
|
||||
},
|
||||
{
|
||||
"source": "4",
|
||||
"target": "5"
|
||||
},
|
||||
{
|
||||
"source": "4",
|
||||
"target": "6"
|
||||
},
|
||||
{
|
||||
"source": "5",
|
||||
"target": "6"
|
||||
},
|
||||
{
|
||||
"source": "7",
|
||||
"target": "13"
|
||||
},
|
||||
{
|
||||
"source": "8",
|
||||
"target": "14"
|
||||
},
|
||||
{
|
||||
"source": "9",
|
||||
"target": "10"
|
||||
},
|
||||
{
|
||||
"source": "10",
|
||||
"target": "22"
|
||||
},
|
||||
{
|
||||
"source": "10",
|
||||
"target": "14"
|
||||
},
|
||||
{
|
||||
"source": "10",
|
||||
"target": "12"
|
||||
},
|
||||
{
|
||||
"source": "10",
|
||||
"target": "24"
|
||||
},
|
||||
{
|
||||
"source": "10",
|
||||
"target": "21"
|
||||
},
|
||||
{
|
||||
"source": "10",
|
||||
"target": "20"
|
||||
},
|
||||
{
|
||||
"source": "11",
|
||||
"target": "24"
|
||||
},
|
||||
{
|
||||
"source": "11",
|
||||
"target": "22"
|
||||
},
|
||||
{
|
||||
"source": "11",
|
||||
"target": "14"
|
||||
},
|
||||
{
|
||||
"source": "12",
|
||||
"target": "13"
|
||||
},
|
||||
{
|
||||
"source": "16",
|
||||
"target": "17"
|
||||
},
|
||||
{
|
||||
"source": "16",
|
||||
"target": "18"
|
||||
},
|
||||
{
|
||||
"source": "16",
|
||||
"target": "21"
|
||||
},
|
||||
{
|
||||
"source": "16",
|
||||
"target": "22"
|
||||
},
|
||||
{
|
||||
"source": "17",
|
||||
"target": "18"
|
||||
},
|
||||
{
|
||||
"source": "17",
|
||||
"target": "20"
|
||||
},
|
||||
{
|
||||
"source": "18",
|
||||
"target": "19"
|
||||
},
|
||||
{
|
||||
"source": "19",
|
||||
"target": "20"
|
||||
},
|
||||
{
|
||||
"source": "19",
|
||||
"target": "33"
|
||||
},
|
||||
{
|
||||
"source": "19",
|
||||
"target": "22"
|
||||
},
|
||||
{
|
||||
"source": "19",
|
||||
"target": "23"
|
||||
},
|
||||
{
|
||||
"source": "20",
|
||||
"target": "21"
|
||||
},
|
||||
{
|
||||
"source": "21",
|
||||
"target": "22"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "24"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "25"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "26"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "23"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "28"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "30"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "31"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "32"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "33"
|
||||
},
|
||||
{
|
||||
"source": "23",
|
||||
"target": "28"
|
||||
},
|
||||
{
|
||||
"source": "23",
|
||||
"target": "27"
|
||||
},
|
||||
{
|
||||
"source": "23",
|
||||
"target": "29"
|
||||
},
|
||||
{
|
||||
"source": "23",
|
||||
"target": "30"
|
||||
},
|
||||
{
|
||||
"source": "23",
|
||||
"target": "31"
|
||||
},
|
||||
{
|
||||
"source": "23",
|
||||
"target": "33"
|
||||
},
|
||||
{
|
||||
"source": "32",
|
||||
"target": "33"
|
||||
}]
|
||||
};
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 1000,
|
||||
height: 800,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
center: [500, 400],
|
||||
radius: 200,
|
||||
ordering: null, // null || 'topology' || 'degree', null means nodes are ordered by data order
|
||||
// use web worker to layout
|
||||
workerEnabled: true,
|
||||
},
|
||||
defaultNode: {
|
||||
size: [20, 20],
|
||||
color: 'steelblue'
|
||||
},
|
||||
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();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
486
demos/layout-data-translation-web-worker.html
Normal file
486
demos/layout-data-translation-web-worker.html
Normal file
@ -0,0 +1,486 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Force Layout(using web worker) with Changing Data</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<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"
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"label": "6"
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"label": "7"
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"label": "8"
|
||||
},
|
||||
{
|
||||
"id": "9",
|
||||
"label": "9"
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
"label": "10"
|
||||
},
|
||||
{
|
||||
"id": "11",
|
||||
"label": "11"
|
||||
},
|
||||
{
|
||||
"id": "12",
|
||||
"label": "12"
|
||||
},
|
||||
{
|
||||
"id": "13",
|
||||
"label": "13"
|
||||
},
|
||||
{
|
||||
"id": "14",
|
||||
"label": "14"
|
||||
},
|
||||
{
|
||||
"id": "15",
|
||||
"label": "15"
|
||||
},
|
||||
{
|
||||
"id": "16",
|
||||
"label": "16"
|
||||
},
|
||||
{
|
||||
"id": "17",
|
||||
"label": "17"
|
||||
},
|
||||
{
|
||||
"id": "18",
|
||||
"label": "18"
|
||||
},
|
||||
{
|
||||
"id": "19",
|
||||
"label": "19"
|
||||
},
|
||||
{
|
||||
"id": "20",
|
||||
"label": "20"
|
||||
},
|
||||
{
|
||||
"id": "21",
|
||||
"label": "21"
|
||||
},
|
||||
{
|
||||
"id": "22",
|
||||
"label": "22"
|
||||
},
|
||||
{
|
||||
"id": "23",
|
||||
"label": "23"
|
||||
},
|
||||
{
|
||||
"id": "24",
|
||||
"label": "24"
|
||||
},
|
||||
{
|
||||
"id": "25",
|
||||
"label": "25"
|
||||
},
|
||||
{
|
||||
"id": "26",
|
||||
"label": "26"
|
||||
},
|
||||
{
|
||||
"id": "27",
|
||||
"label": "27"
|
||||
},
|
||||
{
|
||||
"id": "28",
|
||||
"label": "28"
|
||||
},
|
||||
{
|
||||
"id": "29",
|
||||
"label": "29"
|
||||
},
|
||||
{
|
||||
"id": "30",
|
||||
"label": "30"
|
||||
},
|
||||
{
|
||||
"id": "31",
|
||||
"label": "31"
|
||||
},
|
||||
{
|
||||
"id": "32",
|
||||
"label": "32"
|
||||
},
|
||||
{
|
||||
"id": "33",
|
||||
"label": "33"
|
||||
}],
|
||||
"edges": [{
|
||||
"source": "0",
|
||||
"target": "1"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "2"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "3"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "4"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "5"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "7"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "8"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "9"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "10"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "11"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "13"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "14"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "15"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "16"
|
||||
},
|
||||
{
|
||||
"source": "2",
|
||||
"target": "3"
|
||||
},
|
||||
{
|
||||
"source": "4",
|
||||
"target": "5"
|
||||
},
|
||||
{
|
||||
"source": "4",
|
||||
"target": "6"
|
||||
},
|
||||
{
|
||||
"source": "5",
|
||||
"target": "6"
|
||||
},
|
||||
{
|
||||
"source": "7",
|
||||
"target": "13"
|
||||
},
|
||||
{
|
||||
"source": "8",
|
||||
"target": "14"
|
||||
},
|
||||
{
|
||||
"source": "9",
|
||||
"target": "10"
|
||||
},
|
||||
{
|
||||
"source": "10",
|
||||
"target": "22"
|
||||
},
|
||||
{
|
||||
"source": "10",
|
||||
"target": "14"
|
||||
},
|
||||
{
|
||||
"source": "10",
|
||||
"target": "12"
|
||||
},
|
||||
{
|
||||
"source": "10",
|
||||
"target": "24"
|
||||
},
|
||||
{
|
||||
"source": "10",
|
||||
"target": "21"
|
||||
},
|
||||
{
|
||||
"source": "10",
|
||||
"target": "20"
|
||||
},
|
||||
{
|
||||
"source": "11",
|
||||
"target": "24"
|
||||
},
|
||||
{
|
||||
"source": "11",
|
||||
"target": "22"
|
||||
},
|
||||
{
|
||||
"source": "11",
|
||||
"target": "14"
|
||||
},
|
||||
{
|
||||
"source": "12",
|
||||
"target": "13"
|
||||
},
|
||||
{
|
||||
"source": "16",
|
||||
"target": "17"
|
||||
},
|
||||
{
|
||||
"source": "16",
|
||||
"target": "18"
|
||||
},
|
||||
{
|
||||
"source": "16",
|
||||
"target": "21"
|
||||
},
|
||||
{
|
||||
"source": "16",
|
||||
"target": "22"
|
||||
},
|
||||
{
|
||||
"source": "17",
|
||||
"target": "18"
|
||||
},
|
||||
{
|
||||
"source": "17",
|
||||
"target": "20"
|
||||
},
|
||||
{
|
||||
"source": "18",
|
||||
"target": "19"
|
||||
},
|
||||
{
|
||||
"source": "19",
|
||||
"target": "20"
|
||||
},
|
||||
{
|
||||
"source": "19",
|
||||
"target": "33"
|
||||
},
|
||||
{
|
||||
"source": "19",
|
||||
"target": "22"
|
||||
},
|
||||
{
|
||||
"source": "19",
|
||||
"target": "23"
|
||||
},
|
||||
{
|
||||
"source": "20",
|
||||
"target": "21"
|
||||
},
|
||||
{
|
||||
"source": "21",
|
||||
"target": "22"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "24"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "25"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "26"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "23"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "28"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "30"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "31"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "32"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "33"
|
||||
},
|
||||
{
|
||||
"source": "23",
|
||||
"target": "28"
|
||||
},
|
||||
{
|
||||
"source": "23",
|
||||
"target": "27"
|
||||
},
|
||||
{
|
||||
"source": "23",
|
||||
"target": "29"
|
||||
},
|
||||
{
|
||||
"source": "23",
|
||||
"target": "30"
|
||||
},
|
||||
{
|
||||
"source": "23",
|
||||
"target": "31"
|
||||
},
|
||||
{
|
||||
"source": "23",
|
||||
"target": "33"
|
||||
},
|
||||
{
|
||||
"source": "32",
|
||||
"target": "33"
|
||||
}]
|
||||
};
|
||||
|
||||
const data2 = {
|
||||
"nodes": [{
|
||||
id: 'b0',
|
||||
label: '0'
|
||||
},
|
||||
{
|
||||
id: 'b1',
|
||||
label: '1'
|
||||
},
|
||||
{
|
||||
id: 'b2',
|
||||
label: '2'
|
||||
},
|
||||
{
|
||||
id: 'b3',
|
||||
label: '3'
|
||||
},
|
||||
{
|
||||
id: 'b4',
|
||||
label: '4'
|
||||
},
|
||||
{
|
||||
id: 'b5',
|
||||
label: '5'
|
||||
}],
|
||||
"edges": [{
|
||||
"id": "be1",
|
||||
"source": "b0",
|
||||
"target": "b1"
|
||||
},
|
||||
{
|
||||
"id": "be2",
|
||||
"source": "b0",
|
||||
"target": "b2"
|
||||
},
|
||||
{
|
||||
"id": "be3",
|
||||
"source": "b0",
|
||||
"target": "b3"
|
||||
},
|
||||
{
|
||||
"id": "be4",
|
||||
"source": "b0",
|
||||
"target": "b4"
|
||||
},
|
||||
{
|
||||
"id": "be5",
|
||||
"source": "b0",
|
||||
"target": "b5"
|
||||
}]
|
||||
}
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 1000,
|
||||
height: 600,
|
||||
modes: {
|
||||
default: ['drag-canvas', 'drag-node'],
|
||||
},
|
||||
layout: {
|
||||
type: 'force',
|
||||
center: [500, 300],
|
||||
// use web worker to layout
|
||||
workerEnabled: true,
|
||||
},
|
||||
animate: true,
|
||||
defaultNode: {
|
||||
size: [20, 20],
|
||||
color: 'steelblue',
|
||||
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();
|
||||
|
||||
setTimeout(() => {
|
||||
graph.changeData(data2);
|
||||
},
|
||||
2500);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
143
demos/layout-force-timing-web-worker.html
Normal file
143
demos/layout-force-timing-web-worker.html
Normal file
@ -0,0 +1,143 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>力导布局(使用web worker) 监听布局的开始与结束</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="timingTip"></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"
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"label": "6"
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"label": "7"
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"label": "8"
|
||||
},
|
||||
{
|
||||
"id": "9",
|
||||
"label": "9"
|
||||
}],
|
||||
"edges": [{
|
||||
"source": "0",
|
||||
"target": "1"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "2"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "3"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "4"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "5"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "7"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "8"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "9"
|
||||
},
|
||||
{
|
||||
"source": "2",
|
||||
"target": "3"
|
||||
},
|
||||
{
|
||||
"source": "4",
|
||||
"target": "5"
|
||||
},
|
||||
{
|
||||
"source": "4",
|
||||
"target": "6"
|
||||
},
|
||||
{
|
||||
"source": "5",
|
||||
"target": "6"
|
||||
}]
|
||||
};
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 1000,
|
||||
height: 600,
|
||||
layout: {
|
||||
type: 'force',
|
||||
preventOverlap: true,
|
||||
nodeSize: 20,
|
||||
// use web worker to layout
|
||||
workerEnabled: true
|
||||
},
|
||||
modes: {
|
||||
default: ['drag-node']
|
||||
},
|
||||
defaultNode: {
|
||||
size: [20, 20],
|
||||
color: 'steelblue'
|
||||
},
|
||||
defaultEdge: {
|
||||
size: 1,
|
||||
color: '#e2e2e2'
|
||||
}
|
||||
});
|
||||
|
||||
const tipDiv = document.getElementById('timingTip');
|
||||
graph.on('beforelayout', () => {
|
||||
tipDiv.innerHTML = 'It is doing force-directed layout now!'
|
||||
+ ' After it is done, this text will be changed.';
|
||||
});
|
||||
graph.on('afterlayout', () => {
|
||||
tipDiv.innerHTML = 'Done!';
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
1588
demos/layout-force-web-worker.html
Normal file
1588
demos/layout-force-web-worker.html
Normal file
File diff suppressed because it is too large
Load Diff
1552
demos/layout-fruchterman-web-worker.html
Normal file
1552
demos/layout-fruchterman-web-worker.html
Normal file
File diff suppressed because it is too large
Load Diff
497
demos/layout-method-translation-web-worker.html
Normal file
497
demos/layout-method-translation-web-worker.html
Normal file
@ -0,0 +1,497 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Change Layouts using web worker</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="tip">Random Layout</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"
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"label": "6"
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"label": "7"
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"label": "8"
|
||||
},
|
||||
{
|
||||
"id": "9",
|
||||
"label": "9"
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
"label": "10"
|
||||
},
|
||||
{
|
||||
"id": "11",
|
||||
"label": "11"
|
||||
},
|
||||
{
|
||||
"id": "12",
|
||||
"label": "12"
|
||||
},
|
||||
{
|
||||
"id": "13",
|
||||
"label": "13"
|
||||
},
|
||||
{
|
||||
"id": "14",
|
||||
"label": "14"
|
||||
},
|
||||
{
|
||||
"id": "15",
|
||||
"label": "15"
|
||||
},
|
||||
{
|
||||
"id": "16",
|
||||
"label": "16"
|
||||
},
|
||||
{
|
||||
"id": "17",
|
||||
"label": "17"
|
||||
},
|
||||
{
|
||||
"id": "18",
|
||||
"label": "18"
|
||||
},
|
||||
{
|
||||
"id": "19",
|
||||
"label": "19"
|
||||
},
|
||||
{
|
||||
"id": "20",
|
||||
"label": "20"
|
||||
},
|
||||
{
|
||||
"id": "21",
|
||||
"label": "21"
|
||||
},
|
||||
{
|
||||
"id": "22",
|
||||
"label": "22"
|
||||
},
|
||||
{
|
||||
"id": "23",
|
||||
"label": "23"
|
||||
},
|
||||
{
|
||||
"id": "24",
|
||||
"label": "24"
|
||||
},
|
||||
{
|
||||
"id": "25",
|
||||
"label": "25"
|
||||
},
|
||||
{
|
||||
"id": "26",
|
||||
"label": "26"
|
||||
},
|
||||
{
|
||||
"id": "27",
|
||||
"label": "27"
|
||||
},
|
||||
{
|
||||
"id": "28",
|
||||
"label": "28"
|
||||
},
|
||||
{
|
||||
"id": "29",
|
||||
"label": "29"
|
||||
},
|
||||
{
|
||||
"id": "30",
|
||||
"label": "30"
|
||||
},
|
||||
{
|
||||
"id": "31",
|
||||
"label": "31"
|
||||
},
|
||||
{
|
||||
"id": "32",
|
||||
"label": "32"
|
||||
},
|
||||
{
|
||||
"id": "33",
|
||||
"label": "33"
|
||||
}],
|
||||
"edges": [{
|
||||
"source": "0",
|
||||
"target": "1"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "2"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "3"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "4"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "5"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "7"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "8"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "9"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "10"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "11"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "13"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "14"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "15"
|
||||
},
|
||||
{
|
||||
"source": "0",
|
||||
"target": "16"
|
||||
},
|
||||
{
|
||||
"source": "2",
|
||||
"target": "3"
|
||||
},
|
||||
{
|
||||
"source": "4",
|
||||
"target": "5"
|
||||
},
|
||||
{
|
||||
"source": "4",
|
||||
"target": "6"
|
||||
},
|
||||
{
|
||||
"source": "5",
|
||||
"target": "6"
|
||||
},
|
||||
{
|
||||
"source": "7",
|
||||
"target": "13"
|
||||
},
|
||||
{
|
||||
"source": "8",
|
||||
"target": "14"
|
||||
},
|
||||
{
|
||||
"source": "9",
|
||||
"target": "10"
|
||||
},
|
||||
{
|
||||
"source": "10",
|
||||
"target": "22"
|
||||
},
|
||||
{
|
||||
"source": "10",
|
||||
"target": "14"
|
||||
},
|
||||
{
|
||||
"source": "10",
|
||||
"target": "12"
|
||||
},
|
||||
{
|
||||
"source": "10",
|
||||
"target": "24"
|
||||
},
|
||||
{
|
||||
"source": "10",
|
||||
"target": "21"
|
||||
},
|
||||
{
|
||||
"source": "10",
|
||||
"target": "20"
|
||||
},
|
||||
{
|
||||
"source": "11",
|
||||
"target": "24"
|
||||
},
|
||||
{
|
||||
"source": "11",
|
||||
"target": "22"
|
||||
},
|
||||
{
|
||||
"source": "11",
|
||||
"target": "14"
|
||||
},
|
||||
{
|
||||
"source": "12",
|
||||
"target": "13"
|
||||
},
|
||||
{
|
||||
"source": "16",
|
||||
"target": "17"
|
||||
},
|
||||
{
|
||||
"source": "16",
|
||||
"target": "18"
|
||||
},
|
||||
{
|
||||
"source": "16",
|
||||
"target": "21"
|
||||
},
|
||||
{
|
||||
"source": "16",
|
||||
"target": "22"
|
||||
},
|
||||
{
|
||||
"source": "17",
|
||||
"target": "18"
|
||||
},
|
||||
{
|
||||
"source": "17",
|
||||
"target": "20"
|
||||
},
|
||||
{
|
||||
"source": "18",
|
||||
"target": "19"
|
||||
},
|
||||
{
|
||||
"source": "19",
|
||||
"target": "20"
|
||||
},
|
||||
{
|
||||
"source": "19",
|
||||
"target": "33"
|
||||
},
|
||||
{
|
||||
"source": "19",
|
||||
"target": "22"
|
||||
},
|
||||
{
|
||||
"source": "19",
|
||||
"target": "23"
|
||||
},
|
||||
{
|
||||
"source": "20",
|
||||
"target": "21"
|
||||
},
|
||||
{
|
||||
"source": "21",
|
||||
"target": "22"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "24"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "25"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "26"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "23"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "28"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "30"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "31"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "32"
|
||||
},
|
||||
{
|
||||
"source": "22",
|
||||
"target": "33"
|
||||
},
|
||||
{
|
||||
"source": "23",
|
||||
"target": "28"
|
||||
},
|
||||
{
|
||||
"source": "23",
|
||||
"target": "27"
|
||||
},
|
||||
{
|
||||
"source": "23",
|
||||
"target": "29"
|
||||
},
|
||||
{
|
||||
"source": "23",
|
||||
"target": "30"
|
||||
},
|
||||
{
|
||||
"source": "23",
|
||||
"target": "31"
|
||||
},
|
||||
{
|
||||
"source": "23",
|
||||
"target": "33"
|
||||
},
|
||||
{
|
||||
"source": "32",
|
||||
"target": "33"
|
||||
}]
|
||||
};
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'mountNode',
|
||||
width: 1000,
|
||||
height: 600,
|
||||
modes: {
|
||||
default: ['drag-canvas', 'drag-node'],
|
||||
},
|
||||
layout: {
|
||||
center: [500, 300]
|
||||
},
|
||||
animate: true,
|
||||
defaultNode: {
|
||||
size: [20, 20],
|
||||
color: 'steelblue',
|
||||
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();
|
||||
|
||||
const tipDIv = document.getElementById('tip');
|
||||
|
||||
layoutMethodsTranslation();
|
||||
setInterval(() => {
|
||||
layoutMethodsTranslation();
|
||||
}, 10500);
|
||||
|
||||
function layoutMethodsTranslation() {
|
||||
setTimeout(() => {
|
||||
tipDIv.innerHTML = 'MDS Layout';
|
||||
graph.updateLayout({
|
||||
type: 'mds',
|
||||
// use web worker to layout
|
||||
workerEnabled: true
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
setTimeout(() => {
|
||||
tipDIv.innerHTML = 'Fruchterman Layout';
|
||||
graph.updateLayout({
|
||||
type: 'fruchterman',
|
||||
// use web worker to layout
|
||||
workerEnabled: true
|
||||
});
|
||||
}, 2500);
|
||||
|
||||
setTimeout(() => {
|
||||
tipDIv.innerHTML = 'Force-directed Layout';
|
||||
graph.updateLayout({
|
||||
type: 'force',
|
||||
linkDistance: 50,
|
||||
preventOverlap: true,
|
||||
nodeSize: 30,
|
||||
// use web worker to layout
|
||||
workerEnabled: true
|
||||
});
|
||||
}, 4500);
|
||||
|
||||
setTimeout(() => {
|
||||
tipDIv.innerHTML = 'Circular Layout';
|
||||
graph.updateLayout({
|
||||
type: 'circular',
|
||||
radius: 100,
|
||||
startAngle: Math.PI / 4,
|
||||
endAngle: Math.PI,
|
||||
divisions: 5,
|
||||
ordering: 'degree'
|
||||
});
|
||||
}, 7000);
|
||||
|
||||
setTimeout(() => {
|
||||
tipDIv.innerHTML = 'Radial Layout';
|
||||
graph.updateLayout({
|
||||
type: 'radial',
|
||||
preventOverlap: true,
|
||||
nodeSize: 20
|
||||
});
|
||||
}, 8500);
|
||||
|
||||
setTimeout(() => {
|
||||
tipDIv.innerHTML = 'Random Layout';
|
||||
graph.updateLayout({
|
||||
type: 'random',
|
||||
preventOverlap: true,
|
||||
nodeSize: 20
|
||||
});
|
||||
}, 10000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -100,7 +100,7 @@
|
||||
"prepublishOnly": "npm run build-lib && npm run build",
|
||||
"screenshot": "node ./bin/screenshot.js",
|
||||
"start": "npm run dev",
|
||||
"test": "torch --compile --renderer --opts test/mocha.opts --recursive ./test/unit",
|
||||
"test": "npm run build && torch --compile --renderer --opts test/mocha.opts --recursive ./test/unit",
|
||||
"test-live": "torch --compile --interactive --watch --opts test/mocha.opts --recursive ./test/unit",
|
||||
"test-live-shape": "torch --compile --interactive --watch --opts test/mocha.opts --recursive ./test/unit/shape/nodes/modelRect-spec.js",
|
||||
"test-live-util": "torch --compile --interactive --watch --opts test/mocha.opts --recursive ./test/unit/shape/edge-spec.js",
|
||||
|
@ -1,11 +1,33 @@
|
||||
const Layout = require('../../layout');
|
||||
const Util = require('../../util');
|
||||
const layoutConst = require('../../layout/worker/layoutConst');
|
||||
const LayoutWorker = require('../../layout/worker/layout.worker');
|
||||
|
||||
const { LAYOUT_MESSAGE } = layoutConst;
|
||||
|
||||
const helper = {
|
||||
// pollyfill
|
||||
requestAnimationFrame(callback) {
|
||||
const fn = window.requestAnimationFrame || window.webkitRequestAnimationFrame || function(callback) {
|
||||
return setTimeout(callback, 16);
|
||||
};
|
||||
return fn(callback);
|
||||
},
|
||||
cancelAnimationFrame(requestId) {
|
||||
const fn = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || function(requestId) {
|
||||
return clearTimeout(requestId);
|
||||
};
|
||||
return fn(requestId);
|
||||
}
|
||||
};
|
||||
|
||||
class LayoutController {
|
||||
constructor(graph) {
|
||||
this.graph = graph;
|
||||
this.layoutCfg = graph.get('layout');
|
||||
this.layoutType = this.layoutCfg ? this.layoutCfg.type : undefined;
|
||||
const layoutCfg = this.layoutCfg = graph.get('layout') || {};
|
||||
this.layoutType = layoutCfg.type;
|
||||
this.worker = null;
|
||||
this.workerData = {};
|
||||
this._initLayout();
|
||||
}
|
||||
|
||||
@ -13,7 +35,48 @@ class LayoutController {
|
||||
// no data before rendering
|
||||
}
|
||||
|
||||
layout() {
|
||||
// get layout worker and create one if not exists
|
||||
_getWorker() {
|
||||
if (this.worker) {
|
||||
return this.worker;
|
||||
}
|
||||
|
||||
if (typeof Worker === 'undefined') {
|
||||
// 如果当前浏览器不支持web worker,则不使用web worker
|
||||
console.warn('Web worker is not supported in current browser.');
|
||||
this.worker = null;
|
||||
} else {
|
||||
this.worker = new LayoutWorker();
|
||||
}
|
||||
return this.worker;
|
||||
}
|
||||
|
||||
// stop layout worker
|
||||
_stopWorker() {
|
||||
const { workerData } = this;
|
||||
|
||||
if (!this.worker) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.worker.terminate();
|
||||
this.worker = null;
|
||||
// 重新开始新的布局之前,先取消之前布局的requestAnimationFrame。
|
||||
if (workerData.requestId) {
|
||||
helper.cancelAnimationFrame(workerData.requestId);
|
||||
workerData.requestId = null;
|
||||
}
|
||||
if (workerData.requestId2) {
|
||||
helper.cancelAnimationFrame(workerData.requestId2);
|
||||
workerData.requestId2 = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function} success callback
|
||||
* @return {boolean} 是否使用web worker布局
|
||||
*/
|
||||
layout(success) {
|
||||
const self = this;
|
||||
let layoutType = self.layoutType;
|
||||
const graph = self.graph;
|
||||
@ -23,7 +86,7 @@ class LayoutController {
|
||||
const nodes = self.data.nodes;
|
||||
|
||||
if (!nodes) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
const width = graph.get('width');
|
||||
const height = graph.get('height');
|
||||
@ -33,13 +96,14 @@ class LayoutController {
|
||||
height,
|
||||
center: [ width / 2, height / 2 ]
|
||||
}, self.layoutCfg);
|
||||
self.layoutCfg = layoutCfg;
|
||||
|
||||
if (layoutType === undefined) {
|
||||
if (nodes[0] && nodes[0].x === undefined) {
|
||||
// 创建随机布局
|
||||
layoutType = 'random';
|
||||
layoutType = layoutCfg.type = 'random';
|
||||
} else { // 若未指定布局且数据中有位置信息,则不进行布局,直接按照原数据坐标绘制。
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (nodes[0] && nodes[0].x === undefined) {
|
||||
@ -52,6 +116,13 @@ class LayoutController {
|
||||
if (layoutMethod) {
|
||||
layoutMethod.destroy();
|
||||
}
|
||||
|
||||
this._stopWorker();
|
||||
if (layoutCfg.workerEnabled && this._layoutWithWorker(self.data, success)) {
|
||||
// 如果启用布局web worker并且浏览器支持web worker,用web worker布局。否则回退到不用web worker布局。
|
||||
return true;
|
||||
}
|
||||
|
||||
if (layoutType === 'force') {
|
||||
const onTick = layoutCfg.onTick;
|
||||
const tick = () => {
|
||||
@ -65,21 +136,125 @@ class LayoutController {
|
||||
graph.emit('afterlayout');
|
||||
};
|
||||
}
|
||||
self.layoutCfg = layoutCfg;
|
||||
|
||||
try {
|
||||
layoutMethod = new Layout[layoutType](layoutCfg);
|
||||
} catch (e) {
|
||||
console.warn('The layout method: ' + layoutCfg + ' does not exist! Please specify it first.');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
layoutMethod.init(self.data);
|
||||
graph.emit('beforelayout');
|
||||
layoutMethod.execute();
|
||||
self.layoutMethod = layoutMethod;
|
||||
if (layoutType !== 'force') {
|
||||
graph.emit('afterlayout');
|
||||
self.refreshLayout();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* layout with web worker
|
||||
* @param {object} data graph data
|
||||
* @param {function} success callback function
|
||||
* @return {boolean} 是否支持web worker
|
||||
*/
|
||||
_layoutWithWorker(data, success) {
|
||||
const { nodes, edges } = data;
|
||||
const { layoutCfg, graph } = this;
|
||||
const worker = this._getWorker();
|
||||
// 每次worker message event handler调用之间的共享数据,会被修改。
|
||||
const { workerData } = this;
|
||||
|
||||
if (!worker) {
|
||||
return false;
|
||||
}
|
||||
|
||||
workerData.requestId = null;
|
||||
workerData.requestId2 = null;
|
||||
workerData.currentTick = null;
|
||||
workerData.currentTickData = null;
|
||||
|
||||
graph.emit('beforelayout');
|
||||
// NOTE: postMessage的message参数里面不能包含函数,否则postMessage会报错,
|
||||
// 例如:'function could not be cloned'。
|
||||
// 详情参考:https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
|
||||
// 所以这里需要把过滤layoutCfg里的函数字段过滤掉。
|
||||
const filteredLayoutCfg = filterObject(layoutCfg, value => typeof value !== 'function');
|
||||
worker.postMessage({ type: LAYOUT_MESSAGE.RUN, nodes, edges, layoutCfg: filteredLayoutCfg });
|
||||
worker.onmessage = event => {
|
||||
this._handleWorkerMessage(event, data, success);
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
// success callback will be called when updating graph positions for the first time.
|
||||
_handleWorkerMessage(event, data, success) {
|
||||
const { graph, workerData, layoutCfg } = this;
|
||||
const eventData = event.data;
|
||||
const { type } = eventData;
|
||||
const onTick = () => {
|
||||
if (layoutCfg.onTick) {
|
||||
layoutCfg.onTick();
|
||||
}
|
||||
};
|
||||
const onLayoutEnd = () => {
|
||||
if (layoutCfg.onLayoutEnd) {
|
||||
layoutCfg.onLayoutEnd();
|
||||
}
|
||||
graph.emit('afterlayout');
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case LAYOUT_MESSAGE.TICK:
|
||||
workerData.currentTick = eventData.currentTick;
|
||||
workerData.currentTickData = eventData;
|
||||
if (!workerData.requestId) {
|
||||
workerData.requestId = helper.requestAnimationFrame(function() {
|
||||
updateLayoutPosition(data, eventData);
|
||||
graph.refreshPositions();
|
||||
onTick();
|
||||
if (eventData.currentTick === 1 && success) {
|
||||
success();
|
||||
}
|
||||
|
||||
if (eventData.currentTick === eventData.totalTicks) {
|
||||
// 如果是最后一次tick
|
||||
onLayoutEnd();
|
||||
} else if (workerData.currentTick === eventData.totalTicks) {
|
||||
// 注意这里workerData.currentTick可能已经不再是前面赋值时候的值了,
|
||||
// 因为在requestAnimationFrame等待时间里,可能产生新的tick。
|
||||
// 如果当前tick不是最后一次tick,并且所有的tick消息都已发出来了,那么需要用最后一次tick的数据再刷新一次。
|
||||
workerData.requestId2 = helper.requestAnimationFrame(function() {
|
||||
updateLayoutPosition(data, workerData.currentTickData);
|
||||
graph.refreshPositions();
|
||||
workerData.requestId2 = null;
|
||||
onTick();
|
||||
onLayoutEnd();
|
||||
});
|
||||
}
|
||||
workerData.requestId = null;
|
||||
});
|
||||
}
|
||||
break;
|
||||
case LAYOUT_MESSAGE.END:
|
||||
// 如果没有tick消息(非力导布局)
|
||||
if (workerData.currentTick == null) {
|
||||
updateLayoutPosition(data, eventData);
|
||||
this.refreshLayout();
|
||||
// 非力导布局,没有tick消息,只有end消息,所以需要执行一次回调。
|
||||
if (success) {
|
||||
success();
|
||||
}
|
||||
graph.emit('afterlayout');
|
||||
}
|
||||
break;
|
||||
case LAYOUT_MESSAGE.ERROR:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
self.layoutMethod = layoutMethod;
|
||||
}
|
||||
|
||||
// 绘制
|
||||
@ -100,6 +275,13 @@ class LayoutController {
|
||||
self.layoutType = cfg.type;
|
||||
const layoutMethod = self.layoutMethod;
|
||||
self.data = self.setDataFromGraph();
|
||||
|
||||
this._stopWorker();
|
||||
if (cfg.workerEnabled && this._layoutWithWorker(self.data, null)) {
|
||||
// 如果启用布局web worker并且浏览器支持web worker,用web worker布局。否则回退到不用web worker布局。
|
||||
return;
|
||||
}
|
||||
|
||||
layoutMethod.init(self.data);
|
||||
layoutMethod.updateCfg(cfg);
|
||||
graph.emit('beforelayout');
|
||||
@ -119,7 +301,6 @@ class LayoutController {
|
||||
const layoutMethod = self.layoutMethod;
|
||||
layoutMethod && layoutMethod.destroy();
|
||||
self.layout();
|
||||
self.refreshLayout();
|
||||
}
|
||||
|
||||
// 更换数据
|
||||
@ -221,8 +402,35 @@ class LayoutController {
|
||||
self.graph = null;
|
||||
const layoutMethod = self.layoutMethod;
|
||||
layoutMethod && layoutMethod.destroy();
|
||||
const { worker } = this;
|
||||
if (worker) {
|
||||
worker.terminate();
|
||||
this.worker = null;
|
||||
}
|
||||
self.destroyed = true;
|
||||
}
|
||||
}
|
||||
|
||||
function updateLayoutPosition(data, layoutData) {
|
||||
const { nodes } = data;
|
||||
const { nodes: layoutNodes } = layoutData;
|
||||
nodes.forEach((node, i) => {
|
||||
node.x = layoutNodes[i].x;
|
||||
node.y = layoutNodes[i].y;
|
||||
});
|
||||
}
|
||||
|
||||
function filterObject(collection, callback) {
|
||||
const result = {};
|
||||
if (collection && typeof collection === 'object') {
|
||||
for (const key in collection) {
|
||||
if (collection.hasOwnProperty(key) && callback(collection[key])) {
|
||||
result[key] = collection[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return collection;
|
||||
}
|
||||
|
||||
module.exports = LayoutController;
|
||||
|
@ -546,15 +546,18 @@ class Graph extends EventEmitter {
|
||||
|
||||
// layout
|
||||
const layoutController = self.get('layoutController');
|
||||
layoutController.layout();
|
||||
self.refreshPositions();
|
||||
|
||||
if (self.get('fitView')) {
|
||||
self.get('viewController')._fitView();
|
||||
if (!layoutController.layout(success)) {
|
||||
success();
|
||||
}
|
||||
|
||||
function success() {
|
||||
if (self.get('fitView')) {
|
||||
self.get('viewController')._fitView();
|
||||
}
|
||||
self.paint();
|
||||
self.setAutoPaint(autoPaint);
|
||||
self.emit('afterrender');
|
||||
}
|
||||
self.paint();
|
||||
self.setAutoPaint(autoPaint);
|
||||
self.emit('afterrender');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -649,11 +652,6 @@ class Graph extends EventEmitter {
|
||||
this.set({ nodes: items.nodes, edges: items.edges });
|
||||
const layoutController = this.get('layoutController');
|
||||
layoutController.changeData();
|
||||
if (self.get('animate')) {
|
||||
self.positionsAnimate();
|
||||
} else {
|
||||
this.paint();
|
||||
}
|
||||
this.setAutoPaint(autoPaint);
|
||||
return this;
|
||||
}
|
||||
@ -1179,8 +1177,9 @@ class Graph extends EventEmitter {
|
||||
if (!newLayoutType || oriLayoutType === newLayoutType) {
|
||||
// no type or same type, update layout
|
||||
const layoutCfg = {};
|
||||
Util.mix(layoutCfg, cfg);
|
||||
Util.mix(layoutCfg, oriLayoutCfg, cfg);
|
||||
layoutCfg.type = oriLayoutType ? oriLayoutType : 'random';
|
||||
this.set('layout', layoutCfg);
|
||||
layoutController.updateLayoutCfg(layoutCfg);
|
||||
} else { // has different type, change layout
|
||||
this.set('layout', cfg);
|
||||
@ -1193,6 +1192,13 @@ class Graph extends EventEmitter {
|
||||
*/
|
||||
layout() {
|
||||
const layoutController = this.get('layoutController');
|
||||
const layoutCfg = this.get('layout');
|
||||
|
||||
if (layoutCfg.workerEnabled) {
|
||||
// 如果使用web worker布局
|
||||
layoutController.layout();
|
||||
return;
|
||||
}
|
||||
if (layoutController.layoutMethod) {
|
||||
layoutController.relayout();
|
||||
} else {
|
||||
|
@ -4,11 +4,14 @@
|
||||
*/
|
||||
|
||||
const d3Force = require('d3-force');
|
||||
const Layout = require('./layout');
|
||||
const Util = require('../util');
|
||||
const isArray = require('@antv/util/lib/type/is-array');
|
||||
const isNumber = require('@antv/util/lib/type/is-number');
|
||||
const isFunction = require('@antv/util/lib/type/is-function');
|
||||
const Layout = require('./layout');
|
||||
const Util = require('../util/layout');
|
||||
const layoutConst = require('./worker/layoutConst');
|
||||
|
||||
const { LAYOUT_MESSAGE } = layoutConst;
|
||||
|
||||
/**
|
||||
* 经典力导布局 force-directed
|
||||
@ -30,7 +33,9 @@ Layout.registerLayout('force', {
|
||||
collideStrength: 1, // 防止重叠的力强度
|
||||
tick() {},
|
||||
onLayoutEnd() {}, // 布局完成回调
|
||||
onTick() {} // 每一迭代布局回调
|
||||
onTick() {}, // 每一迭代布局回调
|
||||
// 是否启用web worker。前提是在web worker里执行布局,否则无效
|
||||
workerEnabled: false
|
||||
};
|
||||
},
|
||||
/**
|
||||
@ -71,14 +76,8 @@ Layout.registerLayout('force', {
|
||||
.force('charge', nodeForce)
|
||||
.alpha(alpha)
|
||||
.alphaDecay(alphaDecay)
|
||||
.alphaMin(alphaMin)
|
||||
.on('tick', () => {
|
||||
self.tick();
|
||||
})
|
||||
.on('end', () => {
|
||||
self.ticking = false;
|
||||
self.onLayoutEnd && self.onLayoutEnd();
|
||||
});
|
||||
.alphaMin(alphaMin);
|
||||
|
||||
if (self.preventOverlap) {
|
||||
self.overlapProcess(simulation);
|
||||
}
|
||||
@ -101,8 +100,34 @@ Layout.registerLayout('force', {
|
||||
}
|
||||
simulation.force('link', edgeForce);
|
||||
}
|
||||
|
||||
if (self.workerEnabled && !isInWorker()) {
|
||||
// 如果不是运行在web worker里,不用web worker布局
|
||||
self.workerEnabled = false;
|
||||
console.warn('workerEnabled option is only supported when running in web worker.');
|
||||
}
|
||||
if (!self.workerEnabled) {
|
||||
simulation
|
||||
.on('tick', () => {
|
||||
self.tick();
|
||||
})
|
||||
.on('end', () => {
|
||||
self.ticking = false;
|
||||
self.onLayoutEnd && self.onLayoutEnd();
|
||||
});
|
||||
self.ticking = true;
|
||||
} else {
|
||||
simulation.stop();
|
||||
const totalTicks = getSimulationTicks(simulation);
|
||||
for (let currentTick = 1; currentTick <= totalTicks; currentTick++) {
|
||||
simulation.tick();
|
||||
// currentTick starts from 1.
|
||||
postMessage({ type: LAYOUT_MESSAGE.TICK, currentTick, totalTicks, nodes });
|
||||
}
|
||||
self.ticking = false;
|
||||
}
|
||||
|
||||
self.forceSimulation = simulation;
|
||||
self.ticking = true;
|
||||
} catch (e) {
|
||||
self.ticking = false;
|
||||
console.warn(e);
|
||||
@ -194,3 +219,20 @@ Layout.registerLayout('force', {
|
||||
self.destroyed = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Return total ticks of d3-force simulation
|
||||
function getSimulationTicks(simulation) {
|
||||
const alphaMin = simulation.alphaMin();
|
||||
const alphaTarget = simulation.alphaTarget();
|
||||
const alpha = simulation.alpha();
|
||||
const totalTicksFloat = Math.log((alphaMin - alphaTarget) / (alpha - alphaTarget)) / Math.log(1 - simulation.alphaDecay());
|
||||
const totalTicks = Math.ceil(totalTicksFloat);
|
||||
|
||||
return totalTicks;
|
||||
}
|
||||
|
||||
// 判断是否运行在web worker里
|
||||
function isInWorker() {
|
||||
// eslint-disable-next-line no-undef
|
||||
return typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
* @author shiwu.wyy@antfin.com
|
||||
*/
|
||||
|
||||
const Util = require('../util');
|
||||
const Util = require('../util/layout');
|
||||
const Layout = {};
|
||||
|
||||
/**
|
||||
|
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
const Layout = require('./layout');
|
||||
const Util = require('../util');
|
||||
const Util = require('../util/layout');
|
||||
const Numeric = require('numericjs');
|
||||
|
||||
/**
|
||||
|
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
const Layout = require('../layout');
|
||||
const Util = require('../../util');
|
||||
const Util = require('../../util/layout');
|
||||
const RadialNonoverlapForce = require('./radialNonoverlapForce');
|
||||
const MDS = require('./mds');
|
||||
const isArray = require('@antv/util/lib/type/is-array');
|
||||
|
45
src/layout/worker/layout.worker.js
Normal file
45
src/layout/worker/layout.worker.js
Normal file
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @fileoverview web worker for layout
|
||||
* @author changzhe.zb@antfin.com
|
||||
*/
|
||||
const Layout = require('..');
|
||||
const layoutConst = require('./layoutConst');
|
||||
|
||||
const { LAYOUT_MESSAGE } = layoutConst;
|
||||
|
||||
function isLayoutMessage(event) {
|
||||
const { type } = event.data;
|
||||
return type === LAYOUT_MESSAGE.RUN;
|
||||
}
|
||||
|
||||
function handleLayoutMessage(event) {
|
||||
const { type } = event.data;
|
||||
|
||||
switch (type) {
|
||||
case LAYOUT_MESSAGE.RUN: {
|
||||
const { nodes, edges, layoutCfg = {} } = event.data;
|
||||
const { type: layoutType } = layoutCfg;
|
||||
const LayoutClass = Layout[layoutType];
|
||||
if (!LayoutClass) {
|
||||
postMessage({ type: LAYOUT_MESSAGE.ERROR, message: `layout ${layoutType} not found` });
|
||||
break;
|
||||
}
|
||||
|
||||
const layoutMethod = new LayoutClass(layoutCfg);
|
||||
layoutMethod.init({ nodes, edges });
|
||||
layoutMethod.execute();
|
||||
postMessage({ type: LAYOUT_MESSAGE.END, nodes });
|
||||
layoutMethod.destroy();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// listen to message posted to web worker
|
||||
self.onmessage = event => {
|
||||
if (isLayoutMessage(event)) {
|
||||
handleLayoutMessage(event);
|
||||
}
|
||||
};
|
19
src/layout/worker/layoutConst.js
Normal file
19
src/layout/worker/layoutConst.js
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @fileoverview constants for layout
|
||||
* @author changzhe.zb@antfin.com
|
||||
*/
|
||||
// layout message type
|
||||
const LAYOUT_MESSAGE = {
|
||||
// run layout
|
||||
RUN: 'LAYOUT_RUN',
|
||||
// layout ended with success
|
||||
END: 'LAYOUT_END',
|
||||
// layout error
|
||||
ERROR: 'LAYOUT_ERROR',
|
||||
// layout tick, used in force directed layout
|
||||
TICK: 'LAYOUT_TICK'
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
LAYOUT_MESSAGE
|
||||
};
|
83
src/util/layout.js
Normal file
83
src/util/layout.js
Normal file
@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @fileoverview util for layout
|
||||
* @author changzhe.zb@antfin.com
|
||||
*/
|
||||
const layoutUtil = {
|
||||
mix: require('@antv/util/lib/mix'),
|
||||
augment: require('@antv/util/lib/augment'),
|
||||
isString: require('@antv/util/lib/type/is-string'),
|
||||
getAdjMatrix(data, directed) {
|
||||
const nodes = data.nodes;
|
||||
const edges = data.edges;
|
||||
const matrix = [];
|
||||
// map node with index in data.nodes
|
||||
const nodeMap = new Map();
|
||||
nodes.forEach((node, i) => {
|
||||
nodeMap.set(node.id, i);
|
||||
const row = [];
|
||||
matrix.push(row);
|
||||
});
|
||||
|
||||
// const n = nodes.length;
|
||||
edges.forEach(e => {
|
||||
const source = e.source;
|
||||
const target = e.target;
|
||||
const sIndex = nodeMap.get(source);
|
||||
const tIndex = nodeMap.get(target);
|
||||
matrix[sIndex][tIndex] = 1;
|
||||
if (!directed) matrix[tIndex][sIndex] = 1;
|
||||
});
|
||||
return matrix;
|
||||
},
|
||||
/**
|
||||
* Floyd Warshall algorithm for shortest path distances matrix
|
||||
* @param {array} adjMatrix adjacency matrix
|
||||
* @return {array} distances shortest path distances matrix
|
||||
*/
|
||||
floydWarshall(adjMatrix) {
|
||||
// initialize
|
||||
const dist = [];
|
||||
const size = adjMatrix.length;
|
||||
for (let i = 0; i < size; i += 1) {
|
||||
dist[i] = [];
|
||||
for (let j = 0; j < size; j += 1) {
|
||||
if (i === j) {
|
||||
dist[i][j] = 0;
|
||||
} else if (adjMatrix[i][j] === 0 || !adjMatrix[i][j]) {
|
||||
dist[i][j] = Infinity;
|
||||
} else {
|
||||
dist[i][j] = adjMatrix[i][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
// floyd
|
||||
for (let k = 0; k < size; k += 1) {
|
||||
for (let i = 0; i < size; i += 1) {
|
||||
for (let j = 0; j < size; j += 1) {
|
||||
if (dist[i][j] > dist[i][k] + dist[k][j]) {
|
||||
dist[i][j] = dist[i][k] + dist[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return dist;
|
||||
},
|
||||
|
||||
getEDistance(p1, p2) {
|
||||
return Math.sqrt((p1[0] - p2[0]) * (p1[0] - p2[0])
|
||||
+ (p1[1] - p2[1]) * (p1[1] - p2[1]));
|
||||
},
|
||||
|
||||
scaleMatrix(matrix, scale) {
|
||||
const result = [];
|
||||
matrix.forEach(row => {
|
||||
const newRow = [];
|
||||
row.forEach(v => {
|
||||
newRow.push(v * scale);
|
||||
});
|
||||
result.push(newRow);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
};
|
||||
module.exports = layoutUtil;
|
@ -180,63 +180,7 @@ const MathUtil = {
|
||||
y: vector[1]
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Floyd Warshall algorithm for shortest path distances matrix
|
||||
* @param {array} adjMatrix adjacency matrix
|
||||
* @return {array} distances shortest path distances matrix
|
||||
*/
|
||||
floydWarshall(adjMatrix) {
|
||||
// initialize
|
||||
const dist = [];
|
||||
const size = adjMatrix.length;
|
||||
for (let i = 0; i < size; i += 1) {
|
||||
dist[i] = [];
|
||||
for (let j = 0; j < size; j += 1) {
|
||||
if (i === j) {
|
||||
dist[i][j] = 0;
|
||||
} else if (adjMatrix[i][j] === 0 || !adjMatrix[i][j]) {
|
||||
dist[i][j] = Infinity;
|
||||
} else {
|
||||
dist[i][j] = adjMatrix[i][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
// floyd
|
||||
for (let k = 0; k < size; k += 1) {
|
||||
for (let i = 0; i < size; i += 1) {
|
||||
for (let j = 0; j < size; j += 1) {
|
||||
if (dist[i][j] > dist[i][k] + dist[k][j]) {
|
||||
dist[i][j] = dist[i][k] + dist[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return dist;
|
||||
},
|
||||
|
||||
getAdjMatrix(data, directed) {
|
||||
const nodes = data.nodes;
|
||||
const edges = data.edges;
|
||||
const matrix = [];
|
||||
// map node with index in data.nodes
|
||||
const nodeMap = new Map();
|
||||
nodes.forEach((node, i) => {
|
||||
nodeMap.set(node.id, i);
|
||||
const row = [];
|
||||
matrix.push(row);
|
||||
});
|
||||
|
||||
// const n = nodes.length;
|
||||
edges.forEach(e => {
|
||||
const source = e.source;
|
||||
const target = e.target;
|
||||
const sIndex = nodeMap.get(source);
|
||||
const tIndex = nodeMap.get(target);
|
||||
matrix[sIndex][tIndex] = 1;
|
||||
if (!directed) matrix[tIndex][sIndex] = 1;
|
||||
});
|
||||
return matrix;
|
||||
},
|
||||
/**
|
||||
* if the graph about the shortest path matrix is connected
|
||||
* @param {array} matrix shortest path matrix
|
||||
@ -251,23 +195,6 @@ const MathUtil = {
|
||||
return true;
|
||||
},
|
||||
|
||||
getEDistance(p1, p2) {
|
||||
return Math.sqrt((p1[0] - p2[0]) * (p1[0] - p2[0])
|
||||
+ (p1[1] - p2[1]) * (p1[1] - p2[1]));
|
||||
},
|
||||
|
||||
scaleMatrix(matrix, scale) {
|
||||
const result = [];
|
||||
matrix.forEach(row => {
|
||||
const newRow = [];
|
||||
row.forEach(v => {
|
||||
newRow.push(v * scale);
|
||||
});
|
||||
result.push(newRow);
|
||||
});
|
||||
return result;
|
||||
},
|
||||
|
||||
randomInitPos(size, xRange = [ 0, 1 ], yRange = [ 0, 1 ]) {
|
||||
const positions = [];
|
||||
for (let i = 0; i < size; i++) {
|
||||
|
67
test/unit/layout/circular-web-worker-spec.js
Normal file
67
test/unit/layout/circular-web-worker-spec.js
Normal file
@ -0,0 +1,67 @@
|
||||
const expect = require('chai').expect;
|
||||
// 注意:这里不能直接require原始的src文件,而要使用build后的文件,因为web worker代码是通过worker-loader内联进来的。
|
||||
const G6 = require('../../../build/g6');
|
||||
const data = require('./data.json');
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'circular-layout-web-worker';
|
||||
document.body.appendChild(div);
|
||||
|
||||
function mathEqual(a, b) {
|
||||
return Math.abs(a - b) < 1;
|
||||
}
|
||||
|
||||
describe.only('circular layout(web worker)', () => {
|
||||
it('circular layout(web worker) with default configs', done => {
|
||||
const graph = new G6.Graph({
|
||||
container: div,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
// use web worker to layout
|
||||
workerEnabled: true
|
||||
},
|
||||
width: 500,
|
||||
height: 500,
|
||||
defaultNode: { size: 10 }
|
||||
});
|
||||
graph.data(data);
|
||||
graph.on('afterlayout', () => {
|
||||
const width = graph.get('width');
|
||||
const height = graph.get('height');
|
||||
const radius = height > width ? width / 2 : height / 2;
|
||||
expect(mathEqual(data.nodes[0].x, 250 + radius)).to.equal(true);
|
||||
expect(mathEqual(data.nodes[0].y, 250)).to.equal(true);
|
||||
expect(data.nodes[0].y === 250);
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
graph.render();
|
||||
});
|
||||
|
||||
it('circular(web worker) counterclockwise, and fixed radius, start angle, end angle', done => {
|
||||
const graph = new G6.Graph({
|
||||
container: div,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
center: [ 250, 250 ],
|
||||
radius: 200,
|
||||
startAngle: Math.PI / 4,
|
||||
endAngle: Math.PI,
|
||||
// use web worker to layout
|
||||
workerEnabled: true
|
||||
},
|
||||
width: 500,
|
||||
height: 500,
|
||||
defaultNode: { size: 10 }
|
||||
});
|
||||
graph.data(data);
|
||||
graph.on('afterlayout', () => {
|
||||
const pos = 200 * Math.sqrt(2) / 2;
|
||||
expect(mathEqual(data.nodes[0].x, 250 + pos)).to.equal(true);
|
||||
expect(mathEqual(data.nodes[0].y, 250 + pos)).to.equal(true);
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
graph.render();
|
||||
});
|
||||
});
|
47
test/unit/layout/force-web-worker-spec.js
Normal file
47
test/unit/layout/force-web-worker-spec.js
Normal file
@ -0,0 +1,47 @@
|
||||
const expect = require('chai').expect;
|
||||
// 注意:这里不能直接require原始的src文件,而要使用build后的文件,因为web worker代码是通过worker-loader内联进来的。
|
||||
const G6 = require('../../../build/g6');
|
||||
const data = require('./data.json');
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'force-layout-web-worker';
|
||||
document.body.appendChild(div);
|
||||
|
||||
describe.only('force layout(web worker)', function() {
|
||||
this.timeout(10000);
|
||||
|
||||
it('force layout(web worker) with default configs', function(done) {
|
||||
const node = data.nodes[0];
|
||||
let count = 0;
|
||||
let ended = false;
|
||||
const graph = new G6.Graph({
|
||||
container: div,
|
||||
layout: {
|
||||
type: 'force',
|
||||
onTick() {
|
||||
count++;
|
||||
expect(node.x).to.not.be.undefined;
|
||||
expect(node.y).to.not.be.undefined;
|
||||
},
|
||||
onLayoutEnd() {
|
||||
ended = true;
|
||||
},
|
||||
// use web worker to layout
|
||||
workerEnabled: true
|
||||
},
|
||||
width: 500,
|
||||
height: 500,
|
||||
defaultNode: { size: 10 }
|
||||
});
|
||||
graph.data(data);
|
||||
graph.on('afterlayout', () => {
|
||||
expect(node.x).to.not.be.undefined;
|
||||
expect(node.y).to.not.be.undefined;
|
||||
expect(count >= 1).to.be.true;
|
||||
expect(ended).to.be.true;
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
graph.render();
|
||||
});
|
||||
});
|
68
test/unit/layout/web-worker-spec.js
Normal file
68
test/unit/layout/web-worker-spec.js
Normal file
@ -0,0 +1,68 @@
|
||||
const expect = require('chai').expect;
|
||||
// 注意:这里不能直接require原始的src文件,而要使用build后的文件,因为web worker代码是通过worker-loader内联进来的。
|
||||
const G6 = require('../../../build/g6');
|
||||
const data = require('./data.json');
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'layout-web-worker';
|
||||
document.body.appendChild(div);
|
||||
|
||||
function mathEqual(a, b) {
|
||||
return Math.abs(a - b) < 1;
|
||||
}
|
||||
|
||||
describe.only('layout using web worker', function() {
|
||||
this.timeout(10000);
|
||||
|
||||
it('change layout', function(done) {
|
||||
const node = data.nodes[0];
|
||||
const graph = new G6.Graph({
|
||||
container: div,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
// use web worker to layout
|
||||
workerEnabled: true
|
||||
},
|
||||
width: 500,
|
||||
height: 500,
|
||||
defaultNode: { size: 10 }
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
// 下面的graph.updateLayout又会触发一次afterLayout,为了避免这里的event handler重复执行,
|
||||
// 这里用了graph.one.
|
||||
graph.one('afterlayout', () => {
|
||||
expect(mathEqual(node.x, 500)).to.equal(true);
|
||||
expect(mathEqual(node.y, 250)).to.equal(true);
|
||||
});
|
||||
graph.render();
|
||||
|
||||
let count = 0;
|
||||
let ended = false;
|
||||
|
||||
setTimeout(() => {
|
||||
// 只执行一次
|
||||
graph.one('afterlayout', () => {
|
||||
expect(node.x).to.not.be.undefined;
|
||||
expect(node.y).to.not.be.undefined;
|
||||
expect(count >= 1).to.be.true;
|
||||
expect(ended).to.be.true;
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
graph.updateLayout({
|
||||
type: 'force',
|
||||
onTick() {
|
||||
count++;
|
||||
expect(node.x).to.not.be.undefined;
|
||||
expect(node.y).to.not.be.undefined;
|
||||
},
|
||||
onLayoutEnd() {
|
||||
ended = true;
|
||||
},
|
||||
// use web worker to layout
|
||||
workerEnabled: true
|
||||
});
|
||||
}, 1000);
|
||||
});
|
||||
});
|
@ -44,6 +44,20 @@ module.exports = {
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
// 用于web worker代码。注意这条规则必须在.js规则前面,
|
||||
// 这样.worker.js会经过['worker-loader', 'babel-loader']处理
|
||||
test: /\.worker\.js$/,
|
||||
exclude: /(node_modules|bower_components)/,
|
||||
use: {
|
||||
loader: 'worker-loader',
|
||||
options: {
|
||||
inline: true,
|
||||
fallback: false,
|
||||
name: 'g6Layout.worker.js'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /(node_modules|bower_components)/,
|
||||
@ -53,13 +67,6 @@ module.exports = {
|
||||
babelrc: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.worker.js$/,
|
||||
loader: 'worker-loader',
|
||||
options: {
|
||||
inline: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user