Skip to content

Instantly share code, notes, and snippets.

@GerHobbelt
Created July 16, 2012 22:55
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save GerHobbelt/3125600 to your computer and use it in GitHub Desktop.
Save GerHobbelt/3125600 to your computer and use it in GitHub Desktop.
d3.js: quantitative foci (radial)
# Editor backup files
*.bak
*~
<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<head>
<title>Force Layouts - Quantitative Foci</title>
<script src="http://d3js.org/d3.v2.js"></script>
<link type="text/css" rel="stylesheet" href="style000.css">
<style type="text/css">
circle {
stroke: #fff;
}
rect {
stroke: #777;
}
</style>
</head>
<body>
<div id="body">
<div id="chart"></div>
<div id="header">quantitative foci</div>
</div>
<script type="text/javascript">
var w = 900,
h = 500;
var color = d3.scale.linear()
.domain([h * h / 4, 10000])
.range(["hsl(180,100%,10%)", "hsl(210,100%,90%)"])
.interpolate(d3.interpolateHsl);
var force = d3.layout.force()
.gravity(0)
.charge(0)
.size([w, h]);
var nodes = force.nodes();
var svg = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h);
svg.append("svg:rect")
.attr("width", w)
.attr("height", h);
force.on("tick", function(e) {
var k = e.alpha * .1;
nodes.forEach(function(node) {
var dx, dy, ratio;
dx = node.x - w / 2;
dy = node.y - h / 2;
ratio = node.r2 / (dx * dx + dy * dy + 0.1);
ratio = (ratio - 1) / 3 + 1;
dx = dx * ratio + w / 2;
dy = dy * ratio + h / 2;
node.x += (dx - node.x) * k;
node.y += (dy - node.y) * k;
/*
Why does the above code work? Without Math.sqrt() or ...
From the Utterly basic towards the slightly wicked stuff:
--------------------------------------------------------
To calculate the 'destination' for any random starting point, we
calculate the point (dx, dy) relative to the center of the radial.
To calculate the angle, we could do a `Math.atan2()`, but that takes
time when done in bulk and we don't need polar coordinates: all
we need to answer is: 'are we far enough away from the center?' because
when we are, then we're at our designated spot (at distance `Math.sqrt(node.r2))`
from center, in the direction we started out with when the node was born.
For the above, all we need is a ratio, telling us how far to move both
x and y from the center (same factor means we remain going in the same
direction). Thus we can end up at the designated target spot (dx, dy)
after ratio has been applied. (where we need the ratio in radial distance,
i.e. `ratio = Math.sqrt(node.r2 / (dx * dx + dy * dy)`
But `Math.sqrt()` is relative costly too, and we can arrive at the same result
if we take into consideration that we've got many iterations (force.ticks)
before we've got to get there: we can then approximate the square root by
taking the ratio of the squared distances and divide it by a constant.
Seat of the pants would put '2' there, but that will result in serious
overshoot during the iteration (you get something like a damped oscillation
around the final target at `distance = Math.sqrt(node.r2))`.
In the code we use the constant divisor '3' to reduce ratio overshoot caused
by not rooting the squares; we can handle the larger
undershoot for different values this will cause at the same time: we still
have many iterations to fix that one up.
Tweak the constant or replace by the 'correct' formula to see the difference
in behaviour.
This minimization of the number of calls to Math.atan2/cos/sin (the very
naive way to do this radial distribution) and Math.sqrt(), having them
replaced by just a few multiplications and the minimal number of divisions
is the start to optimize such algorithms for speed.
The current code still has several invariants (w/2, h/2) which can be extracted.
And the why of the 0.1 in there is left as an exercise for the reader. ;-)
*/
});
svg.selectAll("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
var p0;
svg.on("mousemove", function() {
var p1 = d3.svg.mouse(this);
var node = {
x: p1[0],
y: p1[1],
px: (p0 || (p0 = p1))[0],
py: p0[1],
r2: Math.max(Math.random() * h * h / 4, 10000)
};
p0 = p1;
svg.append("svg:circle")
.data([node])
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 15)
.style("fill", function(d) { return color(d.r2); })
.transition()
.delay(3000)
.attr("r", 1e-6)
.each("end", function() { nodes.shift(); })
.remove();
nodes.push(node);
force.start();
});
</script>
</body>
</html>
<!-- This document saved from http://mbostock.github.com/d3/talk/20110921/quantitative-foci.html -->
body {
overflow: hidden;
margin: 0;
font: 14px "Helvetica Neue";
}
svg {
width: 1280px;
height: 800px;
}
#chart, #header {
position: absolute;
top: 0;
}
#header {
z-index: 1;
display: block;
}
#header {
top: 80px;
left: 140px;
font: 300 36px "Helvetica Neue";
}
rect {
fill: none;
pointer-events: all;
}
pre {
font-size: 18px;
}
line {
stroke: #000;
stroke-width: 1.5px;
}
.string, .regexp {
color: #f39;
}
.keyword {
color: #00c;
}
.comment {
color: #555;
}
.number {
color: #369;
}
.class, .special {
color: #1181B8;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment