feat: fruchterman GPU version.

This commit is contained in:
Yanyan-Wang 2020-06-10 09:50:25 +08:00 committed by Yanyan Wang
parent 1c3c49b9fc
commit 26e1ae69bc
13 changed files with 1732 additions and 28 deletions

View File

@ -0,0 +1,31 @@
---
title: API
---
## center
**Type**: Array<br />**Example**: [ 0, 0 ]<br />**Default**: The center of the graph<br />**Required**: false<br />**Description**: The center of the layout
## maxIteration
**Type**: Number<br />**Default**: 1000<br />**Required**: false<br />**Description**: The maximum iteration number
## gravity
**Type**: Number<br />**Default**: 10<br />**Required**: false<br />**Description**: The gravity, which will affect the compactness of the layout
## speed
**Type**: Number<br />**Default**: 1<br />**Required**: false<br />**Description**: The moving speed of each iteraction. Large value of the speed might lead to violent swing
## clustering
**Type**: Boolean<br />**Default**: false<br />**Required**: false<br />**Description**: Whether to layout by cluster
## clusterGravity
**Type**: Number<br />**Default**: 10<br />**Required**: false<br />**Description**: The gravity of each cluster, which will affect the compactness of each cluster. Takes effect only when `clustering` is `true`
## workerEnabled
**Type**: Boolean<br />**Default**: false<br />**Required**: false<br />**Description**: Whether to enable the web-worker in case layout calculation takes too long to block page interaction

View File

@ -0,0 +1,31 @@
---
title: API
---
## center
**类型** Array<br />**示例**[ 0, 0 ]<br />**默认值**:图的中心<br />**是否必须**false<br />**说明**:布局的中心
## maxIteration
**类型** Number<br />**默认值**1000<br />**是否必须**false<br />**说明**:最大迭代次数
## gravity
**类型** Number<br />**默认值**10<br />**是否必须**false<br />**说明**:重力的大小,影响布局的紧凑程度
## speed
**类型** Number<br />**默认值**1<br />**是否必须**false<br />**说明**:每次迭代节点移动的速度。速度太快可能会导致强烈震荡
## clustering
**类型** Boolean<br />**默认值**false<br />**是否必须**false<br />**说明**:是否按照聚类布局
## clusterGravity
**类型** Number<br />**默认值**10<br />**是否必须**false<br />**说明**:聚类内部的重力大小,影响聚类的紧凑程度,在 `clustering``true` 时生效
## workerEnabled
**类型**: Boolean<br />**默认值**: false<br />**是否必须**: false<br />**说明**: 是否启用 web-worker 以防布局计算时间过长阻塞页面交互

View File

@ -0,0 +1,462 @@
import G6 from '@antv/g6';
const data = {
nodes: [
{
id: '0',
label: '0',
cluster: 'a',
},
{
id: '1',
label: '1',
cluster: 'a',
},
{
id: '2',
label: '2',
cluster: 'a',
},
{
id: '3',
label: '3',
cluster: 'a',
},
{
id: '4',
label: '4',
cluster: 'a',
},
{
id: '5',
label: '5',
cluster: 'a',
},
{
id: '6',
label: '6',
cluster: 'a',
},
{
id: '7',
label: '7',
cluster: 'a',
},
{
id: '8',
label: '8',
cluster: 'a',
},
{
id: '9',
label: '9',
cluster: 'a',
},
{
id: '10',
label: '10',
cluster: 'a',
},
{
id: '11',
label: '11',
cluster: 'a',
},
{
id: '12',
label: '12',
cluster: 'a',
},
{
id: '13',
label: '13',
cluster: 'b',
},
{
id: '14',
label: '14',
cluster: 'b',
},
{
id: '15',
label: '15',
cluster: 'b',
},
{
id: '16',
label: '16',
cluster: 'b',
},
{
id: '17',
label: '17',
cluster: 'b',
},
{
id: '18',
label: '18',
cluster: 'c',
},
{
id: '19',
label: '19',
cluster: 'c',
},
{
id: '20',
label: '20',
cluster: 'c',
},
{
id: '21',
label: '21',
cluster: 'c',
},
{
id: '22',
label: '22',
cluster: 'c',
},
{
id: '23',
label: '23',
cluster: 'c',
},
{
id: '24',
label: '24',
cluster: 'c',
},
{
id: '25',
label: '25',
cluster: 'c',
},
{
id: '26',
label: '26',
cluster: 'c',
},
{
id: '27',
label: '27',
cluster: 'c',
},
{
id: '28',
label: '28',
cluster: 'c',
},
{
id: '29',
label: '29',
cluster: 'c',
},
{
id: '30',
label: '30',
cluster: 'c',
},
{
id: '31',
label: '31',
cluster: 'd',
},
{
id: '32',
label: '32',
cluster: 'd',
},
{
id: '33',
label: '33',
cluster: 'd',
},
],
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 width = document.getElementById('container').scrollWidth;
const height = document.getElementById('container').scrollHeight || 500;
const graph = new G6.Graph({
container: 'container',
width,
height,
modes: {
default: ['drag-canvas', 'drag-node'],
},
animate: true,
defaultNode: {
size: 30,
style: {
lineWidth: 2,
stroke: '#5B8FF9',
fill: '#C6E5FF',
},
},
defaultEdge: {
size: 1,
color: '#e2e2e2',
style: {
endArrow: {
path: 'M 0,0 L 8,4 L 8,-4 Z',
fill: '#e2e2e2'
},
},
},
});
graph.data(data);
graph.render();
const gpuLayout = new G6.Layout['fruchtermanGPU']({
canvasEl: graph.get('canvas').get('el'),
width,
height,
onLayoutEnd: () => {
graph.refreshPositions();
}
})
gpuLayout.init(data);
gpuLayout.execute();

View File

@ -0,0 +1,496 @@
import G6 from '@antv/g6';
const data = {
nodes: [
{
id: '0',
label: '0',
cluster: 'a',
},
{
id: '1',
label: '1',
cluster: 'a',
},
{
id: '2',
label: '2',
cluster: 'a',
},
{
id: '3',
label: '3',
cluster: 'a',
},
{
id: '4',
label: '4',
cluster: 'a',
},
{
id: '5',
label: '5',
cluster: 'a',
},
{
id: '6',
label: '6',
cluster: 'a',
},
{
id: '7',
label: '7',
cluster: 'a',
},
{
id: '8',
label: '8',
cluster: 'a',
},
{
id: '9',
label: '9',
cluster: 'a',
},
{
id: '10',
label: '10',
cluster: 'a',
},
{
id: '11',
label: '11',
cluster: 'a',
},
{
id: '12',
label: '12',
cluster: 'a',
},
{
id: '13',
label: '13',
cluster: 'b',
},
{
id: '14',
label: '14',
cluster: 'b',
},
{
id: '15',
label: '15',
cluster: 'b',
},
{
id: '16',
label: '16',
cluster: 'b',
},
{
id: '17',
label: '17',
cluster: 'b',
},
{
id: '18',
label: '18',
cluster: 'c',
},
{
id: '19',
label: '19',
cluster: 'c',
},
{
id: '20',
label: '20',
cluster: 'c',
},
{
id: '21',
label: '21',
cluster: 'c',
},
{
id: '22',
label: '22',
cluster: 'c',
},
{
id: '23',
label: '23',
cluster: 'c',
},
{
id: '24',
label: '24',
cluster: 'c',
},
{
id: '25',
label: '25',
cluster: 'c',
},
{
id: '26',
label: '26',
cluster: 'c',
},
{
id: '27',
label: '27',
cluster: 'c',
},
{
id: '28',
label: '28',
cluster: 'c',
},
{
id: '29',
label: '29',
cluster: 'c',
},
{
id: '30',
label: '30',
cluster: 'c',
},
{
id: '31',
label: '31',
cluster: 'd',
},
{
id: '32',
label: '32',
cluster: 'd',
},
{
id: '33',
label: '33',
cluster: 'd',
},
],
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 colors = [
'#BDD2FD',
'#BDEFDB',
'#C2C8D5',
'#FBE5A2',
'#F6C3B7',
'#B6E3F5',
'#D3C6EA',
'#FFD8B8',
'#AAD8D8',
'#FFD6E7',
];
const strokes = [
'#5B8FF9',
'#5AD8A6',
'#5D7092',
'#F6BD16',
'#E8684A',
'#6DC8EC',
'#9270CA',
'#FF9D4D',
'#269A99',
'#FF99C3',
];
const nodes = data.nodes;
const clusterMap = new Map();
let clusterId = 0;
nodes.forEach(function(node) {
// cluster
if (node.cluster && clusterMap.get(node.cluster) === undefined) {
clusterMap.set(node.cluster, clusterId);
clusterId++;
}
const cid = clusterMap.get(node.cluster);
if (!node.style) {
node.style = {};
}
node.style.fill = colors[cid % colors.length];
node.style.stroke = strokes[cid % strokes.length];
});
const graphDiv = document.getElementById('container');
const width = graphDiv.scrollWidth;
const height = graphDiv.scrollHeight;
const graph = new G6.Graph({
container: 'container',
width,
height,
modes: {
default: ['drag-canvas', 'drag-node'],
},
layout: {
type: 'fruchterman',
gravity: 10,
speed: 5,
clustering: true,
},
animate: true,
defaultNode: {
size: 20,
style: {
lineWidth: 2,
},
},
defaultEdge: {
size: 1,
color: '#e2e2e2',
style: {
endArrow: {
path: 'M 0,0 L 8,4 L 8,-4 Z',
fill: '#e2e2e2'
},
},
},
});
graph.data(data);
graph.render();

View File

@ -0,0 +1,51 @@
import G6 from '@antv/g6';
const width = document.getElementById('container').scrollWidth;
const height = document.getElementById('container').scrollHeight || 500;
const graph = new G6.Graph({
container: 'container',
width,
height,
modes: {
default: ['drag-canvas', 'drag-node'],
},
animate: true,
defaultNode: {
size: 10,
style: {
lineWidth: 2,
stroke: '#5B8FF9',
fill: '#C6E5FF',
},
},
defaultEdge: {
size: 1,
color: '#e2e2e2',
style: {
endArrow: {
path: 'M 0,0 L 8,4 L 8,-4 Z',
fill: '#e2e2e2'
},
},
},
});
fetch('https://gw.alipayobjects.com/os/basement_prod/7bacd7d1-4119-4ac1-8be3-4c4b9bcbc25f.json')
.then(res => res.json())
.then(data => {
graph.data(data);
graph.render();
const gpuLayout = new G6.Layout['fruchtermanGPU']({
canvasEl: graph.get('canvas').get('el'),
width,
height,
maxIteration: 1000,
onLayoutEnd: () => {
graph.refreshPositions();
}
})
gpuLayout.init(data);
gpuLayout.execute();
});

View File

@ -0,0 +1,538 @@
import G6 from '@antv/g6';
const data = {
nodes: [
{
id: '0',
label: '0',
cluster: 'a',
},
{
id: '1',
label: '1',
cluster: 'a',
},
{
id: '2',
label: '2',
cluster: 'a',
},
{
id: '3',
label: '3',
cluster: 'a',
},
{
id: '4',
label: '4',
cluster: 'a',
},
{
id: '5',
label: '5',
cluster: 'a',
},
{
id: '6',
label: '6',
cluster: 'a',
},
{
id: '7',
label: '7',
cluster: 'a',
},
{
id: '8',
label: '8',
cluster: 'a',
},
{
id: '9',
label: '9',
cluster: 'a',
},
{
id: '10',
label: '10',
cluster: 'a',
},
{
id: '11',
label: '11',
cluster: 'a',
},
{
id: '12',
label: '12',
cluster: 'a',
},
{
id: '13',
label: '13',
cluster: 'b',
},
{
id: '14',
label: '14',
cluster: 'b',
},
{
id: '15',
label: '15',
cluster: 'b',
},
{
id: '16',
label: '16',
cluster: 'b',
},
{
id: '17',
label: '17',
cluster: 'b',
},
{
id: '18',
label: '18',
cluster: 'c',
},
{
id: '19',
label: '19',
cluster: 'c',
},
{
id: '20',
label: '20',
cluster: 'c',
},
{
id: '21',
label: '21',
cluster: 'c',
},
{
id: '22',
label: '22',
cluster: 'c',
},
{
id: '23',
label: '23',
cluster: 'c',
},
{
id: '24',
label: '24',
cluster: 'c',
},
{
id: '25',
label: '25',
cluster: 'c',
},
{
id: '26',
label: '26',
cluster: 'c',
},
{
id: '27',
label: '27',
cluster: 'c',
},
{
id: '28',
label: '28',
cluster: 'c',
},
{
id: '29',
label: '29',
cluster: 'c',
},
{
id: '30',
label: '30',
cluster: 'c',
},
{
id: '31',
label: '31',
cluster: 'd',
},
{
id: '32',
label: '32',
cluster: 'd',
},
{
id: '33',
label: '33',
cluster: 'd',
},
],
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 colors = [
'#BDD2FD',
'#BDEFDB',
'#C2C8D5',
'#FBE5A2',
'#F6C3B7',
'#B6E3F5',
'#D3C6EA',
'#FFD8B8',
'#AAD8D8',
'#FFD6E7',
];
const strokes = [
'#5B8FF9',
'#5AD8A6',
'#5D7092',
'#F6BD16',
'#E8684A',
'#6DC8EC',
'#9270CA',
'#FF9D4D',
'#269A99',
'#FF99C3',
];
const nodes = data.nodes;
const clusterMap = new Map();
let clusterId = 0;
nodes.forEach(function(node) {
// cluster
if (node.cluster && clusterMap.get(node.cluster) === undefined) {
clusterMap.set(node.cluster, clusterId);
clusterId++;
}
const cid = clusterMap.get(node.cluster);
if (!node.style) {
node.style = {};
}
node.style.fill = colors[cid % colors.length];
node.style.stroke = strokes[cid % strokes.length];
});
const graphDiv = document.getElementById('container');
const descriptionDiv = document.createElement('div');
descriptionDiv.innerHTML = 'Fruchterman layout, gravity = 1';
graphDiv.appendChild(descriptionDiv);
const width = graphDiv.scrollWidth;
const height = graphDiv.scrollHeight - 30;
const graph = new G6.Graph({
container: 'container',
width,
height,
modes: {
default: ['drag-canvas', 'drag-node'],
},
layout: {
type: 'fruchterman',
gravity: 1,
speed: 5,
},
animate: true,
defaultNode: {
size: 20,
style: {
lineWidth: 2,
},
},
defaultEdge: {
size: 1,
color: '#e2e2e2',
style: {
endArrow: {
path: 'M 0,0 L 8,4 L 8,-4 Z',
fill: '#e2e2e2'
},
},
},
});
graph.data(data);
graph.render();
layoutConfigTranslation();
function layoutConfigTranslation() {
setTimeout(function() {
descriptionDiv.innerHTML = 'Fructherman layout, gravity = 5';
graph.updateLayout({
gravity: 5,
});
}, 1000);
setTimeout(function() {
descriptionDiv.innerHTML = 'Fructherman layout, gravity = 10, layout by cluster';
graph.updateLayout({
gravity: 10,
clustering: true,
});
}, 2500);
setTimeout(function() {
descriptionDiv.innerHTML = 'Fructherman layout, gravity = 20, layout by cluster';
graph.updateLayout({
gravity: 20,
});
}, 4000);
setTimeout(function() {
descriptionDiv.innerHTML = 'Fructherman layout, gravity = 50, layout by cluster';
graph.updateLayout({
gravity: 50,
});
}, 5500);
setTimeout(function() {
descriptionDiv.innerHTML = 'Fructherman layout, gravity = 80, layout by cluster';
graph.updateLayout({
gravity: 80,
});
}, 7000);
}

View File

@ -0,0 +1,40 @@
{
"title": {
"zh": "中文分类",
"en": "Category"
},
"demos": [
{
"filename": "basicFruchterman.js",
"title": {
"zh": "基本 Fruchterman 布局",
"en": "Basic Fruchterman Layout"
},
"screenshot": "https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*1KY7SLEXxqMAAAAAAAAAAABkARQnAQ"
},
{
"filename": "fruchtermanComplexData.js",
"title": {
"zh": "Fruchterman 复杂数据",
"en": "Fruchterman with Complex Data"
},
"screenshot": "https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*0sl9RZ7Cp28AAAAAAAAAAABkARQnAQ"
},
{
"filename": "fruchtermanClustering.js",
"title": {
"zh": "Fruchterman 聚类布局",
"en": "Fruchterman with Clustering"
},
"screenshot": "https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*WO1OTbNE_ugAAAAAAAAAAABkARQnAQ"
},
{
"filename": "fruchtermanConfigurationTranslate.js",
"title": {
"zh": "Fruchterman 布局参数动态变化",
"en": "Update the Configurations for Fruchterman"
},
"screenshot": "https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*W9MoQoKaQbYAAAAAAAAAAABkARQnAQ"
}
]
}

View File

@ -0,0 +1,15 @@
---
title: GPU Layout
order: 12
---
Fruchterman Reingold Layout is a kind of force-directed layout in theory. The differences are the definitions of attracitve force and repulsive force.
## Usage
As the demo below, you can deploy it in `layout` while instantiating Graph. it can also be used for [Subgraph Layout](/zh/docs/manual/middle/layout/#%E5%AD%90%E5%9B%BE%E5%B8%83%E5%B1%80). By tuning the parameters, you can adjust the iteration number, layout compactness, layout by clusters, and so on.
- Example 1 : Basic Fruchterman layout.
- Example 2 : Fruchterman clustering layout.
- Example 3 : Translate the layout parameters in dynamic.
- Example 4 : Fruchterman layout with web-worker in case layout calculation takes too long to block page interaction.

View File

@ -0,0 +1,15 @@
---
title: GPU 图布局
order: 12
---
Fruchterman Reingold 布局算法在原理上而言属于力导向布局算法。其引力与斥力的定义方式与经典的 Force Diected 力导向图布局有少许不同。
## 使用指南
G6 内置的 Fruchterman 布局可在实例化 Graph 时使用该布局。除此之外,还可以如[子图布局](/zh/docs/manual/middle/layout/#%E5%AD%90%E5%9B%BE%E5%B8%83%E5%B1%80)所示单独使用布局。该布局可以通过配置调整迭代次数、紧凑程度、是否按照聚类布局等。
- 代码演示 1 :基本的 Fruchterman 布局。
- 代码演示 2 Fruchterman 的聚类布局。
- 代码演示 3 Fruchterman 布局参数动态变化。
- 代码演示 4 Fruchterman 使用 web-worker 以避免阻塞页面。

View File

@ -1,4 +1,4 @@
// window.g6 = require('./src/index.ts'); // import the source for debugging
window.g6 = require('./dist/g6.min.js'); // import the package for webworker
window.g6 = require('./src/index.ts'); // import the source for debugging
// window.g6 = require('./dist/g6.min.js'); // import the package for webworker
window.insertCss = require('insert-css');
window.Chart = require('@antv/chart-node-g6');

View File

@ -3,10 +3,9 @@
* @author shiwu.wyy@antfin.com
*/
import { EdgeConfig, IPointTuple, NodeConfig, NodeIdxMap } from '../types';
import { BaseLayout } from './layout';
import { EdgeConfig, IPointTuple, NodeConfig, NodeIdxMap } from '../../types';
import { BaseLayout } from '../layout';
import { isNumber } from '@antv/util';
import { Point } from '@antv/g-base';
import { World } from '@antv/g-webgpu';
const lineIndexBufferData = [];
@ -56,6 +55,7 @@ const convertWebGLCoord2Canvas = (c: number, size: number) => {
return ((c + 1) / 2) * size;
}
const gCode = `
import { globalInvocationID } from 'g-webgpu';
@ -74,6 +74,12 @@ class Fruchterman {
@in
u_K2: float;
@in
u_CenterX: float;
@in
u_CenterY: float;
@in
u_Gravity: float;
@ -86,15 +92,14 @@ class Fruchterman {
calcRepulsive(i: int, currentNode: vec4): vec2 {
let dx = 0, dy = 0;
for (let j = 0; j < VERTEX_COUNT; j++) {
if (i != j + 1) {
if (i != j) {
const nextNode = this.u_Data[j];
const xDist = currentNode[0] - nextNode[0];
const yDist = currentNode[1] - nextNode[1];
const dist = sqrt(xDist * xDist + yDist * yDist) + 0.01;
const dist = (xDist * xDist + yDist * yDist) + 0.01;
if (dist > 0.0) {
const repulsiveF = this.u_K2 / dist;
dx += xDist / dist * repulsiveF;
dy += yDist / dist * repulsiveF;
dx += this.u_K2 * xDist / dist ;
dy += this.u_K2 * yDist / dist ;
}
}
}
@ -102,9 +107,10 @@ class Fruchterman {
}
calcGravity(currentNode: vec4): vec2 {
const d = sqrt(currentNode[0] * currentNode[0] + currentNode[1] * currentNode[1]);
const gf = 0.01 * this.u_K * this.u_Gravity * d;
return [gf * currentNode[0] / d, gf * currentNode[1] / d];
const vx = currentNode[0] - this.u_CenterX;
const vy = currentNode[1] - this.u_CenterY;
const gf = 0.01 * this.u_K * this.u_Gravity;
return [gf * vx, gf * vy];
}
calcAttractive(currentNode: vec4): vec2 {
@ -128,10 +134,10 @@ class Fruchterman {
const xDist = currentNode[0] - nextNode[0];
const yDist = currentNode[1] - nextNode[1];
const dist = sqrt(xDist * xDist + yDist * yDist) + 0.01;
const attractiveF = dist * dist / this.u_K;
const attractiveF = dist / this.u_K;
if (dist > 0.0) {
dx -= xDist / dist * attractiveF;
dy -= yDist / dist * attractiveF;
dx -= xDist * attractiveF;
dy -= yDist * attractiveF;
}
}
return [dx, dy];
@ -180,6 +186,8 @@ class Fruchterman {
currentNode[3]
];
}
//const currentDis = this.u_MaxDisplace;
//this.u_MaxDisplace = currentDis * 0.99;
}
}
`;
@ -207,7 +215,7 @@ export default class FruchtermanGPULayout extends BaseLayout {
/** 重力大小,影响图的紧凑程度 */
public gravity: number = 10;
/** 速度 */
public speed: number = 1;
public speed: number = 0.1;
/** 是否产生聚类力 */
public clustering: boolean = false;
/** 聚类力大小 */
@ -230,7 +238,7 @@ export default class FruchtermanGPULayout extends BaseLayout {
maxIteration: 1000,
center: [0, 0],
gravity: 10,
speed: 1,
speed: 0.1,
clustering: false,
clusterGravity: 10,
};
@ -277,8 +285,10 @@ export default class FruchtermanGPULayout extends BaseLayout {
self.height = window.innerHeight;
}
const center = self.center;
const maxDisplace = self.width / 10;
const k = Math.sqrt((self.width * self.height) / (nodes.length + 1));
const area = self.height * self.width;
const maxDisplace = Math.sqrt(area) / 10;
const k2 = area / (nodes.length + 1);
const k = Math.sqrt(k2);
const gravity = self.gravity;
const speed = self.speed;
const clustering = self.clustering;
@ -332,13 +342,14 @@ export default class FruchtermanGPULayout extends BaseLayout {
const compute = world.createComputePipeline({
shader: gCode,
dispatch: [numParticles, 1, 1],
maxIteration: self.maxIteration,
maxIteration,
onCompleted: (finalParticleData) => {
console.log(maxIteration, gravity, center, k, k2, maxDisplace, speed)
self.nodes.forEach((node, i) => {
const x = finalParticleData[4 * i];
const y = finalParticleData[4 * i + 1];
node.x = convertWebGLCoord2Canvas(x, self.width);
node.y = convertWebGLCoord2Canvas(y, self.height);
node.x = x; //convertWebGLCoord2Canvas(x, self.width);
node.y = y; //convertWebGLCoord2Canvas(y, self.height);
});
self.onLayoutEnd && self.onLayoutEnd();
// setTimeElapsed(window.performance.now() - timeStart);
@ -357,20 +368,32 @@ export default class FruchtermanGPULayout extends BaseLayout {
world.setBinding(
compute,
'u_K',
Math.sqrt((numParticles * numParticles) / (numParticles + 1) / 300),
k,
);
world.setBinding(
compute,
'u_K2',
(numParticles * numParticles) / (numParticles + 1) / 300 / 300,
k2,
);
world.setBinding(compute, 'u_Gravity', 50);
world.setBinding(compute, 'u_Speed', 0.1);
world.setBinding(
compute,
'u_CenterX',
self.width / 2,
);
world.setBinding(
compute,
'u_CenterY',
self.height / 2,
);
world.setBinding(compute, 'u_Gravity', gravity);
world.setBinding(compute, 'u_Speed', speed);
world.setBinding(
compute,
'u_MaxDisplace',
Math.sqrt(numParticles * numParticles) / 10,
maxDisplace,
);
world.setBinding(compute, 'u_CenterX', center[0]);
world.setBinding(compute, 'u_CenterY', center[1]);
world.setBinding(compute, 'MAX_EDGE_PER_VERTEX', maxEdgePerVetex);
world.setBinding(compute, 'VERTEX_COUNT', numParticles);
}

View File

@ -17,6 +17,7 @@ import MDS from './mds';
import Radial from './radial/radial';
import Random from './random';
import ComboForce from './comboForce';
import FruchtermanGPU from './gpu/fruchterman';
const layouts = {
circular: Circular,
@ -30,6 +31,7 @@ const layouts = {
mds: MDS,
radial: Radial,
random: Random,
fruchtermanGPU: FruchtermanGPU
};
// 注册布局