Skip to content

Instantly share code, notes, and snippets.

@HarryStevens
Last active April 27, 2017 07:23
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 HarryStevens/3b2b47a50de220e901244c2ee54b0716 to your computer and use it in GitHub Desktop.
Save HarryStevens/3b2b47a50de220e901244c2ee54b0716 to your computer and use it in GitHub Desktop.
Contiguous Cartogram I
height: 530
license: gpl-3.0
(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);
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
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.
<!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>
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
}
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