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
forked from veltman's block: Dorling cartogram transitions
xxxxxxxxxx
<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>
https://d3js.org/d3.v4.min.js
https://cdnjs.cloudflare.com/ajax/libs/topojson/3.0.0/topojson.min.js
https://d3js.org/d3-scale-chromatic.v1.min.js