(function(exports) { /* * d3.cartogram is a d3-friendly implementation of An Algorithm to Construct * Continuous Area Cartograms: * * * * It requires topojson to decode TopoJSON-encoded topologies: * * * * 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);