Better force-directed layout.
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.
This commit is contained in:
Родитель
1d9ed5ae72
Коммит
ced7b7242b
|
@ -178,6 +178,7 @@ function drag(n) {
|
|||
var m = d3.svg.mouse(vis[0][0]);
|
||||
active.x = m[0];
|
||||
active.y = m[1];
|
||||
force.resume(); // restart annealing
|
||||
});
|
||||
|
||||
return false;
|
||||
|
@ -221,8 +222,7 @@ function init() {
|
|||
.nodes(net.nodes)
|
||||
.links(net.links)
|
||||
.size({x: w, y: h})
|
||||
.nodeDistance(100)
|
||||
.linkDistance(60)
|
||||
.distance(60)
|
||||
.start();
|
||||
|
||||
hullg.selectAll("path.hull").remove();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
var w = 960,
|
||||
h = 500;
|
||||
h = 500,
|
||||
fill = d3.scale.category20();
|
||||
|
||||
var vis = d3.select("#chart")
|
||||
.append("svg:svg")
|
||||
|
@ -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()
|
||||
|
|
|
@ -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"});
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче