Skip to content

Instantly share code, notes, and snippets.

@tnightingale
Last active January 27, 2016 05:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save tnightingale/4687146 to your computer and use it in GitHub Desktop.
Save tnightingale/4687146 to your computer and use it in GitHub Desktop.
Scaling Canada by Area Population
(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);
Geo_Code Prov_Name Topic Characteristic Note Total Flag_Total Male Flag_Male Female Flag_Female
01 Canada Population and dwelling counts Population in 2011 1 33476688 + 0 ... 0 ...
13 New Brunswick Population and dwelling counts Population in 2011 1 751171 0 ... 0 ...
60 Yukon Population and dwelling counts Population in 2011 1 33897 0 ... 0 ...
11 Prince Edward Island Population and dwelling counts Population in 2011 1 140204 0 ... 0 ...
48 Alberta Population and dwelling counts Population in 2011 1 3645257 + 0 ... 0 ...
35 Ontario Population and dwelling counts Population in 2011 1 12851821 + 0 ... 0 ...
10 Newfoundland and Labrador Population and dwelling counts Population in 2011 1 514536 0 ... 0 ...
24 Quebec Population and dwelling counts Population in 2011 1 7903001 + 0 ... 0 ...
61 Northwest Territories Population and dwelling counts Population in 2011 1 41462 0 ... 0 ...
12 Nova Scotia Population and dwelling counts Population in 2011 1 921727 0 ... 0 ...
01 Canada Population and dwelling counts Population in 2006 1 31612897 + 0 ... 0 ...
13 New Brunswick Population and dwelling counts Population in 2006 1 729997 0 ... 0 ...
61 Northwest Territories Population and dwelling counts Population in 2006 1 41464 0 ... 0 ...
60 Yukon Population and dwelling counts Population in 2006 1 30372 0 ... 0 ...
11 Prince Edward Island Population and dwelling counts Population in 2006 1 135851 0 ... 0 ...
48 Alberta Population and dwelling counts Population in 2006 1 3290350 + 0 ... 0 ...
35 Ontario Population and dwelling counts Population in 2006 1 12160282 + 0 ... 0 ...
10 Newfoundland and Labrador Population and dwelling counts Population in 2006 1 505469 0 ... 0 ...
24 Quebec Population and dwelling counts Population in 2006 1 7546131 + 0 ... 0 ...
12 Nova Scotia Population and dwelling counts Population in 2006 1 913462 0 ... 0 ...
47 Saskatchewan Population and dwelling counts Population in 2011 1 1033381 + 0 ... 0 ...
47 Saskatchewan Population and dwelling counts Population in 2006 1 968157 + 0 ... 0 ...
46 Manitoba Population and dwelling counts Population in 2011 1 1208268 + 0 ... 0 ...
46 Manitoba Population and dwelling counts Population in 2006 1 1148401 0 ... 0 ...
59 British Columbia Population and dwelling counts Population in 2011 1 4400057 + 0 ... 0 ...
59 British Columbia Population and dwelling counts Population in 2006 1 4113487 + 0 ... 0 ...
62 Nunavut Population and dwelling counts Population in 2006 1 29474 0 ... 0 ...
62 Nunavut Population and dwelling counts Population in 2011 1 31906 0 ... 0 ...
<!DOCTYPE html>
<html>
<head>
<script src="http://d3js.org/d3.v2.js"></script>
<script src="topojson-v0.0.10.js"></script>
<script src="cartogram.js"></script>
<style type="text/css">
html, body { margin: 0; padding: 0; height: 100%; }
#map {
display: block;
position: absolute;
background: #fff;
width: 100%;
height: 100%;
margin: 0;
}
path.province {
fill: steelblue;
stroke: white;
}
path.province:hover {
fill: orange;
}
</style>
</head>
<body>
<div id="map-container" data-provinces="prov_4326_simple.topo.json" data-profile="census-profile.csv"></div>
<script type="text/javascript">
(function () {
var container = d3.select('#map-container').node(),
data = container.dataset,
census = d3.map(),
scale;
var width = 960,
height = 500;
var proj = d3.geo.albers(),
path = d3.geo.path().projection(proj);
var svg = d3.select(container).append("svg")
.attr("width", width)
.attr("height", height);
d3.json(data.provinces, function (canada) {
d3.csv(data.profile, function (profile) {
profile.forEach(function (d) {
census.set(d.Prov_Name, d);
});
var extent = d3.extent(profile, function (d) { return +d.Total; });
scale = d3.scale.linear()
.domain(extent)
.range([1, 100]);
ready(canada, profile);
});
});
function ready(canada, profile) {
var carto = d3.cartogram()
.projection(proj)
.properties(function (geom, topo) {
return geom.properties;
})
// Morph on the number of characters in province's name.
.value(function (d) {
var p = get_population(d);
s = scale(p);
return s;
});
var no_morph = create_canada(canada);
no_morph.attr("transform", "translate(0,250)" +
"scale(0.5,0.5)");
var provinces = create_canada(canada);
provinces.attr("transform", "translate(450,250)" +
"scale(0.5,0.5)");
var features = carto(canada, canada.objects.provinces.geometries).features;
provinces.data(features)
.transition()
.duration(2000)
.ease("sin-in-out")
.attr("d", carto.path);
}
function get_population(d) {
return +census.get(d.properties.PRENAME).Total;
}
function create_canada(canada) {
var provinces = svg.append("g")
.attr("class", "provinces")
.selectAll("path")
.data(topojson.object(canada, canada.objects.provinces).geometries)
.enter().append("path")
.attr("class", "province")
.attr("d", path);
provinces.append("title")
.text(function(d) { return d.id + ": " + d3.format(',')(get_population(d)); });
return provinces;
}
}());
</script>
</body>
</html>
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.
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);
geomsByArc.forEach(arguments.length < 3
? function(geoms, i) { arcs.push([i]); }
: function(geoms, i) { if (filter(geoms[0], geoms[geoms.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 point(coordinates) {
return [coordinates[0] * kx + dx, coordinates[1] * ky + dy];
}
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);
return o;
}
var geometryType = {
Point: function(o) { return point(o.coordinates); },
MultiPoint: function(o) { return o.coordinates.map(point); },
LineString: function(o) { return line(o.arcs); },
MultiLineString: function(o) { return polygon(o.arcs); },
Polygon: function(o) { return polygon(o.arcs); },
MultiPolygon: function(o) { return o.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;
}
function bisect(a, x) {
var lo = 0, hi = a.length;
while (lo < hi) {
var mid = lo + hi >>> 1;
if (a[mid] < x) lo = mid + 1;
else hi = mid;
}
return lo;
}
function neighbors(topology, objects) {
var objectsByArc = [],
neighbors = objects.map(function() { return []; });
function line(arcs, i) {
arcs.forEach(function(a) {
if (a < 0) a = ~a;
var o = objectsByArc[a] || (objectsByArc[a] = []);
if (!o[i]) o.forEach(function(j) {
var n, k;
k = bisect(n = neighbors[i], j); if (n[k] !== j) n.splice(k, 0, j);
k = bisect(n = neighbors[j], i); if (n[k] !== i) n.splice(k, 0, i);
}), o[i] = i;
});
}
function polygon(arcs, i) {
arcs.forEach(function(arc) { line(arc, i); });
}
function geometry(o, i) {
geometryType[o.type](o.arcs, i);
}
var geometryType = {
LineString: line,
MultiLineString: polygon,
Polygon: polygon,
MultiPolygon: function(arcs, i) { arcs.forEach(function(arc) { polygon(arc, i); }); }
};
objects.forEach(geometry);
return neighbors;
}
return {
version: "0.0.10",
mesh: mesh,
object: object,
neighbors: neighbors
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment