Skip to content

Instantly share code, notes, and snippets.

@veltman
Last active February 21, 2024 10:30
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save veltman/33dbad5aa12d92b977fd29128eb5358e to your computer and use it in GitHub Desktop.
Save veltman/33dbad5aa12d92b977fd29128eb5358e to your computer and use it in GitHub Desktop.
Dorling cartogram transitions #2
height: 600
<!DOCTYPE html>
<meta charset="utf-8">
<style>
path {
stroke: #000;
stroke-width: 1px;
}
text {
font: 600 36px sans-serif;
text-anchor: end;
}
</style>
<body>
<svg width="960", height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://unpkg.com/topojson@3"></script>
<script>
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
radius = d3.scaleSqrt().range([0, 72]).clamp(true),
color = d3.scaleLinear(),
worker = new Worker("worker.js"),
label = svg.append("text").attr("x", width - 25).attr("y", height - 25);
var colorSchemes = [
d3.interpolateOranges,
d3.interpolatePuRd,
d3.interpolateYlGn,
d3.interpolateYlOrBr,
d3.interpolateYlGnBu
];
d3.queue()
.defer(d3.json, "us.json")
.defer(d3.csv, "usda.csv", numeric)
.await(function(err, us, data) {
var neighbors = topojson.neighbors(us.objects.states.geometries),
features = topojson.feature(us, us.objects.states).features,
columns = d3.keys(data[0]).filter(d => d !== "id"),
dataById = d3.nest().key(d => d.id).rollup(d => d[0]).object(data);
features.forEach(cleanUpGeometry);
// Get a flat list of neighbor-neighbor links
var links = d3.merge(neighbors.map(function(neighborSet, i) {
return neighborSet.map(j => ({ source: i, target: j }));
}));
var states = svg.selectAll("path")
.data(features)
.enter()
.append("path")
.attr("d", pathString)
.attr("fill", "#ccc");
dorling();
function dorling(data) {
var column = columns.pop(),
colorScheme = colorSchemes.pop();
// Update scales
color.domain(d3.extent(features, f => f.count = dataById[f.id][column]));
radius.domain([0, color.domain()[1]]);
features.forEach(function(f){
f.r = radius(f.count);
f.color = colorScheme(color(f.count));
});
links.forEach(link => link.distance = features[link.source].r + features[link.target].r + 3);
// Compute the next simulation while the current animation runs
Promise.all([
compute(features, links),
data ? animate(data, columns[0]) : Promise.resolve(true)
]).then(d => dorling(d[0]));
columns.unshift(column);
colorSchemes.unshift(colorScheme);
}
function animate(nodes, column) {
nodes.forEach(function(node){
var interpolator = d3.interpolateArray(node.rings, node.targets);
node.interpolator = function(t){
var left, right, r;
// Return a true circle at t = ~1
if (t > 0.99) {
return node.circlePath;
}
return pathString(interpolator(t));
};
});
return new Promise(function(resolve){
states
.data(nodes)
.sort((a, b) => b.r - a.r)
.transition()
.delay(500)
.duration(1500)
.attrTween("d", node => node.interpolator)
.attr("fill", node => node.color)
.transition()
.delay(1000)
.attrTween("d", node => t => node.interpolator(1 - t))
.attr("fill", "#ccc")
.on("end", resolve);
label.transition()
.delay(1000)
.duration(0)
.on("end", () => label.text(column))
.transition()
.delay(2500)
.duration(0)
.on("end", () => label.text(""));
});
}
});
// Post new set of nodes and links to the worker
function compute(nodes, links) {
return new Promise(function(resolve) {
worker.onmessage = event => resolve(event.data);
worker.postMessage({ nodes, links });
});
}
// Turn GeoJSON into a flat list of rings
// Add some extra points to smooth things out
// Compute the relative distances of points along the perimeter
function cleanUpGeometry(f) {
var centroid = d3.geoPath().centroid(f);
f.x = f.x0 = centroid[0], f.y = f.y0 = centroid[1];
f.rings = f.geometry.type === "Polygon" ? [f.geometry.coordinates] : f.geometry.coordinates;
// Remove holes
f.rings = f.rings.map(function(polygon){
polygon[0].area = d3.polygonArea(polygon[0]);
polygon[0].centroid = d3.polygonCentroid(polygon[0]);
return polygon[0];
});
// Largest ring as primary
f.rings.sort((a, b) => b.area - a.area);
f.perimeter = d3.polygonLength(f.rings[0]);
// Optional step, makes for more circular circles
bisect(f.rings[0], f.perimeter / 36);
f.rings[0].reduce(function(prev, point){
point.along = prev ? prev.along + distance(point, prev) : 0;
return point;
}, null);
f.startingAngle = Math.atan2(f.rings[0][0][1] - f.y0, f.rings[0][0][0] - f.x0);
delete f.geometry;
}
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(" ");
}
function numeric(row) {
for (var key in row) {
if (key !== "id") {
row[key] = +row[key];
}
}
delete row[""];
return row;
}
</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.
id peaches hogs avocados potatoes bison citrus berries goats maple syrup corn
AK 0 1009 0 676 1597 0 42 595 0 0
AL 2012 142555 0 808 252 231 1154 52749 0 763864
AR 673 109316 0 60 333 5 893 41610 0 3196252
AZ 189 0 0 3389 74 17830 22 71654 0 1162856
CA 51948 111893 122482 42660 1465 293387 52626 140042 0 13386807
CO 2776 727301 0 59281 10731 0 85 34757 0 5814436
CT 493 4737 0 0 122 0 1049 4393 10495 450169
DE 234 5891 0 1363 94 0 98 2008 0 689972
FL 1231 14915 25555 35251 385 539181 19568 52052 0 622418
GA 12318 153733 0 3634 278 25 14374 71709 0 1985804
HI 3 11441 564 26 0 928 0 12996 0 9325
IA 47 20455666 0 1028 1838 0 679 56239 828 52272402
ID 962 0 0 345217 3553 0 201 18139 0 6986296
IL 1430 4630796 0 7021 688 0 749 31546 3331 33628916
IN 429 3747352 0 3539 1319 0 1172 38632 12449 16945953
KS 187 1886197 0 5178 6638 0 185 42315 0 11847438
KY 512 313360 0 360 1411 3 866 64118 531 3801138
LA 276 6806 0 0 83 957 816 18779 0 2410708
MA 454 11151 0 3898 0 0 15727 8599 42074 230382
MD 999 19869 0 2266 441 0 480 10745 2423 2030985
ME 39 8923 0 61336 267 0 39734 6449 443024 387678
MI 4002 1099478 0 46662 1901 0 23389 27059 69017 12832067
MN 4 7606785 0 48212 3096 0 1158 33721 10776 39123408
MO 0 2774597 0 9056 2044 0 799 103669 144 6989424
MS 435 401898 0 229 49 246 2675 24528 0 3378718
MT 11 173953 0 8682 14671 0 42 10323 0 1014153
NC 1422 8901434 0 16293 312 2 8427 66367 197 3105015
ND 0 133653 0 85844 9560 0 64 4740 0 12084880
NE 31 2992576 0 22823 23152 0 144 25840 0 34793746
NH 132 3287 0 172 301 0 1068 4875 82574 226527
NJ 4873 7901 0 2427 199 0 13872 8258 406 383621
NM 230 1294 0 0 5156 0 50 30981 0 2079923
NV 67 0 0 7273 80 0 20 21388 0 161218
NY 2003 74671 0 21865 997 0 4217 36441 358603 10457196
OH 1244 2058503 0 2074 849 0 1793 51558 94133 14065910
OK 651 2304740 0 489 9685 0 345 89060 0 1165560
OR 722 12693 0 41667 1398 71 24573 33226 42 1161748
PA 4831 1134957 0 8659 1308 0 2671 50174 96266 10393075
RI 39 1830 0 558 0 0 339 886 259 903
SC 16274 224076 0 308 131 4 1450 38732 0 1077421
SD 1 1191162 0 156 33637 6 68 16545 0 17327763
TN 672 147795 0 359 346 0 990 91716 33 2704320
TX 4402 800893 37 22535 4378 24778 2048 878914 0 8647322
UT 1594 731666 0 981 1132 0 383 14723 0 1337123
VA 1773 239899 0 5423 1037 0 1120 50831 1800 2571079
VT 30 3874 0 267 108 0 749 10589 999391 1558863
WA 2714 19861 0 163925 961 1 24076 27062 4 2926068
WI 31 311651 0 66400 4246 0 22362 61111 66115 24132431
WV 1229 5873 0 335 45 0 490 18825 1341 364360
WY 0 85432 0 865 9569 0 12 9246 0 896419
DC 0 0 0 0 0 0 0 0 0 0
importScripts("https://d3js.org/d3-collection.v1.min.js");
importScripts("https://d3js.org/d3-dispatch.v1.min.js");
importScripts("https://d3js.org/d3-quadtree.v1.min.js");
importScripts("https://d3js.org/d3-timer.v1.min.js");
importScripts("https://d3js.org/d3-force.v1.min.js");
onmessage = function(event) {
var nodes = event.data.nodes, links = event.data.links;
// Compute the layout
var simulation = d3
.forceSimulation(nodes)
.force("cx", d3.forceX().x(d => 480).strength(0.02))
.force("cy", d3.forceY().y(d => 300).strength(0.02))
.force("link", d3.forceLink(links).distance(d => d.distance))
.force("x", d3.forceX().x(d => d.x0).strength(0.1))
.force("y", d3.forceY().y(d => d.y0).strength(0.1))
.force("collide", d3.forceCollide().strength(0.8).radius(d => d.r + 3))
.stop();
while (simulation.alpha() > 0.001) {
simulation.tick();
}
// Compute the point-point pairs for animation
nodes.forEach(function(node) {
var circle = 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];
});
// Extra rings fold into the closest point on the main circle
var closestPoints = node.rings.slice(1).map(function(ring) {
var angle = Math.atan2(ring.centroid[1] - node.y, ring.centroid[0] - node.x),
point = [Math.cos(angle) * node.r + node.x, Math.sin(angle) * node.r + node.y];
return new Array(ring.length).fill(point);
});
node.targets = [circle, ...closestPoints];
node.circlePath = trueCircle(node);
});
postMessage(nodes);
};
function distance(a, b) {
return Math.sqrt((a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]));
}
function trueCircle(node) {
var left = [node.x - node.r, node.y], right = [node.x + node.r, node.y], r = [node.r, node.r];
return "M" + left + "A" + r + ",0,1,1," + right + "A" + r + ",0,1,1," + left + "Z";
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment