Skip to content

Instantly share code, notes, and snippets.

@veltman
Last active June 3, 2017 00:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save veltman/938ea2d0ef98c02633bec15d6fb3a177 to your computer and use it in GitHub Desktop.
Save veltman/938ea2d0ef98c02633bec15d6fb3a177 to your computer and use it in GitHub Desktop.
Dorling cartogram transitions
height: 600

Generating random Dorling cartograms using a force simulation.

Could probably improve performance a little by running the next simulation during the animation instead of waiting until it's done.

See also: Pseudo-Dorling cartogram

<!DOCTYPE html>
<meta charset="utf-8">
<style>
path {
stroke: #000;
stroke-width: 1px;
}
</style>
<body>
<svg width="960", height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/3.0.0/topojson.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script>
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
radius = d3.scaleSqrt().range([0, 45]).clamp(true),
randomizer = d3.randomNormal(0.5, 0.2),
color = d3.scaleLinear();
d3.json("us.json", function(err, us) {
var neighbors = topojson.neighbors(us.objects.states.geometries),
nodes = topojson.feature(us, us.objects.states).features;
nodes.forEach(function(node, i) {
var centroid = d3.geoPath().centroid(node);
node.x0 = centroid[0];
node.y0 = centroid[1];
cleanUpGeometry(node);
});
var states = svg.selectAll("path")
.data(nodes)
.enter()
.append("path")
.attr("d", pathString)
.attr("fill", "#ccc");
simulate();
function simulate() {
nodes.forEach(function(node) {
node.x = node.x0;
node.y = node.y0;
node.r = radius(randomizer());
});
color.domain(d3.extent(nodes, d => d.r));
var links = d3.merge(neighbors.map(function(neighborSet, i) {
return neighborSet.filter(j => nodes[j]).map(function(j) {
return {source: i, target: j, distance: nodes[i].r + nodes[j].r + 3};
});
}));
var simulation = d3.forceSimulation(nodes)
.force("cx", d3.forceX().x(d => width / 2).strength(0.02))
.force("cy", d3.forceY().y(d => height / 2).strength(0.02))
.force("link", d3.forceLink(links).distance(d => d.distance))
.force("x", d3.forceX().x(d => d.x).strength(0.1))
.force("y", d3.forceY().y(d => d.y).strength(0.1))
.force("collide", d3.forceCollide().strength(0.8).radius(d => d.r + 3))
.stop();
while (simulation.alpha() > 0.1) {
simulation.tick();
}
nodes.forEach(function(node){
var circle = pseudocircle(node),
closestPoints = node.rings.slice(1).map(function(ring){
var i = d3.scan(circle.map(point => distance(point, ring.centroid)));
return ring.map(() => circle[i]);
}),
interpolator = d3.interpolateArray(node.rings, [circle, ...closestPoints]);
node.interpolator = function(t){
var str = pathString(interpolator(t));
// Prevent some fill-rule flickering for MultiPolygons
if (t > 0.99) {
return str.split("Z")[0] + "Z";
}
return str;
};
});
states
.sort((a, b) => b.r - a.r)
.transition()
.delay(1000)
.duration(1500)
.attrTween("d", node => node.interpolator)
.attr("fill", d => d3.interpolateSpectral(color(d.r)))
.transition()
.delay(1000)
.attrTween("d", node => t => node.interpolator(1 - t))
.attr("fill", "#ccc")
.on("end", (d, i) => i || simulate());
}
});
function pseudocircle(node) {
return node.rings[0].map(function(point){
var angle = node.startingAngle - 2 * Math.PI * (point.along / node.perimeter);
return [
Math.cos(angle) * node.r + node.x,
Math.sin(angle) * node.r + node.y
];
});
}
function cleanUpGeometry(node) {
node.rings = (node.geometry.type === "Polygon" ? [node.geometry.coordinates] : node.geometry.coordinates);
node.rings = node.rings.map(function(polygon){
polygon[0].area = d3.polygonArea(polygon[0]);
polygon[0].centroid = d3.polygonCentroid(polygon[0]);
return polygon[0];
});
node.rings.sort((a, b) => b.area - a.area);
node.perimeter = d3.polygonLength(node.rings[0]);
// Optional step, but makes for more circular circles
bisect(node.rings[0], node.perimeter / 72);
node.rings[0].reduce(function(prev, point){
point.along = prev ? prev.along + distance(point, prev) : 0;
node.perimeter = point.along;
return point;
}, null);
node.startingAngle = Math.atan2(node.rings[0][0][1] - node.y0, node.rings[0][0][0] - node.x0);
}
function bisect(ring, maxSegmentLength) {
for (var i = 0; i < ring.length; i++) {
var a = ring[i], b = i === ring.length - 1 ? ring[0] : ring[i + 1];
while (distance(a, b) > maxSegmentLength) {
b = midpoint(a, b);
ring.splice(i + 1, 0, b);
}
}
}
function distance(a, b) {
return Math.sqrt((a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]));
}
function midpoint(a, b) {
return [a[0] + (b[0] - a[0]) * 0.5, a[1] + (b[1] - a[1]) * 0.5];
}
function pathString(d) {
return (d.rings || d).map(ring => "M" + ring.join("L") + "Z").join(" ");
}
</script>
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment