Skip to content

Instantly share code, notes, and snippets.

@sxywu
Last active December 22, 2015 08:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save sxywu/9358409 to your computer and use it in GitHub Desktop.
Save sxywu/9358409 to your computer and use it in GitHub Desktop.
Enter-Update-Exit in Force Layout

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="http://d3js.org/d3.v3.min.js"></script>
<script src="http://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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment