An implementation of d3-cartogram in D3.js version 2. Based on Maggie Lee's block of Georgia counties, which was based on Jeff Fletcher's tutorial. The algorithm used to construct the cartogram is by James A. Dougenik, Nicholas R. Chrisman, and Duane R. Niemeyer.
Last active
December 20, 2024 05:13
-
-
Save HarryStevens/3b2b47a50de220e901244c2ee54b0716 to your computer and use it in GitHub Desktop.
Contiguous Cartogram I
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
height: 530 | |
license: gpl-3.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function(exports) { | |
/* | |
* d3.cartogram is a d3-friendly implementation of An Algorithm to Construct | |
* Continuous Area Cartograms: | |
* | |
* <http://chrisman.scg.ulaval.ca/G360/dougenik.pdf> | |
* | |
* It requires topojson to decode TopoJSON-encoded topologies: | |
* | |
* <http://github.com/mbostock/topojson/> | |
* | |
* Usage: | |
* | |
* var cartogram = d3.cartogram() | |
* .projection(d3.geo.albersUsa()) | |
* .value(function(d) { | |
* return Math.random() * 100; | |
* }); | |
* d3.json("path/to/topology.json", function(topology) { | |
* var features = cartogram(topology); | |
* d3.select("svg").selectAll("path") | |
* .data(features) | |
* .enter() | |
* .append("path") | |
* .attr("d", cartogram.path); | |
* }); | |
*/ | |
d3.cartogram = function() { | |
function carto(topology, geometries) { | |
// copy it first | |
topology = copy(topology); | |
// objects are projected into screen coordinates | |
var projectGeometry = projector(projection); | |
// project the arcs into screen space | |
var tf = transformer(topology.transform), | |
projectedArcs = topology.arcs.map(function(arc) { | |
var x = 0, y = 0; | |
return arc.map(function(coord) { | |
coord[0] = (x += coord[0]); | |
coord[1] = (y += coord[1]); | |
return projection(tf(coord)); | |
}); | |
}); | |
// path with identity projection | |
var path = d3.geo.path() | |
.projection(ident); | |
var objects = object(projectedArcs, {type: "GeometryCollection", geometries: geometries}) | |
.geometries.map(function(geom) { | |
return { | |
type: "Feature", | |
id: geom.id, | |
properties: properties.call(null, geom, topology), | |
geometry: geom | |
}; | |
}); | |
var values = objects.map(value), | |
totalValue = sum(values); | |
// no iterations; just return the features | |
if (iterations <= 0) { | |
return objects; | |
} | |
var i = 0, | |
targetSizeError = 1; | |
while (i++ < iterations) { | |
var areas = objects.map(path.area), | |
totalArea = sum(areas), | |
sizeErrors = [], | |
meta = objects.map(function(o, j) { | |
var area = Math.abs(areas[j]), // XXX: why do we have negative areas? | |
v = +values[j], | |
desired = totalArea * v / totalValue, | |
radius = Math.sqrt(area / Math.PI), | |
mass = Math.sqrt(desired / Math.PI) - radius, | |
sizeError = Math.max(area, desired) / Math.min(area, desired); | |
sizeErrors.push(sizeError); | |
// console.log(o.id, "@", j, "area:", area, "value:", v, "->", desired, radius, mass, sizeError); | |
return { | |
id: o.id, | |
area: area, | |
centroid: path.centroid(o), | |
value: v, | |
desired: desired, | |
radius: radius, | |
mass: mass, | |
sizeError: sizeError | |
}; | |
}); | |
var sizeError = mean(sizeErrors), | |
forceReductionFactor = 1 / (1 + sizeError); | |
// console.log("meta:", meta); | |
// console.log(" total area:", totalArea); | |
// console.log(" force reduction factor:", forceReductionFactor, "mean error:", sizeError); | |
projectedArcs.forEach(function(arc) { | |
arc.forEach(function(coord) { | |
// create an array of vectors: [x, y] | |
var vectors = meta.map(function(d) { | |
var centroid = d.centroid, | |
mass = d.mass, | |
radius = d.radius, | |
theta = angle(centroid, coord), | |
dist = distance(centroid, coord), | |
Fij = (dist > radius) | |
? mass * radius / dist | |
: mass * | |
(Math.pow(dist, 2) / Math.pow(radius, 2)) * | |
(4 - 3 * dist / radius); | |
return [ | |
Fij * Math.cos(theta), | |
Fij * Math.sin(theta) | |
]; | |
}); | |
// using Fij and angles, calculate vector sum | |
var delta = vectors.reduce(function(a, b) { | |
return [ | |
a[0] + b[0], | |
a[1] + b[1] | |
]; | |
}, [0, 0]); | |
delta[0] *= forceReductionFactor; | |
delta[1] *= forceReductionFactor; | |
coord[0] += delta[0]; | |
coord[1] += delta[1]; | |
}); | |
}); | |
// break if we hit the target size error | |
if (sizeError <= targetSizeError) break; | |
} | |
return { | |
features: objects, | |
arcs: projectedArcs | |
}; | |
} | |
var iterations = 8, | |
projection = d3.geo.albers(), | |
properties = function(id) { | |
return {}; | |
}, | |
value = function(d) { | |
return 1; | |
}; | |
// for convenience | |
carto.path = d3.geo.path() | |
.projection(ident); | |
carto.iterations = function(i) { | |
if (arguments.length) { | |
iterations = i; | |
return carto; | |
} else { | |
return iterations; | |
} | |
}; | |
carto.value = function(v) { | |
if (arguments.length) { | |
value = d3.functor(v); | |
return carto; | |
} else { | |
return value; | |
} | |
}; | |
carto.projection = function(p) { | |
if (arguments.length) { | |
projection = p; | |
return carto; | |
} else { | |
return projection; | |
} | |
}; | |
carto.feature = function(topology, geom) { | |
return { | |
type: "Feature", | |
id: geom.id, | |
properties: properties.call(null, geom, topology), | |
geometry: { | |
type: geom.type, | |
coordinates: topojson.object(topology, geom).coordinates | |
} | |
}; | |
}; | |
carto.features = function(topo, geometries) { | |
return geometries.map(function(f) { | |
return carto.feature(topo, f); | |
}); | |
}; | |
carto.properties = function(props) { | |
if (arguments.length) { | |
properties = d3.functor(props); | |
return carto; | |
} else { | |
return properties; | |
} | |
}; | |
return carto; | |
}; | |
var transformer = d3.cartogram.transformer = function(tf) { | |
var kx = tf.scale[0], | |
ky = tf.scale[1], | |
dx = tf.translate[0], | |
dy = tf.translate[1]; | |
function transform(c) { | |
return [c[0] * kx + dx, c[1] * ky + dy]; | |
} | |
transform.invert = function(c) { | |
return [(c[0] - dx) / kx, (c[1]- dy) / ky]; | |
}; | |
return transform; | |
}; | |
function sum(numbers) { | |
var total = 0; | |
for (var i = numbers.length - 1; i-- > 0;) { | |
total += numbers[i]; | |
} | |
return total; | |
} | |
function mean(numbers) { | |
return sum(numbers) / numbers.length; | |
} | |
function angle(a, b) { | |
return Math.atan2(b[1] - a[1], b[0] - a[0]); | |
} | |
function distance(a, b) { | |
var dx = b[0] - a[0], | |
dy = b[1] - a[1]; | |
return Math.sqrt(dx * dx + dy * dy); | |
} | |
function projector(proj) { | |
var types = { | |
Point: proj, | |
LineString: function(coords) { | |
return coords.map(proj); | |
}, | |
MultiLineString: function(arcs) { | |
return arcs.map(types.LineString); | |
}, | |
Polygon: function(rings) { | |
return rings.map(types.LineString); | |
}, | |
MultiPolygon: function(rings) { | |
return rings.map(types.Polygon); | |
} | |
}; | |
return function(geom) { | |
return types[geom.type](geom.coordinates); | |
}; | |
} | |
// identity projection | |
function ident(c) { | |
return c; | |
} | |
function copy(o) { | |
return (o instanceof Array) | |
? o.map(copy) | |
: (typeof o === "string" || typeof o === "number") | |
? o | |
: copyObject(o); | |
} | |
function copyObject(o) { | |
var obj = {}; | |
for (var k in o) obj[k] = copy(o[k]); | |
return obj; | |
} | |
function object(arcs, o) { | |
function arc(i, points) { | |
if (points.length) points.pop(); | |
for (var a = arcs[i < 0 ? ~i : i], k = 0, n = a.length; k < n; ++k) { | |
points.push(a[k]); | |
} | |
if (i < 0) reverse(points, n); | |
} | |
function line(arcs) { | |
var points = []; | |
for (var i = 0, n = arcs.length; i < n; ++i) arc(arcs[i], points); | |
return points; | |
} | |
function polygon(arcs) { | |
return arcs.map(line); | |
} | |
function geometry(o) { | |
o = Object.create(o); | |
o.coordinates = geometryType[o.type](o.arcs); | |
return o; | |
} | |
var geometryType = { | |
LineString: line, | |
MultiLineString: polygon, | |
Polygon: polygon, | |
MultiPolygon: function(arcs) { return arcs.map(polygon); } | |
}; | |
return o.type === "GeometryCollection" | |
? (o = Object.create(o), o.geometries = o.geometries.map(geometry), o) | |
: geometry(o); | |
} | |
function reverse(array, n) { | |
var t, j = array.length, i = j - n; while (i < --j) t = array[i], array[i++] = array[j], array[j] = t; | |
} | |
})(this); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
state_ut | population_2011 | sex_ratio | |
---|---|---|---|
Uttar Pradesh | 199281477 | 908 | |
Maharashtra | 112372972 | 946 | |
Bihar | 103804637 | 916 | |
West Bengal | 91347736 | 947 | |
Madhya Pradesh | 72597565 | 931 | |
Tamil Nadu | 72138958 | 995 | |
Rajasthan | 68621012 | 926 | |
Karnataka | 61130704 | 968 | |
Gujarat | 60383628 | 918 | |
Andhra Pradesh | 49386799 | 993 | |
Odisha | 41947358 | 978 | |
Telangana | 35286757 | 988 | |
Kerala | 33387677 | 1,084 | |
Jharkhand | 32966238 | 947 | |
Assam | 31169272 | 954 | |
Punjab | 27704236 | 893 | |
Chhattisgarh | 25540196 | 991 | |
Haryana | 25353081 | 877 | |
Jammu & Kashmir | 12548926 | 883 | |
Uttarakhand | 10116752 | 963 | |
Himachal Pradesh | 6864602 | 974 | |
Tripura | 3671032 | 961 | |
Meghalaya | 2964007 | 986 | |
Manipur | 2721756 | 987 | |
Nagaland | 1980602 | 931 | |
Goa | 1457723 | 968 | |
Arunachal Pradesh | 1382611 | 920 | |
Mizoram | 1091014 | 975 | |
Sikkim | 607688 | 889 | |
NCT of Delhi | 16753235 | 866 | |
Puducherry | 1244464 | 1,038 | |
Chandigarh | 1054686 | 818 | |
Andaman & Nicobar Islands | 379944 | 878 | |
Dadra & Nagar Haveli | 342853 | 775 | |
Daman & Diu | 242911 | 618 | |
Lakshadweep | 64429 | 946 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<head> | |
<meta charset="utf-8"> | |
</head> | |
<body> | |
<div id="legendholder"> | |
<button id="click_to_run" onclick="do_update()">View by Population</button> | |
<button id="click_to_normal" onclick="do_normal()">View Normal</button> | |
</div> | |
<div id="map-wrapper"></div> | |
<script src="https://d3js.org/d3.v2.min.js"></script> | |
<script src="topojson.js"></script> | |
<script src="cartogram.js"></script> | |
<script src="scripts.js"></script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var width = window.innerWidth, | |
height = window.innerHeight, | |
margin = {top: 0, bottom: 0, left: 0, right: 0}; | |
var svg = d3.select("#map-wrapper").append("svg") | |
.attr("width", width - margin.left - margin.right) | |
.attr("height", height - margin.top - margin.bottom); | |
var states = svg.append("g") | |
.attr("id", "states") | |
.selectAll("path"); | |
var projection = d3.geo.albers() | |
.origin([79.375986, 23.368801]) | |
.scale(1000); | |
var topology, | |
geometries, | |
carto_features; | |
var pop_data = d3.map(); | |
var carto = d3.cartogram() | |
.projection(projection) | |
.properties(function (d) { | |
// this adds the "properties" properties to the geometries | |
return d.properties; | |
}); | |
d3.csv("data.csv", function(data){ | |
data.forEach(function(d){ | |
pop_data.set(d.state_ut, [d.population_2011, d.sex_ratio]) | |
}); | |
}); | |
d3.json("geo.json", function(data){ | |
topology = data; | |
geometries = topology.objects['india_state'].geometries; | |
var features = carto.features(topology, geometries), | |
path = d3.geo.path() | |
.projection(projection); | |
states = states.data(features) | |
.enter() | |
.append("path") | |
.attr("class", "state") | |
.attr("id", function (d) { return slugify(d.properties.ST_NM); }) | |
.attr("fill", "white") | |
.attr("d", path) | |
.attr("stroke", "black"); | |
}); | |
function do_update() { | |
d3.select("#click_to_run").text("thinking..."); | |
setTimeout(function () { | |
carto.value(function (d) { | |
var ret = +pop_data.get(d.properties['ST_NM'])[0] | |
return ret; | |
}); | |
if (carto_features == undefined) | |
carto_features = carto(topology, geometries).features; | |
states.data(carto_features) | |
.text(function (d) { | |
return d.properties.ST_NM; | |
}) | |
states.transition() | |
.duration(3750) | |
.each("end", function () { | |
d3.select("#click_to_run").text("View by Population") | |
}) | |
.attr("d", carto.path); | |
}, 10); | |
} | |
function do_normal() { | |
d3.select("#click_to_normal").text("thinking..."); | |
setTimeout(function () { | |
var features = carto.features(topology, geometries), | |
path = d3.geo.path() | |
.projection(projection); | |
states.data(features) | |
.transition() | |
.duration(3750) | |
.each("end", function () { | |
d3.select("#click_to_normal").text("View Normal") | |
}) | |
.attr("d", path); | |
}, 10); | |
}; | |
function slugify(text){ | |
return text.toString().toLowerCase() | |
.replace(/\s+/g, '-') // Replace spaces with - | |
.replace(/[^\w\-]+/g, '') // Remove all non-word chars | |
.replace(/\-\-+/g, '-') // Replace multiple - with single - | |
.replace(/^-+/, '') // Trim - from start of text | |
.replace(/-+$/, ''); // Trim - from end of text | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
topojson = (function() { | |
function merge(topology, arcs) { | |
var arcsByEnd = {}, | |
fragmentByStart = {}, | |
fragmentByEnd = {}; | |
arcs.forEach(function(i) { | |
var e = ends(i); | |
(arcsByEnd[e[0]] || (arcsByEnd[e[0]] = [])).push(i); | |
(arcsByEnd[e[1]] || (arcsByEnd[e[1]] = [])).push(~i); | |
}); | |
arcs.forEach(function(i) { | |
var e = ends(i), | |
start = e[0], | |
end = e[1], | |
f, g; | |
if (f = fragmentByEnd[start]) { | |
delete fragmentByEnd[f.end]; | |
f.push(i); | |
f.end = end; | |
if (g = fragmentByStart[end]) { | |
delete fragmentByStart[g.start]; | |
var fg = g === f ? f : f.concat(g); | |
fragmentByStart[fg.start = f.start] = fragmentByEnd[fg.end = g.end] = fg; | |
} else if (g = fragmentByEnd[end]) { | |
delete fragmentByStart[g.start]; | |
delete fragmentByEnd[g.end]; | |
var fg = f.concat(g.map(function(i) { return ~i; }).reverse()); | |
fragmentByStart[fg.start = f.start] = fragmentByEnd[fg.end = g.start] = fg; | |
} else { | |
fragmentByStart[f.start] = fragmentByEnd[f.end] = f; | |
} | |
} else if (f = fragmentByStart[end]) { | |
delete fragmentByStart[f.start]; | |
f.unshift(i); | |
f.start = start; | |
if (g = fragmentByEnd[start]) { | |
delete fragmentByEnd[g.end]; | |
var gf = g === f ? f : g.concat(f); | |
fragmentByStart[gf.start = g.start] = fragmentByEnd[gf.end = f.end] = gf; | |
} else if (g = fragmentByStart[start]) { | |
delete fragmentByStart[g.start]; | |
delete fragmentByEnd[g.end]; | |
var gf = g.map(function(i) { return ~i; }).reverse().concat(f); | |
fragmentByStart[gf.start = g.end] = fragmentByEnd[gf.end = f.end] = gf; | |
} else { | |
fragmentByStart[f.start] = fragmentByEnd[f.end] = f; | |
} | |
} else if (f = fragmentByStart[start]) { | |
delete fragmentByStart[f.start]; | |
f.unshift(~i); | |
f.start = end; | |
if (g = fragmentByEnd[end]) { | |
delete fragmentByEnd[g.end]; | |
var gf = g === f ? f : g.concat(f); | |
fragmentByStart[gf.start = g.start] = fragmentByEnd[gf.end = f.end] = gf; | |
} else if (g = fragmentByStart[end]) { | |
delete fragmentByStart[g.start]; | |
delete fragmentByEnd[g.end]; | |
var gf = g.map(function(i) { return ~i; }).reverse().concat(f); | |
fragmentByStart[gf.start = g.end] = fragmentByEnd[gf.end = f.end] = gf; | |
} else { | |
fragmentByStart[f.start] = fragmentByEnd[f.end] = f; | |
} | |
} else if (f = fragmentByEnd[end]) { | |
delete fragmentByEnd[f.end]; | |
f.push(~i); | |
f.end = start; | |
if (g = fragmentByEnd[start]) { | |
delete fragmentByStart[g.start]; | |
var fg = g === f ? f : f.concat(g); | |
fragmentByStart[fg.start = f.start] = fragmentByEnd[fg.end = g.end] = fg; | |
} else if (g = fragmentByStart[start]) { | |
delete fragmentByStart[g.start]; | |
delete fragmentByEnd[g.end]; | |
var fg = f.concat(g.map(function(i) { return ~i; }).reverse()); | |
fragmentByStart[fg.start = f.start] = fragmentByEnd[fg.end = g.start] = fg; | |
} else { | |
fragmentByStart[f.start] = fragmentByEnd[f.end] = f; | |
} | |
} else { | |
f = [i]; | |
fragmentByStart[f.start = start] = fragmentByEnd[f.end = end] = f; | |
} | |
}); | |
function ends(i) { | |
var arc = topology.arcs[i], p0 = arc[0], p1 = [0, 0]; | |
arc.forEach(function(dp) { p1[0] += dp[0], p1[1] += dp[1]; }); | |
return [p0, p1]; | |
} | |
var fragments = []; | |
for (var k in fragmentByEnd) fragments.push(fragmentByEnd[k]); | |
return fragments; | |
} | |
function mesh(topology, o, filter) { | |
var arcs = []; | |
if (arguments.length > 1) { | |
var geomsByArc = [], | |
geom; | |
function arc(i) { | |
if (i < 0) i = ~i; | |
(geomsByArc[i] || (geomsByArc[i] = [])).push(geom); | |
} | |
function line(arcs) { | |
arcs.forEach(arc); | |
} | |
function polygon(arcs) { | |
arcs.forEach(line); | |
} | |
function geometry(o) { | |
geom = o; | |
geometryType[o.type](o.arcs); | |
} | |
var geometryType = { | |
LineString: line, | |
MultiLineString: polygon, | |
Polygon: polygon, | |
MultiPolygon: function(arcs) { arcs.forEach(polygon); } | |
}; | |
o.type === "GeometryCollection" | |
? o.geometries.forEach(geometry) | |
: geometry(o); | |
if (arguments.length < 3) for (var i in geomsByArc) arcs.push([i]); | |
else for (var i in geomsByArc) if (filter((geom = geomsByArc[i])[0], geom[geom.length - 1])) arcs.push([i]); | |
} else { | |
for (var i = 0, n = topology.arcs.length; i < n; ++i) arcs.push([i]); | |
} | |
return object(topology, {type: "MultiLineString", arcs: merge(topology, arcs)}); | |
} | |
function object(topology, o) { | |
var tf = topology.transform, | |
kx = tf.scale[0], | |
ky = tf.scale[1], | |
dx = tf.translate[0], | |
dy = tf.translate[1], | |
arcs = topology.arcs; | |
function arc(i, points) { | |
if (points.length) points.pop(); | |
for (var a = arcs[i < 0 ? ~i : i], k = 0, n = a.length, x = 0, y = 0, p; k < n; ++k) points.push([ | |
(x += (p = a[k])[0]) * kx + dx, | |
(y += p[1]) * ky + dy | |
]); | |
if (i < 0) reverse(points, n); | |
} | |
function line(arcs) { | |
var points = []; | |
for (var i = 0, n = arcs.length; i < n; ++i) arc(arcs[i], points); | |
return points; | |
} | |
function polygon(arcs) { | |
return arcs.map(line); | |
} | |
function geometry(o) { | |
o = Object.create(o); | |
o.coordinates = geometryType[o.type](o.arcs); | |
return o; | |
} | |
var geometryType = { | |
LineString: line, | |
MultiLineString: polygon, | |
Polygon: polygon, | |
MultiPolygon: function(arcs) { return arcs.map(polygon); } | |
}; | |
return o.type === "GeometryCollection" | |
? (o = Object.create(o), o.geometries = o.geometries.map(geometry), o) | |
: geometry(o); | |
} | |
function reverse(array, n) { | |
var t, j = array.length, i = j - n; while (i < --j) t = array[i], array[i++] = array[j], array[j] = t; | |
} | |
return { | |
version: "0.0.3", | |
mesh: mesh, | |
object: object | |
}; | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you for sharing this. This is super helpful.