feat: add plugin force layout

This commit is contained in:
elaine 2019-05-20 17:55:55 +08:00
parent 2531219aa3
commit d0a791e708
4 changed files with 783 additions and 1 deletions

View File

@ -116,7 +116,8 @@
"dependencies": {
"@antv/g": "~3.4.4",
"@antv/hierarchy": "~0.5.0",
"@antv/util": "~1.3.1"
"@antv/util": "~1.3.1",
"d3-force": "^2.0.1"
},
"engines": {
"node": ">=8.9.0"

113
plugins/force/index.js Normal file
View File

@ -0,0 +1,113 @@
const d3Force = require('d3-force');
const Base = require('../base');
class Force extends Base {
getDefaultCfgs() {
return {
center: [ 0, 0 ], // 向心力作用点
nodeStrength: null, // 节点作用力
preventOverlap: false, // 是否防止节点相互覆盖
nodeRadius: null, // 节点半径
edgeStrength: null, // 边的作用力, 默认为根据节点的入度出度自适应
linkDistance: 50, // 默认边长度
forceSimulation: null, // 自定义 force 方法
onLayoutEnd() {}, // 布局完成回调
onTick() {} // 每一迭代布局回调
};
}
layout(data) {
const self = this;
// 如果正在布局,忽略布局请求
if (self.isTicking()) {
return;
}
const cfgs = self._cfgs;
let simulation = cfgs.forceSimulation;
if (!simulation) {
try {
// 定义节点的力
const nodeForce = d3Force.forceManyBody();
if (cfgs.nodeStrength) {
nodeForce.strength(cfgs.nodeStrength);
}
simulation = d3Force.forceSimulation()
.nodes(data.nodes)
.force('center', d3Force.forceCenter(cfgs.center[0], cfgs.center[1]))
.force('charge', nodeForce)
.on('tick', cfgs.tick)
.on('end', () => {
self.set('ticking', false);
cfgs.onLayoutEnd && cfgs.onLayoutEnd();
});
if (cfgs.preventOverlap) {
let nodeRadius = cfgs.nodeRadius;
if (!nodeRadius) {
nodeRadius = d => {
if (d.size) {
if (Array.isArray(d.size)) {
return d.size[0] / 2;
}
return d.size / 2;
}
return 20;
};
}
simulation.force('collide', d3Force.forceCollide().radius(nodeRadius));
}
// 如果有边,定义边的力
if (data.edges) {
// d3 的 forceLayout 会重新生成边的数据模型,为了避免污染源数据
const edges = data.edges.map(edge => {
return {
id: edge.id,
source: edge.source,
target: edge.target
};
});
const edgeForce = d3Force.forceLink().id(function(d) { return d.id; }).links(edges);
if (cfgs.edgeStrength) {
edgeForce.strength(cfgs.edgeStrength);
}
if (cfgs.distance) {
edgeForce.distance(cfgs.distance);
}
simulation.force('link', edgeForce);
}
this.set('forceSimulation', simulation);
this.set('ticking', true);
} catch (e) {
this.set('ticking', false);
console.warn(e);
}
} else {
simulation.alphaTarget(0.3).restart();
this.set('ticking', true);
}
}
updateLayout(cfg) {
const self = this;
const simulation = self.getSimulation();
if (self.get('ticking')) {
simulation.stop();
self.set('ticking', false);
}
this.set('forceSimulation', null);
this.set('tick', null);
Object.keys(cfg).forEach(key => {
self.set(key, cfg[key]);
});
}
isTicking() {
return this.get('ticking');
}
getSimulation() {
return this.get('forceSimulation');
}
destroy() {
if (this.get('ticking')) {
this.getSimulation().stop();
}
super.destroy();
}
}
module.exports = Force;

547
test/unit/plugins/data.json Normal file
View File

@ -0,0 +1,547 @@
{
"nodes": [{
"id": "Argentina",
"name": "Argentina"
}, {
"id": "Australia",
"name": "Australia"
}, {
"id": "Belgium",
"name": "Belgium"
}, {
"id": "Brazil",
"name": "Brazil"
}, {
"id": "Colombia",
"name": "Colombia"
}, {
"id": "Costa Rica",
"name": "Costa Rica"
}, {
"id": "Croatia",
"name": "Croatia"
}, {
"id": "Denmark",
"name": "Denmark"
}, {
"id": "Egypt",
"name": "Egypt"
}, {
"id": "England",
"name": "England"
}, {
"id": "France",
"name": "France"
}, {
"id": "Germany",
"name": "Germany"
}, {
"id": "Iceland",
"name": "Iceland"
}, {
"id": "IR Iran",
"name": "IR Iran"
}, {
"id": "Japan",
"name": "Japan"
}, {
"id": "Korea Republic",
"name": "Korea Republic"
}, {
"id": "Mexico",
"name": "Mexico"
}, {
"id": "Morocco",
"name": "Morocco"
}, {
"id": "Nigeria",
"name": "Nigeria"
}, {
"id": "Panama",
"name": "Panama"
}, {
"id": "Peru",
"name": "Peru"
}, {
"id": "Poland",
"name": "Poland"
}, {
"id": "Portugal",
"name": "Portugal"
}, {
"id": "Russia",
"name": "Russia"
}, {
"id": "Saudi Arabia",
"name": "Saudi Arabia"
}, {
"id": "Senegal",
"name": "Senegal"
}, {
"id": "Serbia",
"name": "Serbia"
}, {
"id": "Spain",
"name": "Spain"
}, {
"id": "Sweden",
"name": "Sweden"
}, {
"id": "Switzerland",
"name": "Switzerland"
}, {
"id": "Tunisia",
"name": "Tunisia"
}, {
"id": "Uruguay",
"name": "Uruguay"
}],
"edges": [{
"id": "0",
"target": "Russia",
"source": "Saudi Arabia",
"target_score": 5,
"source_score": 0,
"directed": true
}, {
"id": "1",
"target": "Uruguay",
"source": "Egypt",
"target_score": 1,
"source_score": 0,
"directed": true
}, {
"id": "2",
"target": "Russia",
"source": "Egypt",
"target_score": 3,
"source_score": 1,
"directed": true
}, {
"id": "3",
"target": "Uruguay",
"source": "Saudi Arabia",
"target_score": 1,
"source_score": 0,
"directed": true
}, {
"id": "4",
"target": "Uruguay",
"source": "Russia",
"target_score": 3,
"source_score": 0,
"directed": true
}, {
"id": "5",
"target": "Saudi Arabia",
"source": "Egypt",
"target_score": 2,
"source_score": 1,
"directed": true
}, {
"id": "6",
"target": "IR Iran",
"source": "Morocco",
"target_score": 1,
"source_score": 0,
"directed": true
}, {
"id": "7",
"target": "Portugal",
"source": "Spain",
"target_score": 3,
"source_score": 3,
"directed": false
}, {
"id": "8",
"target": "Portugal",
"source": "Morocco",
"target_score": 1,
"source_score": 0,
"directed": true
}, {
"id": "9",
"target": "Spain",
"source": "IR Iran",
"target_score": 1,
"source_score": 0,
"directed": true
}, {
"id": "10",
"target": "IR Iran",
"source": "Portugal",
"target_score": 1,
"source_score": 1,
"directed": false
}, {
"id": "11",
"target": "Spain",
"source": "Morocco",
"target_score": 2,
"source_score": 2,
"directed": false
}, {
"id": "12",
"target": "France",
"source": "Australia",
"target_score": 2,
"source_score": 1,
"directed": true
}, {
"id": "13",
"target": "Denmark",
"source": "Peru",
"target_score": 1,
"source_score": 0,
"directed": true
}, {
"id": "14",
"target": "Denmark",
"source": "Australia",
"target_score": 1,
"source_score": 1,
"directed": false
}, {
"id": "15",
"target": "France",
"source": "Peru",
"target_score": 1,
"source_score": 0,
"directed": true
}, {
"id": "16",
"target": "Denmark",
"source": "France",
"target_score": 0,
"source_score": 0,
"directed": false
}, {
"id": "17",
"target": "Peru",
"source": "Australia",
"target_score": 2,
"source_score": 0,
"directed": true
}, {
"id": "18",
"target": "Argentina",
"source": "Iceland",
"target_score": 1,
"source_score": 1
}, {
"id": "19",
"target": "Croatia",
"source": "Nigeria",
"target_score": 2,
"source_score": 0,
"directed": true
}, {
"id": "20",
"target": "Croatia",
"source": "Argentina",
"target_score": 3,
"source_score": 0,
"directed": true
}, {
"id": "21",
"target": "Nigeria",
"source": "Iceland",
"target_score": 2,
"source_score": 0,
"directed": true
}, {
"id": "22",
"target": "Argentina",
"source": "Nigeria",
"target_score": 2,
"source_score": 1,
"directed": true
}, {
"id": "23",
"target": "Croatia",
"source": "Iceland",
"target_score": 2,
"source_score": 1,
"directed": true
}, {
"id": "24",
"target": "Serbia",
"source": "Costa Rica",
"target_score": 1,
"source_score": 0,
"directed": true
}, {
"id": "25",
"target": "Brazil",
"source": "Switzerland",
"target_score": 1,
"source_score": 1,
"directed": false
}, {
"id": "26",
"target": "Brazil",
"source": "Costa Rica",
"target_score": 2,
"source_score": 0,
"directed": true
}, {
"id": "27",
"target": "Switzerland",
"source": "Serbia",
"target_score": 2,
"source_score": 1,
"directed": true
}, {
"id": "28",
"target": "Brazil",
"source": "Serbia",
"target_score": 2,
"source_score": 0,
"directed": true
}, {
"id": "29",
"target": "Switzerland",
"source": "Costa Rica",
"target_score": 2,
"source_score": 2,
"directed": false
}, {
"id": "30",
"target": "Mexico",
"source": "Germany",
"target_score": 1,
"source_score": 0,
"directed": true
}, {
"id": "31",
"target": "Sweden",
"source": "Korea Republic",
"target_score": 1,
"source_score": 0,
"directed": true
}, {
"id": "32",
"target": "Mexico",
"source": "Korea Republic",
"target_score": 1,
"source_score": 0,
"directed": true
}, {
"id": "33",
"target": "Germany",
"source": "Sweden",
"target_score": 2,
"source_score": 1,
"directed": true
}, {
"id": "34",
"target": "Korea Republic",
"source": "Germany",
"target_score": 2,
"source_score": 0,
"directed": true
}, {
"id": "35",
"target": "Sweden",
"source": "Mexico",
"target_score": 3,
"source_score": 0,
"directed": true
}, {
"id": "36",
"target": "Belgium",
"source": "Panama",
"target_score": 3,
"source_score": 0,
"directed": true
}, {
"id": "37",
"target": "England",
"source": "Tunisia",
"target_score": 2,
"source_score": 1,
"directed": true
}, {
"id": "38",
"target": "Belgium",
"source": "Tunisia",
"target_score": 5,
"source_score": 2,
"directed": true
}, {
"id": "39",
"target": "England",
"source": "Panama",
"target_score": 6,
"source_score": 1,
"directed": true
}, {
"id": "40",
"target": "Belgium",
"source": "England",
"target_score": 1,
"source_score": 0,
"directed": true
}, {
"id": "41",
"target": "Tunisia",
"source": "Panama",
"target_score": 2,
"source_score": 1,
"directed": true
}, {
"id": "42",
"target": "Japan",
"source": "Colombia",
"target_score": 2,
"source_score": 1,
"directed": true
}, {
"id": "43",
"target": "Senegal",
"source": "Poland",
"target_score": 2,
"source_score": 1,
"directed": true
}, {
"id": "44",
"target": "Japan",
"source": "Senegal",
"target_score": 2,
"source_score": 2,
"directed": false
}, {
"id": "45",
"target": "Colombia",
"source": "Poland",
"target_score": 3,
"source_score": 0,
"directed": true
}, {
"id": "46",
"target": "Poland",
"source": "Japan",
"target_score": 1,
"source_score": 0,
"directed": true
}, {
"id": "47",
"target": "Colombia",
"source": "Senegal",
"target_score": 1,
"source_score": 0,
"directed": true
}, {
"id": "48",
"target": "Uruguay",
"source": "Portugal",
"target_score": 2,
"source_score": 1,
"directed": true
}, {
"id": "49",
"target": "France",
"source": "Argentina",
"target_score": 4,
"source_score": 3,
"directed": true
}, {
"id": "50",
"target": "Russia",
"source": "Spain",
"target_score": 5,
"source_score": 4,
"directed": true
}, {
"id": "51",
"target": "Croatia",
"source": "Denmark",
"target_score": 4,
"source_score": 3,
"directed": true
}, {
"id": "52",
"target": "Brazil",
"source": "Mexico",
"target_score": 2,
"source_score": 0,
"directed": true
}, {
"id": "53",
"target": "Belgium",
"source": "Japan",
"target_score": 3,
"source_score": 2,
"directed": true
}, {
"id": "54",
"target": "Sweden",
"source": "Switzerland",
"target_score": 1,
"source_score": 0,
"directed": true
}, {
"id": "55",
"target": "England",
"source": "Colombia",
"target_score": 4,
"source_score": 3,
"directed": true
}, {
"id": "56",
"target": "France",
"source": "Uruguay",
"target_score": 2,
"source_score": 0,
"directed": true
}, {
"id": "57",
"target": "Belgium",
"source": "Brazil",
"target_score": 2,
"source_score": 1,
"directed": true
}, {
"id": "58",
"target": "Croatia",
"source": "Russia",
"target_score": 6,
"source_score": 5,
"directed": true
}, {
"id": "59",
"target": "England",
"source": "Sweden",
"target_score": 2,
"source_score": 0,
"directed": true
}, {
"id": "60",
"target": "France",
"source": "Belgium",
"target_score": 1,
"source_score": 0,
"directed": true
}, {
"id": "61",
"target": "Croatia",
"source": "England",
"target_score": 2,
"source_score": 1,
"directed": true
}, {
"id": "62",
"target": "Belgium",
"source": "England",
"target_score": 2,
"source_score": 0,
"directed": true
}, {
"id": "63",
"target": "France",
"source": "Croatia",
"target_score": 4,
"source_score": 2,
"directed": true
}]
}

View File

@ -0,0 +1,121 @@
const expect = require('chai').expect;
// const G6 = require('../../../src');
const Force = require('../../../plugins/force');
const data = require('./data.json');
const div = document.createElement('div');
div.id = 'force-layout';
document.body.appendChild(div);
describe('force layout', () => {
/* const graph = new G6.Graph({
container: div,
width: 500,
height: 500,
defaultNode: { size: 10 }
});*/
it('force layout with default configs', done => {
const node = data.nodes[0];
const edge = data.edges[0];
let x,
y,
count = 0;
const force = new Force({
center: [ 250, 250 ],
tick() {
count++;
expect(node.x !== x);
expect(node.y !== y);
expect(edge.x).to.be.undefined;
expect(edge.y).to.be.undefined;
x = node.x;
y = node.y;
},
onLayoutEnd() {
expect(node.x);
expect(node.y);
expect(edge.x).to.be.undefined;
expect(edge.y).to.be.undefined;
}
});
force.layout(data);
setTimeout(() => {
expect(count > 30);
done();
}, 500);
});
it('force with fixed link length', done => {
const node = data.nodes[0];
const edge = data.edges[0];
let x,
y,
count = 0;
const force = new Force({
center: [ 250, 250 ],
distance: 40,
edgeStrength: 0.5,
tick() {
count++;
expect(node.x !== x);
expect(node.y !== y);
expect(edge.x).to.be.undefined;
expect(edge.y).to.be.undefined;
x = node.x;
y = node.y;
},
onLayoutEnd() {
expect(node.x);
expect(node.y);
expect(edge.x).to.be.undefined;
expect(edge.y).to.be.undefined;
}
});
force.layout(data);
setTimeout(() => {
force.layout(data);
expect(count > 30);
done();
}, 500);
});
it('force collide & update', done => {
const node = data.nodes[0];
const edge = data.edges[0];
let x,
y,
count = 0;
const force = new Force({
center: [ 250, 250 ],
preventOverlap: true,
nodeRadius(d) {
d.size = Math.random() * 100 + 10;
return d.size / 2;
},
nodeStrength: 10,
tick() {
count++;
expect(node.x !== x);
expect(node.y !== y);
expect(edge.x).to.be.undefined;
expect(edge.y).to.be.undefined;
x = node.x;
y = node.y;
},
onLayoutEnd() {
expect(node.x);
expect(node.y);
expect(edge.x).to.be.undefined;
expect(edge.y).to.be.undefined;
}
});
force.layout({ nodes: data.nodes });
setTimeout(() => {
force.updateLayout({
preventOverlap: false
});
expect(force.get('preventOverlap')).to.be.false;
expect(count > 30);
done();
}, 500);
});
});