Skip to content

Instantly share code, notes, and snippets.

@dankronstal
Last active February 5, 2016 22:02
Show Gist options
  • Save dankronstal/ef69992a950bd7e3e9e1 to your computer and use it in GitHub Desktop.
Save dankronstal/ef69992a950bd7e3e9e1 to your computer and use it in GitHub Desktop.
Force Layout I

Like many folks, I really liked the classic Obama Budget visualization in the NYT (now sheltered behind the vigilant guard of a paywall, but you can read about it here: https://flowingdata.com/2012/02/15/slicing-obamas-2013-budget-proposal-four-ways/). I wanted to have some bubbles floating about the screen too, so I made this. The hardest part to making it appear dynamic was the subtle hesitation in the movement between groups.

<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Force Layout w/ Shift</title>
<style>
.node {
fill: #ccc;
stroke: #333;
stroke-width: 2px;
}
.link {
stroke: #777;
stroke-width: 2px;
}
</style>
</head>
<body>
<script src='https://d3js.org/d3.v3.min.js'></script>
<input type="radio" name="rorl" value="0">Right<br />
<input type="radio" name="rorl" value="1">Left<br />
<input type="radio" name="rorl" value="2" checked>Group<br />
<script>
var width = 640,
height = 480,
rorl = 2;
/*** start setting up data */
var nodes = [ //todo: make this node list less static (either by randomly generating, or by pulling from some actual data file)
{"Name":"First", "Value":100, "Group":0},
{"Name":"Second", "Value":120, "Group":0},
{"Name":"Third", "Value":40, "Group":0},
{"Name":"Fourth", "Value":100, "Group":1},
{"Name":"Fifth", "Value":200, "Group":1},
{"Name":"Sixth", "Value":300, "Group":1},
{"Name":"Seventh", "Value":50, "Group":1},
{"Name":"Eigth", "Value":100, "Group":2},
{"Name":"Ninth", "Value":150, "Group":2},
{"Name":"Tenth", "Value":50, "Group":2},
{"Name":"First", "Value":10, "Group":0},
{"Name":"Second", "Value":12, "Group":0},
{"Name":"Third", "Value":40, "Group":0},
{"Name":"Fourth", "Value":10, "Group":1},
{"Name":"Fifth", "Value":20, "Group":1},
{"Name":"Sixth", "Value":30, "Group":1},
{"Name":"Seventh", "Value":50, "Group":1},
{"Name":"Eigth", "Value":10, "Group":2},
{"Name":"Ninth", "Value":15, "Group":2},
{"Name":"Tenth", "Value":50, "Group":2},
];
var max_amount = d3.max(nodes, function(d) { return parseInt(d.Value); });
var radius_scale = d3.scale.pow().exponent(0.5).domain([0, max_amount]).range([2, 35]);
nodes.forEach(function(d){ d.radius = radius_scale(parseInt(d.Value)); });
/*** end setting up data */
var gravityTargets = [{x:640, y:height/2},{x:0, y:height/2}, //rorl targets
[{x:0, y:height/2},{x:width/2, y:height/2},{x:640, y:height/2}]]; //group targets
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.attr('fill','#eee');
svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("height", height)
.attr("width",width)
.style("stroke", '#333')
.style("fill", "none")
.style("stroke-width", 1);
var node = svg.selectAll('.node')
.data(nodes)
.enter().append('circle')
.attr('class', 'node');
var force = d3.layout.force()
.gravity(0.1)
.charge(-0)
.friction(0.7)
.nodes(nodes)
.size([width, height]);
force.on("tick", function(e) {
var q = d3.geom.quadtree(nodes),
i = 0,
n = nodes.length;
while (++i < n) q.visit(collide(nodes[i]));
var k = 0.15 * e.alpha;
if (rorl == 2) {
nodes.forEach(function (d, i) {
d.y += (gravityTargets[rorl][d.Group].y - d.y) * k;
d.x += (gravityTargets[rorl][d.Group].x - d.x) * k;
force.chargeDistance(75);
});
} else {
nodes.forEach(function (d, i) {
d.y += (gravityTargets[rorl].y - d.y) * k;
d.x += (gravityTargets[rorl].x - d.x) * k;
force.chargeDistance(9999999999);
});
}
node
.attr("cx", function(d) {return d.x;})
.attr("cy", function(d) {return d.y;})
.attr('r', function(d) { return d.radius; });
});
node.call(force.drag);
function collide(n) {
var r = n.radius + 100,
nx1 = n.x - r,
nx2 = n.x + r,
ny1 = n.y - r,
ny2 = n.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== n)) {
var x = n.x - quad.point.x,
y = n.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = n.radius + quad.point.radius;
if (l < r) {
l = (l - r) / r * 0.5;
n.x -= x *= l;
n.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
}
d3.selectAll("input")
.on("click",function(d) {
if(rorl == this.value) return;
rorl = 2; //initially set to 'group' to shake up the nodes (purely for visual appeal)
var v = this.value;
setTimeout(function(){rorl = v;},100); //then set actual value
force.start();
});
force.start();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment