UNPKG

topogram

Version:

Create continuous area cartograms with TopoJSON and D3

415 lines (353 loc) 13 kB
<!DOCTYPE html> <html> <head> <title>Cartograms with d3 &amp; TopoJSON</title> <meta charset="utf-8"> <meta property="og:image" content="placeholder.png"> <script src="http://d3js.org/d3.v4.js"></script> <script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script> <script src="build/topogram.js"></script> <style type="text/css"> body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.4em; padding: 0; margin: 0; } #container { width: 800px; margin: 20px auto; } h1 { font-size: 200%; margin: 0 0 15px 0; } h2 { font-size: 160%; margin: 0 0 10px 0; } p { margin: 0 0 10px; } form, form > * { margin: 0; } #status { color: #999; } #map-container { height: 500px; text-align: center; position: relative; margin: 20px 0; } #map { display: block; position: absolute; background: #fff; width: 100%; height: 100%; margin: 0; } path.state { stroke: #666; stroke-width: .5; } path.state:hover { stroke: #000; } form { font-size: 120%; } select { font-size: inherit; } #placeholder { position: absolute; z-index: -1; display: block; left: 0; top: 0; } </style> </head> <body> <div id="container"> <h1>Cartograms with d3 &amp; TopoJSON</h1> <form> <p> <label>Scale by <select id="field"></select></label> <label>in <select id="year"></select></label> <span id="status"></span> </p> </form> <div id="map-container"> <img id="placeholder" alt="placeholder image for old browsers" src="placeholder.png"> <svg id="map"></svg> </div> <div id="about"> <h2>About</h2> <p><a href="cartogram.js">cartogram.js</a> is a JavaScript implementation of <a href="http://lambert.nico.free.fr/tp/biblio/Dougeniketal1985.pdf">an algoritm to construct continuous area cartograms</a>, by James A. Dougenik, Nicholas R. Chrisman and Duane R. Niemeyer, &copy;1985 by the Association of American Geographers. This example combines <a href="http://github.com/mbostock/topojson">TopoJSON</a>-encoded boundaries of the United States from <a href="http://www.naturalearthdata.com/downloads/110m-cultural-vectors/">Natural Earth</a> with <a href="http://www.census.gov/popest/data/state/totals/2011/">2011 US Census population estimates</a> to size each state proportionally.</p> <p>There&rsquo;s also a <a class="hashish" href="?segmentized">segmentized topology</a>, which distorts the shapes more fluidly than the <a class="hashish" href="?">original</a>.</p> <p>Designed and built by <a href="http://stamen.com/studio/shawn">Shawn Allen</a> at <a href="http://stamen.com">Stamen</a>. But <a href="http://d3js.org">d3.js</a> does most of the heavy lifting; colors by <a href="http://colorbrewer2.org">colorbrewer</a>.</p> </div> <a href="https://github.com/shawnbot/topogram/"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png" alt="Fork me on GitHub"></a> </div> <script> // hide the form if the browser doesn't do SVG, // (then just let everything else fail) if (!document.createElementNS) { document.getElementsByTagName("form")[0].style.display = "none"; } // field definitions from: // <http://www.census.gov/popest/data/national/totals/2011/files/NST-EST2011-alldata.pdf> var percent = (function() { var fmt = d3.format(".2f"); return function(n) { return fmt(n) + "%"; }; })(), fields = [ {name: "(no scale)", id: "none"}, // {name: "Census Population", id: "censuspop", key: "CENSUS%dPOP", years: [2010]}, // {name: "Estimate Base", id: "censuspop", key: "ESTIMATESBASE%d", years: [2010]}, {name: "Population Estimate", id: "popest", key: "POPESTIMATE%d"}, {name: "Population Change", id: "popchange", key: "NPOPCHG_%d", format: "+,"}, {name: "Births", id: "births", key: "BIRTHS%d"}, {name: "Deaths", id: "deaths", key: "DEATHS%d"}, {name: "Natural Increase", id: "natinc", key: "NATURALINC%d", format: "+,"}, {name: "Int'l Migration", id: "intlmig", key: "INTERNATIONALMIG%d", format: "+,"}, {name: "Domestic Migration", id: "domesticmig", key: "DOMESTICMIG%d", format: "+,"}, {name: "Net Migration", id: "netmig", key: "NETMIG%d", format: "+,"}, {name: "Residual", id: "residual", key: "RESIDUAL%d", format: "+,"}, {name: "Birth Rate", id: "birthrate", key: "RBIRTH%d", years: [2011], format: percent}, {name: "Death Rate", id: "deathrate", key: "RDEATH%d", years: [2011], format: percent}, {name: "Natural Increase Rate", id: "natincrate", key: "RNATURALINC%d", years: [2011], format: percent}, {name: "Int'l Migration Rate", id: "intlmigrate", key: "RINTERNATIONALMIG%d", years: [2011], format: percent}, {name: "Net Domestic Migration Rate", id: "domesticmigrate", key: "RDOMESTICMIG%d", years: [2011], format: percent}, {name: "Net Migration Rate", id: "netmigrate", key: "RNETMIG%d", years: [2011], format: percent}, ], years = [2010, 2011], fieldsById = d3.nest() .key(function(d) { return d.id; }) .rollup(function(d) { return d[0]; }) .map(fields), field = fields[0], year = years[0], colors = d3.schemeRdYlBu[3].reverse() .map(function(rgb) { return d3.hsl(rgb); }); var body = d3.select("body"), stat = d3.select("#status"); var fieldSelect = d3.select("#field") .on("change", function(e) { field = fields[this.selectedIndex]; location.hash = "#" + [field.id, year].join("/"); }); fieldSelect.selectAll("option") .data(fields) .enter() .append("option") .attr("value", function(d) { return d.id; }) .text(function(d) { return d.name; }); var yearSelect = d3.select("#year") .on("change", function(e) { year = years[this.selectedIndex]; location.hash = "#" + [field.id, year].join("/"); }); yearSelect.selectAll("option") .data(years) .enter() .append("option") .attr("value", function(y) { return y; }) .text(function(y) { return y; }); var map = d3.select("#map"), layer = map.append("g") .attr("id", "layer"), states = layer.append("g") .attr("id", "states") .selectAll("path"); var translation = [-38, 32], scaling = 0.94; layer.attr("transform", "translate(" + translation + ")" + "scale(" + scaling + ")"); var proj = d3.geoAlbersUsa(), topology, geometries, rawData, dataById = {}, carto = d3.cartogram() .projection(proj) .properties(function(d) { return dataById.get(d.id); }) .value(function(d) { return +d.properties[field]; }); window.onhashchange = function() { parseHash(); }; var segmentized = location.search === "?segmentized", url = ["data", segmentized ? "us-states-segmentized.topojson" : "us-states.topojson" ].join("/"); d3.json(url, function(topo) { topology = topo; geometries = topology.objects.states.geometries; d3.csv("data/nst_2011.csv", function(data) { rawData = data; dataById = d3.nest() .key(function(d) { return d.NAME; }) .rollup(function(d) { return d[0]; }) .map(data); init(); }); }); function init() { var features = carto.features(topology, geometries), path = d3.geoPath() .projection(proj); states = states.data(features) .enter() .append("path") .attr("class", "state") .attr("id", function(d) { return d.properties.NAME; }) .attr("fill", "#fafafa") .attr("d", path); states.append("title"); parseHash(); } function reset() { stat.text(""); body.classed("updating", false); var features = carto.features(topology, geometries), path = d3.geoPath() .projection(proj); states.data(features) .transition() .duration(750) .ease(d3.easeLinear) .attr("fill", "#fafafa") .attr("d", path); states.select("title") .text(function(d) { return d.properties.NAME; }); } function update() { var start = Date.now(); body.classed("updating", true); var key = field.key.replace("%d", year), fmt = (typeof field.format === "function") ? field.format : d3.format(field.format || ","), value = function(d) { return +d.properties[key]; }, values = states.data() .map(value) .filter(function(n) { return !isNaN(n); }) .sort(d3.ascending), lo = values[0], hi = values[values.length - 1]; var color = d3.scaleLinear() .range(colors) .domain(lo < 0 ? [lo, 0, hi] : [lo, d3.mean(values), hi]); // normalize the scale to positive numbers var scale = d3.scaleLinear() .domain([lo, hi]) .range([1, 1000]); // tell the cartogram to use the scaled values carto.value(function(d) { return scale(value(d)); }); // generate the new features, pre-projected var features = carto(topology, geometries).features; // update the data states.data(features) .select("title") .text(function(d) { return [d.properties.NAME, fmt(value(d))].join(": "); }); states.transition() .duration(750) .ease(d3.easeLinear) .attr("fill", function(d) { return color(value(d)); }) .attr("d", carto.path); var delta = (Date.now() - start) / 1000; stat.text(["calculated in", delta.toFixed(1), "seconds"].join(" ")); body.classed("updating", false); } var deferredUpdate = (function() { var timeout; return function() { var args = arguments; clearTimeout(timeout); stat.text("calculating..."); return timeout = setTimeout(function() { update.apply(null, arguments); }, 10); }; })(); var hashish = d3.selectAll("a.hashish") .datum(function() { return this.href; }); function parseHash() { var parts = location.hash.substr(1).split("/"), desiredFieldId = parts[0], desiredYear = +parts[1]; field = fieldsById.get(desiredFieldId) || fields[0]; year = (years.indexOf(desiredYear) > -1) ? desiredYear : years[0]; fieldSelect.property("selectedIndex", fields.indexOf(field)); if (field.id === "none") { yearSelect.attr("disabled", "disabled"); reset(); } else { if (field.years) { if (field.years.indexOf(year) === -1) { year = field.years[0]; } yearSelect.selectAll("option") .attr("disabled", function(y) { return (field.years.indexOf(y) === -1) ? "disabled" : null; }); } else { yearSelect.selectAll("option") .attr("disabled", null); } yearSelect .property("selectedIndex", years.indexOf(year)) .attr("disabled", null); deferredUpdate(); location.replace("#" + [field.id, year].join("/")); hashish.attr("href", function(href) { return href + location.hash; }); } } </script> </body> </html>