Skip to content

Instantly share code, notes, and snippets.

@jmacqueen
Created November 12, 2013 23:42
Show Gist options
  • Save jmacqueen/7440844 to your computer and use it in GitHub Desktop.
Save jmacqueen/7440844 to your computer and use it in GitHub Desktop.
D3.js: Collision Detection

Adding some collision detection makes the graph cleaner at a wider variety of settings. But notice that the conflict between collision detection, gravity pulling nodes together and links attempting to limit distance can create some unusual behavior at extreme values. The loners need a little space to escape :-)

There's some light refactoring going on here. The random node generation code now uses d3.range to generate an array of numbers and then map on top of that to create the node objects themselves. A similar approach is used for the links, too.

The collision detection code was lifted from here with minor tweaks to account for my node scaling.

If you want to interact with these D3.js gists without downloading them yourself, replace https://gist.github.com with http://bl.ocks.org to use Mike Bostock's excellent website for running D3.js gists.

body {
font-family: Helvetica,sans-serif;
font-size: 14px;
}
.controls {
border-bottom: solid 1px black;
padding: 1em 5%;
text-align: center;
}
.controls label {
display: inline-block;
margin: 0.5em 1em;
}
span[id $= "Value"] {
display: inline-block;
width: 3em;
}
svg {
border: solid 1px black;
display: block;
margin: 1em auto;
max-width: 100%;
}
.node {
fill: lightblue;
stroke: black;
}
.link {
stroke: black;
stroke-width: 1px;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Collision Detection</title>
<link rel="stylesheet" href="collision-detection.css">
<script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<div class="controls">
<!-- <label>Gravity:&nbsp;<input id="gravitySlider" type="range" min=0 max=2 step=0.1>&nbsp;<span id="gravityValue"></span></label> -->
<label>Charge Scaling:&nbsp;<input id="chargeSlider" type="range" min=0 max=400 step=10>&nbsp;<span id="chargeValue"></span></label>
<label>Node Scaling:&nbsp;<input id="nodeSlider" type="range" min=0 max=4 step=0.1>&nbsp;<span id="nodeValue"></span></label>
</div>
<script>
var width = 800;
var height = 400;
var numPoints = 200;
var initGravity = 1;
var initChargeScaling = 0;
var initNodeScaling = 2;
var svg = d3.select("body").append("svg")
.attr("width", width).attr("height", height)
.attr("viewBox", "0 0 " + width + " " + height)
.attr("preserveAspectRatio", "xMidYMid");
// Buncha random nodes
var force = d3.layout.force().size([width,height]);
var nodes = d3.range(numPoints).map(function(d,i){
return { name: "Node " + i, radius: 5 };
});
// Buncha random links between nodes
var links = d3.range(numPoints).map(function(d,i){
var src = Math.floor(Math.random() * numPoints);
var tar = Math.floor(Math.random() * numPoints);
if (tar === src) {
(src === 0) ? tar = 1 : tar = src - 1;
}
return { source: src, target: tar };
});
// Push links first so links get drawn *behind* the nodes
force.links(links);
force.nodes(nodes);
force.gravity(initGravity);
var linkSelection = svg.selectAll("line.link").data(links);
linkSelection.enter()
.insert("line")
.classed("link", true);
var nodeSelection = svg.selectAll("circle.node").data(nodes);
nodeSelection.enter()
.append("circle")
.classed("node", true);
force.on("start", function(){
force.charge(function(node,i){
var chargeScale = d3.select("#chargeSlider").property("valueAsNumber");
return -1*(weightedValue(chargeScale, node.weight, 30));
});
});
force.on("tick", function(e){
var nodeScaling = d3.select("#nodeSlider").property("valueAsNumber");
var q = d3.geom.quadtree(nodes),
i = 0,
n = nodes.length;
while (++i < n) q.visit(collide(nodes[i], nodeScaling));
linkSelection.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;});
nodeSelection.attr("cx", function(d){return d.x;})
.attr("cy", function(d) {return d.y})
.attr("r", function(d){return weightedValue(d.weight, nodeScaling, 2);});
});
d3.select("#chargeSlider").on("change", function(){
var val = this.value;
d3.select("#chargeValue").text(val);
force.start();
});
d3.select("#nodeSlider").on("change", function(){
var val = this.value;
d3.select("#nodeValue").text(val);
force.resume();
});
function collide(node, nodeScaling) {
var radius = weightedValue(node.weight,nodeScaling,2);
var r = radius,// + 16,
nx1 = node.x - r,
nx2 = node.x + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = radius + weightedValue(quad.point.weight,nodeScaling,2);
if (l < r) {
l = (l - r) / l * .5;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
}
function weightedValue(weight,scalingFactor,minimum) {
return weight * scalingFactor + minimum;
}
// Main
d3.select("#chargeSlider").property("value", initChargeScaling);
d3.select("#chargeValue").text(initChargeScaling);
d3.select("#nodeSlider").property("value", initNodeScaling);
d3.select("#nodeValue").text(initNodeScaling);
force.start();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment