Skip to content

Instantly share code, notes, and snippets.

@apcj
Forked from mbostock/.block
Last active December 19, 2015 13:28
Show Gist options
  • Save apcj/5961826 to your computer and use it in GitHub Desktop.
Save apcj/5961826 to your computer and use it in GitHub Desktop.

Smooth graph exploration

D3 force layout keeps track of node positions by setting x and y properties on the underlying data. By default it chooses starting positions at random, which is convenient, but leads to high-velocity, distracting movements when force calculations begin.

For many graph structures, it's easy to do better than random for starting positions. We can set specific start positions on the data before handing over control to force layout.

In this example, clicking on a node expands its 10 immediate neighbours. For a short period the force layout is suspended, and the new nodes move radially from the clicked node until they end up on a circle with at the ideal link distance. Once the force layout restarts, the nodes are already quite close to a sensible position, so the violence of the force layout appears much less than with default behaviour.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.link {
stroke: #000;
stroke-width: 1.5px;
}
.node {
fill: #000;
stroke: #fff;
stroke-width: 1.5px;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var width = 960,
height = 500,
linkDistance = 120;
var color = d3.scale.category10();
var idGenerator = 0;
var nodes = [ { id: idGenerator++, x: width / 2, y: height / 2 } ],
links = [];
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.charge(-400)
.linkDistance(linkDistance)
.size([width, height])
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
start();
function expandChildren(source) {
force.stop();
var newLinks = [];
var childCount = 10;
for ( var i = 0; i < childCount; i++ )
{
var target = {
id: idGenerator++,
x: source.x + linkDistance * Math.sin( 2 * Math.PI * i / childCount ),
y: source.y + linkDistance * Math.cos( 2 * Math.PI * i / childCount )
};
var newLink = {
source: source,
target: target
};
newLinks.push( newLink );
}
svg.selectAll("circle.node")
.data(newLinks.map( function(d) { return d.target; } ), function(d) { return d.id; } )
.enter()
.append("circle")
.attr("class", "node new")
.attr("r", 8)
.attr("cx", source.x )
.attr("cy", source.y );
svg.selectAll(".link")
.data(newLinks, function(d) { return d.source.id + "-" + d.target.id; } )
.enter()
.insert("line", ".node" )
.attr("class", "link" )
.attr("x1", function(d) { return d.source.x; } )
.attr("y1", function(d) { return d.source.y; } )
.attr("x2", function(d) { return d.source.x; } )
.attr("y2", function(d) { return d.source.y; } );
var transition = svg.transition()
.duration(1000);
transition.selectAll("circle.node")
.attr("cx", function(d) { return d.x; } )
.attr("cy", function(d) { return d.y; } );
transition.selectAll("line.link")
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
transition
.each("end", function() {
for ( var i = 0; i < newLinks.length; i++ )
{
var link = newLinks[i];
nodes.push( link.target );
links.push( link );
}
start();
svg.selectAll(".node").on("click", expandChildren);
} );
}
svg.selectAll(".node").on("click", expandChildren);
function start() {
var node = svg.selectAll(".node")
.data(force.nodes(), function(d) { return d.id;});
node.enter()
.append("circle")
.attr("class", "node")
.attr("r", 8);
node.exit().remove();
var link = svg.selectAll(".link")
.data(force.links(), function(d) { return d.source.id + "-" + d.target.id; });
link.enter()
.insert("line", ".node")
.attr("class", "link");
link.exit().remove();
force.start();
}
function tick() {
svg.selectAll(".node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
svg.selectAll(".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; });
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment