Skip to content

Commit

Permalink
Better force-directed layout.
Browse files Browse the repository at this point in the history
Use the Floyd-Warshall algorithm to compute the shortest path between nodes, and
use that graph theoretic distance as the distance constraint for the Gauss-
Seidel relaxation. In addition, have the constraint alpha decay over time, as in
Simulated Annealing.
  • Loading branch information
mbostock committed Feb 28, 2011
1 parent 1d9ed5a commit ced7b72
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 79 deletions.
4 changes: 2 additions & 2 deletions examples/force/cluster.html
Expand Up @@ -178,6 +178,7 @@
var m = d3.svg.mouse(vis[0][0]);
active.x = m[0];
active.y = m[1];
force.resume(); // restart annealing
});

return false;
Expand Down Expand Up @@ -221,8 +222,7 @@
.nodes(net.nodes)
.links(net.links)
.size({x: w, y: h})
.nodeDistance(100)
.linkDistance(60)
.distance(60)
.start();

hullg.selectAll("path.hull").remove();
Expand Down
6 changes: 3 additions & 3 deletions examples/force/force.css
@@ -1,9 +1,9 @@
circle.node {
fill: lightsteelblue;
stroke: steelblue;
stroke: #fff;
stroke-width: 1.5px;
}

line.link {
stroke: #333;
stroke: #999;
stroke-opacity: .6;
}
6 changes: 4 additions & 2 deletions examples/force/force.js
@@ -1,5 +1,6 @@
var w = 960,
h = 500;
h = 500,
fill = d3.scale.category20();

var vis = d3.select("#chart")
.append("svg:svg")
Expand Down Expand Up @@ -28,7 +29,8 @@ d3.json("miserables.json", function(json) {
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 4.5);
.attr("r", 5)
.style("fill", function(d) { return fill(d.group); });

vis.attr("opacity", 0)
.transition()
Expand Down
143 changes: 71 additions & 72 deletions examples/force/layout.js
Expand Up @@ -3,85 +3,53 @@ function layout_force() {
var force = {},
event = d3.dispatch("tick"),
size = {x: 1, y: 1},
alpha = .1,
nodeDistance = 60,
linkDistance = 30,
alpha = .5,
distance = 30,
interval,
nodes,
links;

// TODO
// slow the interval as the graph stabilizes
// allow the nodes to be dragged interactively
links,
distances;

function tick() {
var n = nodes.length,
m = links.length,
var n = distances.length,
i, // current index
j, // current index
o, // current link
s, // current source
t, // current target
l, // current distance
x,
y;
x, // x-distance
y; // y-distance

// repel nodes
// gauss-seidel relaxation
for (i = 0; i < n; ++i) {
s = nodes[i];
for (j = i + 1; j < n; ++j) {
t = nodes[j];
x = t.x - s.x;
y = t.y - s.y;
l = Math.sqrt(x * x + y * y);
if (l < nodeDistance) {
l = alpha * (l - nodeDistance) / l;
x *= l;
y *= l;
if (s.fixed) {
if (t.fixed) continue;
t.x -= x;
t.y -= y;
} else if (t.fixed) {
s.x += x;
s.y += y;
} else {
s.x += x;
s.y += y;
t.x -= x;
t.y -= y;
}
}
}
}

// position constraint for links
for (i = 0; i < m; ++i) {
o = links[i];
o = distances[i];
s = o.source;
t = o.target;
x = t.x - s.x;
y = t.y - s.y;
l = Math.sqrt(x * x + y * y);
if (l <= 0) l = 0.01;
l = alpha * (l - linkDistance) / l;
x *= l;
y *= l;
if (s.fixed) {
if (t.fixed) continue;
t.x -= x;
t.y -= y;
} else if (t.fixed) {
s.x += x;
s.y += y;
} else {
s.x += x;
s.y += y;
t.x -= x;
t.y -= y;
if (l = Math.sqrt(x * x + y * y)) {
l = alpha / (o.distance * o.distance) * (l - distance * o.distance) / l;
x *= l;
y *= l;
if (s.fixed) {
if (t.fixed) continue;
t.x -= x;
t.y -= y;
} else if (t.fixed) {
s.x += x;
s.y += y;
} else {
s.x += x;
s.y += y;
t.x -= x;
t.y -= y;
}
}
}

// simulated annealing, basically
if ((alpha *= .99) < 1e-6) force.stop();

event.tick.dispatch({type: "tick"});
}

Expand All @@ -108,44 +76,75 @@ function layout_force() {
return force;
};

force.nodeDistance = function(d) {
if (!arguments.length) return nodeDistance;
nodeDistance = d;
return force;
};

force.linkDistance = function(d) {
if (!arguments.length) return linkDistance;
linkDistance = d;
force.distance = function(d) {
if (!arguments.length) return distance;
distance = d;
return force;
};

force.start = function() {
var i,
j,
k,
n = nodes.length,
m = links.length,
w = size.x,
h = size.y,
o;

var paths = [];
for (i = 0; i < n; ++i) {
o = nodes[i];
o.x = o.x || Math.random() * w;
o.y = o.y || Math.random() * h;
o.fixed = 0;
paths[i] = [];
for (j = 0; j < n; ++j) {
paths[i][j] = Infinity;
}
paths[i][i] = 0;
}

for (i = 0; i < m; ++i) {
o = links[i];
paths[o.source][o.target] = 1;
paths[o.target][o.source] = 1;
o.source = nodes[o.source];
o.target = nodes[o.target];
}

// Floyd-Warshall
for (k = 0; k < n; ++k) {
for (i = 0; i < n; ++i) {
for (j = 0; j < n; ++j) {
paths[i][j] = Math.min(paths[i][j], paths[i][k] + paths[k][j]);
}
}
}

distances = [];
for (i = 0; i < n; ++i) {
for (j = i + 1; j < n; ++j) {
distances.push({
source: nodes[i],
target: nodes[j],
distance: paths[i][j] * paths[i][j]
});
}
}

distances.sort(function(a, b) {
return a.distance - b.distance;
});

if (interval) clearInterval(interval);
interval = setInterval(tick, 24);
return force;
};

force.resume = function() {
if (interval) clearInterval(interval);
interval = setInterval(tick, 24);
alpha = .1;
if (!interval) interval = setInterval(tick, 24);
return force;
};

Expand Down

0 comments on commit ced7b72

Please sign in to comment.