0%

Force Directed Graph Layout

Force Directed Graph Layout

Algorithm

算法优化

对于所有连接起来的相邻点,可把其当成一个整体,计算弹簧弹力的时候,可以只计算一个整体和另一个整体之间的弹簧弹力,然后再把这个力附加到每一个个体中。

同理计算斥力的时候也可取一堆点的平均分布,代替计算两点间的斥力。

我大概就只会这样了,不过计算斥力的时候我并没有用整体算。

计算力的时候,我把引力和弹簧弹力一起算的,用 if 判断是否需要计算弹力。

不考虑随机生成数据所用时间,算法的时间复杂度是 $O(n^2)$ 。

效果图

有两层图,颜色浅的那层是初始数据,颜色深的是计算后的数据。

随机生成 200 个节点:

image.png

随机生成 800 个节点:

image.png

Play & Have Fun

force directed graph







Source Code

<html>
<meta charset="utf-8">
<header>force directed graph</header>
<style>
#svgHolder {
position: relative
}
.svg2 {
position: absolute;
top: 0px;
left: 0px;
}
</style>
<body>
<label for = "n_cnt">Nodes Count:</label>
<input type = "number" id = "n_cnt" name = "n_cnt" value = "60" min = "30" max = "1000"></input>
<button type = "button" onclick = "update()">Generate</button><br></br>
<div id = "svgHolder">
<svg id = "svgOne" width ="1190" height = "590"></svg>
<svg id = "svgTwo" class = "svg2" width ="1190" height = "590"></svg>
</div>
<script>
var margin = {top: 10, right: 10, bottom: 10, left: 10},
width = 1200 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom;

var svgns = "http://www.w3.org/2000/svg";

var colorset1 = ['#FFADAD', '#FFC2A9', '#FFD6A5', '#FDFFB6', '#CAFFBF', '#9BF6FF', '#A0C4FF', '#BDB2FF', '#FFC6FF', '#FFFFFC'];
var colorset2 = ['#F94144', '#F3722C', '#F8961E', '#F9C74F', '#90BE6D', '#52838F', '#577590', '#7209B7', '#B5179E', '#EDF2F4'];

let o_nodes = [], nodes = [];
var node_cnt = 60, maxNodeX = width - 2, maxNodeY = height - 2;
var connect = [], con_fsx = [], con_fsy = [], con_fs = [];

var L = 50, K_r = 6250, K_s = 1, delta_t = 0.04;
var MAX_DIS_SQR = 50;// ???

gen_d();
draw_old();
cal_force();
update_position();

draw_new()

function update() {
document.getElementById('svgOne').innerHTML = "";
document.getElementById('svgTwo').innerHTML = "";
o_nodes.length = 0, nodes.length = 0, node_cnt = document.getElementById("n_cnt").value;
connect.length = 0, con_fsx.length = 0, con_fsy.length = 0, con_fs.length = 0;
gen_d();
draw_old();
cal_force();
update_position();

draw_new();
}

function update2(_l, _kr, _ks, _dt, _msd) {
document.getElementById('svgTwo').innerHTML = "";
if (_l != -1)
L = _l;
if (_kr != -1)
K_r = _kr;
if (_ks != -1)
K_s = _ks;
if (_dt != -1)
delta_t = _dt;
if (_msd != -1)
MAX_DIS_SQR = _msd;
con_fs.length = 0;
for (var i = 0; i < con_fsx.length; i++) {
for (var j = 0; j < con_fsx[i].length; j++) {
con_fsx[i][j] = 0;
con_fsy[i][j] = 0;
}
}

nodes.length = 0;
nodes = deepClone(o_nodes);

cal_force();
update_position();

draw_new();
}

function deepClone (obj) {
if (typeof obj !== 'object')
return obj;
if (!obj) // obj null
return obj;
if (obj instanceof Date)
return new Date(obj);
if (obj instanceof RegExp)
return new RegExp(obj);
if (obj instanceof Function)
return obj;

let newObj;
if (obj instanceof Array) {
newObj = [];
for(let i = 0, len = obj.length; i < len; i++)
newObj.push(deepClone(obj[i]));
return newObj;
}
newObj = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] !== 'object')
newObj[key] = obj[key];
else
newObj[key] = deepClone(obj[key]);
}
}
return newObj;
}

// generate data....
function gen_d() {
var temp_cnt = 0, group = 0;
while (temp_cnt < node_cnt) {//max-min+1 +min
// generate nodes
var x0 = Math.floor(Math.random() * (maxNodeX - 1 + 1) ) + 1;
var y0 = Math.floor(Math.random() * (maxNodeY - 1 + 1) ) + 1;
var cnt1 = Math.floor(Math.random() * (15 - 2 + 1) ) + 2;
if (temp_cnt + cnt1 > node_cnt)
cnt1 = node_cnt - temp_cnt;
var range_g = 50;
var Fr_x = 0, Fr_y = 0, Fs_x = 0, Fs_y = 0;
var x = x0, y = y0;
o_nodes[group] = new Array;
nodes[group] = new Array;
o_nodes[group].push({x,y,Fr_x,Fr_y,Fs_x,Fs_y});
nodes[group].push({x,y,Fr_x,Fr_y,Fs_x,Fs_y});
for (var i = 1; i < cnt1; i++) { // generate group nodes
var dx = Math.floor(Math.random() * (range_g - 1 + 1) ) + 1;
var dy = Math.floor(Math.random() * (range_g - 1 + 1) ) + 1;
var x1 = x0 + dx, y1 = y0 + dy;
var x2 = x0 - dx, y2 = y0 - dy;
var x, y;
if (x1 > maxNodeX || x1 < 0)
x = x2;
else x = x1;
if (y1 > maxNodeY || y1 < 0)
y = y2;
else y = y1;
o_nodes[group].push({x,y,Fr_x,Fr_y,Fs_x,Fs_y});
nodes[group].push({x,y,Fr_x,Fr_y,Fs_x,Fs_y});
}
// generate connection
var con0 = [];
for (var i = 0; i < cnt1; i++) {
con0[i] = new Array(cnt1);
con0[i].fill(0, 0, cnt1);
}
for (var i = 0; i < cnt1 - 1; i++) {
for (var j = i + 1; j < cnt1; j++) {
var isconnected = Math.floor(Math.random() * 10);
if (isconnected >= 6) {
con0[i][j] = 1;
con0[j][i] = 1;
}
}
}
connect.push(con0);
group++;
temp_cnt += cnt1;
}
var con1 = [];
for (var i = 0; i < group; i++) {
con1[i] = new Array(group);
con1[i].fill(0, 0, group);
con_fsx[i] = new Array(group);
con_fsx[i].fill(0, 0, group);
con_fsy[i] = new Array(group);
con_fsy[i].fill(0, 0, group);
}
for (var i = 0; i < group - 1; i++) {
var newCon = Math.floor(Math.random() * group - i) + i;
con1[newCon][i] = 1;
con1[i][newCon] = 1;
for (var j = i + 1; j < group; j++) {
var isconnected = Math.floor(Math.random() * 10);
if (isconnected >= 6) {
con1[i][j] = 1;
con1[j][i] = 1;
}
}
}
connect.push(con1);
console.log("connected? : ", connect);
console.log("now get data: ", o_nodes);
console.log("copy nodes: ", nodes);
}

function cal_force() {
// repulsion force between all pairs
// if they are connected, calculate spring force
for (var i1 = 0; i1 < nodes.length; i1++) {
for (var i2 = 0; i2 < nodes[i1].length - 1; i2++) {
var node1 = nodes[i1][i2];
for(var j1 = i1; j1 < nodes.length; j1++) {
for (var j2 = 0; j2 < nodes[j1].length; j2++) {
if (j1 == i1 && j2 <= i2)
continue;
var node2 = nodes[j1][j2];
var dx = node2.x - node1.x;
var dy = node2.y - node1.y;
if (dx != 0 || dy != 0) {
var disSqu = dx*dx + dy*dy;
var dis = Math.sqrt(disSqu);
var force = K_r / disSqu;
var fx = force * dx / dis;
var fy = force * dy / dis;
node1.Fr_x = node1.Fr_x - fx;
node1.Fr_y = node1.Fr_y - fy;
node2.Fr_x = node2.Fr_x + fx;
node2.Fr_y = node2.Fr_y + fy;
if (i1 == j1 && connect[i1][i2][j2] == 1) {
var force2 = K_s * (dis - L);
var fx2 = force2 * dx / dis;
var fy2 = force2 * dy / dis;
node1.Fs_x = node1.Fs_x + fx2;
node1.Fs_y = node1.Fs_y + fy2;
node2.Fs_x = node2.Fs_x - fx2;
node2.Fs_y = node2.Fs_y - fy2;
}
if (i2 == 0 && j2 == 0 && i1 != j1) {
if (connect[nodes.length][i1][j1] == 1) {
var force3 = K_s * (dis - L);
var fx3 = force3 * dx / dis;
var fy3 = force3 * dy / dis;
con_fsx[i1][j1] += fx3;
con_fsy[i1][j1] += fy3;
con_fsx[j1][i1] -= fx3;
con_fsy[j1][i1] -= fy3;
}
}
nodes[i1][i2] = node1;
nodes[j1][j2] = node2;
}
}
}
}
}
for (var i = 0; i < con_fsx.length; i++) {
var x = 0, y = 0;
for (var j = 0; j < con_fsx.length; j++) {
x = x + con_fsx[i][j];
y = y + con_fsy[i][j];
}
con_fs.push({x,y});
}
}

function update_position() {
for (var i1 = 0; i1 < nodes.length; i1++) {
for (var i2 = 0; i2 < nodes[i1].length; i2++) {
var _node = nodes[i1][i2];
var dx = delta_t * (_node.Fr_x + _node.Fs_x + con_fs[i1].x);
var dy = delta_t * (_node.Fr_y + _node.Fs_y + con_fs[i1].y);
var displacement_Sqr = dx*dx + dy*dy;
if (displacement_Sqr > MAX_DIS_SQR) {
var disS = Math.sqrt(MAX_DIS_SQR / displacement_Sqr);
dx = dx * disS;
dy = dy * disS;
}
_node.x = _node.x + dx;
_node.y = _node.y + dy;
nodes[i1][i2] = _node;
}
}
console.log("old & new nodes are here: ", o_nodes, nodes);
}

function draw_old() {
// draw line
for (var i = 0; i < connect.length; i++) {
var temp_con = connect[i];

for (var j = 0; j < temp_con.length - 1; j++) {
for (var k = j + 1; k < temp_con.length; k++) {
if (temp_con[j][k] == 1) {
var x1, x2, y1, y2;
var lineStroke = '#ffdab9';
var lineLen = 0.5;
if (i == connect.length - 1) {
x1 = o_nodes[j][0].x, y1 = o_nodes[j][0].y;
x2 = o_nodes[k][0].x, y2 = o_nodes[k][0].y;
lineStroke = '#f08080';
lineLen = 1.0;
}
else{
x1 = o_nodes[i][j].x, y1 = o_nodes[i][j].y;
x2 = o_nodes[i][k].x, y2 = o_nodes[i][k].y;
}

var line = document.createElementNS(svgns, 'line');
line.setAttributeNS(null, 'x1', x1);
line.setAttributeNS(null, 'x2', x2);
line.setAttributeNS(null, 'y1', y1);
line.setAttributeNS(null, 'y2', y2);
line.setAttributeNS(null, 'stroke', lineStroke);
line.setAttributeNS(null, 'stroke-width', lineLen);
document.getElementById('svgOne').appendChild(line);
}
}
}
}

// draw circle
for (var i = 0; i < o_nodes.length; i++) {
for (j = 0; j < o_nodes[i].length; j++) {
var getx = o_nodes[i][j].x;
var gety = o_nodes[i][j].y;
var circle = document.createElementNS(svgns, 'circle');
circle.setAttributeNS(null, "cx", getx);
circle.setAttributeNS(null, "cy", gety);
circle.setAttributeNS(null, "r", 3);
circle.setAttributeNS(null, "fill", colorset1[i % colorset1.length]);
circle.setAttributeNS(null, "stroke", "none");
document.getElementById('svgOne').appendChild(circle);
}
}
}

function draw_new() {
// draw line
for (var i = 0; i < connect.length; i++) {
var temp_con = connect[i];

for (var j = 0; j < temp_con.length - 1; j++) {
for (var k = j + 1; k < temp_con.length; k++) {
if (temp_con[j][k] == 1) {
var x1, x2, y1, y2;
var lineStroke = '#ffcad4';
var lineLen = 1.0;
if (i == connect.length - 1) {
x1 = nodes[j][0].x, y1 = nodes[j][0].y;
x2 = nodes[k][0].x, y2 = nodes[k][0].y;
lineStroke = '#9e2a2b';
lineLen = 1.5;
}
else{
x1 = nodes[i][j].x, y1 = nodes[i][j].y;
x2 = nodes[i][k].x, y2 = nodes[i][k].y;
}

var line = document.createElementNS(svgns, 'line');
line.setAttributeNS(null, 'x1', x1);
line.setAttributeNS(null, 'x2', x2);
line.setAttributeNS(null, 'y1', y1);
line.setAttributeNS(null, 'y2', y2);
line.setAttributeNS(null, 'stroke', lineStroke);
line.setAttributeNS(null, 'stroke-width', lineLen);
document.getElementById('svgTwo').appendChild(line);
}
}
}
}

// draw circle
for (var i = 0; i < nodes.length; i++) {
for (j = 0; j < nodes[i].length; j++) {
var getx = nodes[i][j].x;
var gety = nodes[i][j].y;
var circle = document.createElementNS(svgns, 'circle');
circle.setAttributeNS(null, "cx", getx);
circle.setAttributeNS(null, "cy", gety);
circle.setAttributeNS(null, "r", 4.5);
circle.setAttributeNS(null, "fill", colorset2[i % colorset2.length]);
circle.setAttributeNS(null, "stroke", "none");
document.getElementById('svgTwo').appendChild(circle);
}
}
}
</script>
<div class = "slidecontainer" id = "slidecon">
<label>L: </label>
<input type = "range" min = "10" max="1000" value = "50" class = "slider"
id = "s_L" oninput = "document.getElementById('l_L').innerHTML = this.value"
onchange = "update2(this.value, -1, -1, -1, -1)" />
<label id = "l_L">50</label>
<br>

<label>K_s: </label>
<input type = "range" min = "100" max="10000" value = "6250" class = "slider"
id = "s_S" oninput = "document.getElementById('l_S').innerHTML = this.value"
onchange = "update2(-1, this.value, -1, -1, -1)" />
<label id = "l_S">6250</label>
<br>

<label>K_r: </label>
<input type = "range" min = "0.01" max="5" value = "1" step = "0.01" class = "slider"
id = "s_R" oninput = "document.getElementById('l_R').innerHTML = this.value"
onchange = "update2(-1, -1, this.value, -1, -1)" />
<label id = "l_R">1</label>
<br>

<label>Δt: </label>
<input type = "range" min = "0.005" max="5.000" value = "0.04" step = "0.001" class = "slider"
id = "s_T" oninput = "document.getElementById('l_T').innerHTML = this.value"
onchange = "update2(-1, -1, -1, this.value, -1)" />
<label id = "l_T">0.04</label>
<br>

<label>MAX_DIS_SQR: </label>
<input type = "range" min = "10" max="200" value = "50" class = "slider"
id = "s_M" oninput = "document.getElementById('l_M').innerHTML = this.value"
onchange = "update2(-1, -1, -1, -1, this.value)" />
<label id = "l_M">50</label>
<br>
</div>
</body>
</html>