The force graph has been the subject of my fascination in the recent couple weeks. One of the fun things about the force layout is that the positioning of each node and link is calculated at every tick, which means the normal update-transition paradigm doesn’t work too well.
This was my approach to the enter-update-exit pattern in a force graph, and other then a couple bounces of the nodes, seem to work pretty decently.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font-family: Helvetica;
}
.update {
color: #888888;
position:absolute;
top: 10px;
left: 10px;
padding: 5px 10px;
margin: 10px;
cursor: pointer;
border: 1px solid #999999;
border-radius: 3px;
}
.node circle {
fill: #888888;
stroke: #fff;
stroke-width: 2px;
}
.node text {
fill: #888888;
stroke: none;
font-size: .6em;
}
.link {
stroke: #cccccc;
stroke-opacity: .6;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//underscorejs.org/underscore-min.js"></script>
<script>
var width = 400,
height = 400,
nodes, links, oldNodes, // data
svg, node, link, // d3 selections
force = d3.layout.force()
.charge(-300)
.linkDistance(50)
.size([width, height]);
function randomData() {
oldNodes = nodes;
// generate some data randomly
nodes = _.chain(_.range(_.random(10, 20)))
.map(function() {
var node = {};
node.key = _.random(0, 30);
node.weight = _.random(4, 10);
return node;
}).uniq(function(node) {
return node.key
}).value();
if (oldNodes) {
var add = _.initial(oldNodes, _.random(0, oldNodes.length));
add = _.rest(add, _.random(0, add.length));
nodes = _.union(nodes, add);
}
links = _.map(_.range(_.random(15, 25)), function() {
var link = {};
link.source = _.random(0, nodes.length - 1);
link.target = _.random(0, nodes.length - 1);
link.weight = _.random(1, 3);
return link;
});
maintainNodePositions();
}
function render() {
randomData();
force.nodes(nodes).links(links);
svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var l = svg.selectAll(".link")
.data(links, function(d) {return d.source + "," + d.target});
var n = svg.selectAll(".node")
.data(nodes, function(d) {return d.key});
enterLinks(l);
enterNodes(n);
link = svg.selectAll(".link");
node = svg.selectAll(".node");
force.start();
}
function update() {
randomData();
force.nodes(nodes).links(links);
var l = svg.selectAll(".link")
.data(links, function(d) {return d.source + "," + d.target});
var n = svg.selectAll(".node")
.data(nodes, function(d) {return d.key});
enterLinks(l);
exitLinks(l);
enterNodes(n);
exitNodes(n);
link = svg.selectAll(".link");
node = svg.selectAll(".node");
link.style("stroke-width", function(d) { return d.weight; });
node.select("circle").attr("r", function(d) {return d.weight});
force.start();
}
function enterNodes(n) {
var g = n.enter().append("g")
.attr("class", "node");
g.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", function(d) {return d.weight})
.call(force.drag);
g.append("text")
.attr("x", function(d) {return d.weight + 5})
.attr("dy", ".35em")
.text(function(d) {return d.key});
}
function exitNodes(n) {
n.exit().remove();
}
function enterLinks(l) {
l.enter().insert("line", ".node")
.attr("class", "link")
.style("stroke-width", function(d) { return d.weight; });
}
function exitLinks(l) {
l.exit().remove();
}
function maintainNodePositions() {
var kv = {};
_.each(oldNodes, function(d) {
kv[d.key] = d;
});
_.each(nodes, function(d) {
if (kv[d.key]) {
// if the node already exists, maintain current position
d.x = kv[d.key].x;
d.y = kv[d.key].y;
} else {
// else assign it a random position near the center
d.x = width / 2 + _.random(-150, 150);
d.y = height / 2 + _.random(-25, 25);
}
});
}
force.on("tick", function(e) {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
});
render();
</script>
<div class="update" onClick="update()">update</update>
</body>