|
<!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 --> |