block by mbostock 670b9fe0577a29c39a1803f59c628769

Merged Choropleth

Full Screen

Source: American Community Survey, 2014 5-Year Estimate

A variation of my California population density map using topomerge to combine census tracts in the same density interval. The resulting TopoJSON file is substantially smaller (328K instead of 1.5M), renders faster, and looks better due to fewer antialiasing artifacts!

index.html

<!DOCTYPE html>
<svg width="960" height="1100"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script>

var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height");

var path = d3.geoPath();

var color = d3.scaleThreshold()
    .domain([1, 10, 50, 200, 500, 1000, 2000, 4000])
    .range(d3.schemeOrRd[9]);

var x = d3.scaleSqrt()
    .domain([0, 4500])
    .rangeRound([440, 950]);

var g = svg.append("g")
    .attr("class", "key")
    .attr("transform", "translate(0,40)");

g.selectAll("rect")
  .data(color.range().map(function(d) {
      d = color.invertExtent(d);
      if (d[0] == null) d[0] = x.domain()[0];
      if (d[1] == null) d[1] = x.domain()[1];
      return d;
    }))
  .enter().append("rect")
    .attr("height", 8)
    .attr("x", function(d) { return x(d[0]); })
    .attr("width", function(d) { return x(d[1]) - x(d[0]); })
    .attr("fill", function(d) { return color(d[0]); });

g.append("text")
    .attr("class", "caption")
    .attr("x", x.range()[0])
    .attr("y", -6)
    .attr("fill", "#000")
    .attr("text-anchor", "start")
    .attr("font-weight", "bold")
    .text("Population per square mile");

g.call(d3.axisBottom(x)
    .tickSize(13)
    .tickValues(color.domain()))
  .select(".domain")
    .remove();

d3.json("topo.json", function(error, topology) {
  if (error) throw error;

  svg.append("g")
    .selectAll("path")
    .data(topojson.feature(topology, topology.objects.tracts).features)
    .enter().append("path")
      .attr("fill", function(d) { return d3.schemeOrRd[9][d.id]; })
      .attr("d", path);

  svg.append("path")
      .datum(topojson.feature(topology, topology.objects.counties))
      .attr("fill", "none")
      .attr("stroke", "#000")
      .attr("stroke-opacity", 0.3)
      .attr("d", path);
});

</script>

package.json

{
  "private": true,
  "license": "gpl-3.0",
  "author": {
    "name": "Mike Bostock",
    "url": "https://bost.ocks.org/mike"
  },
  "scripts": {
    "prepublish": "bash prepublish"
  },
  "devDependencies": {
    "d3-array": "^1.0.1",
    "d3-geo-projection": "^1.2.0",
    "ndjson-cli": "^0.3.0",
    "shapefile": "^0.5.8",
    "topojson": "^2.0.0",
    "topojson-client": "^2.1.0",
    "topojson-simplify": "^2.0.0"
  }
}

prepublish

#!/bin/bash

# EPSG:3310 California Albers
PROJECTION='d3.geoAlbers().parallels([34, 40.5]).rotate([120, 0])'

# The state FIPS code.
STATE=06

# The ACS 5-Year Estimate vintage.
YEAR=2014

# The display size.
WIDTH=960
HEIGHT=1100

# Download the census tract boundaries.
# Extract the shapefile (.shp) and dBASE (.dbf).
if [ ! -f cb_${YEAR}_${STATE}_tract_500k.shp ]; then
  curl -o cb_${YEAR}_${STATE}_tract_500k.zip \
    "http://www2.census.gov/geo/tiger/GENZ${YEAR}/shp/cb_${YEAR}_${STATE}_tract_500k.zip"
  unzip -o \
    cb_${YEAR}_${STATE}_tract_500k.zip \
    cb_${YEAR}_${STATE}_tract_500k.shp \
    cb_${YEAR}_${STATE}_tract_500k.dbf
fi

# Download the census tract population estimates.
if [ ! -f cb_${YEAR}_${STATE}_tract_B01003.json ]; then
  curl -o cb_${YEAR}_${STATE}_tract_B01003.json \
    "http://api.census.gov/data/${YEAR}/acs5?get=B01003_001E&for=tract:*&in=state:${STATE}"
fi

# 1. Convert to GeoJSON.
# 2. Project.
# 3. Join with the census data.
# 4. Compute the population density.
# 5. Simplify.
# 6. Compute the county borders.
geo2topo -n \
  tracts=<(ndjson-join 'd.id' \
    <(shp2json cb_${YEAR}_${STATE}_tract_500k.shp \
      | geoproject "${PROJECTION}.fitExtent([[10, 10], [${WIDTH} - 10, ${HEIGHT} - 10]], d)" \
      | ndjson-split 'd.features' \
      | ndjson-map 'd.id = d.properties.GEOID.slice(2), d') \
    <(ndjson-cat cb_${YEAR}_${STATE}_tract_B01003.json \
      | ndjson-split 'd.slice(1)' \
      | ndjson-map '{id: d[2] + d[3], B01003: +d[0]}') \
    | ndjson-map -r d3=d3-array 'd[0].properties = {density: d3.bisect([1, 10, 50, 200, 500, 1000, 2000, 4000], (d[1].B01003 / d[0].properties.ALAND || 0) * 2589975.2356)}, d[0]') \
  | topomerge -k 'd.id.slice(0, 3)' counties=tracts \
  | topomerge --mesh -f 'a !== b' counties=counties \
  | topomerge -k 'd.properties.density' tracts=tracts \
  | toposimplify -p 1 -f \
  > topo.json

# Re-compute the topology as a further optimization.
# This consolidates unique sequences of arcs.
# https://github.com/topojson/topojson-simplify/issues/4
topo2geo \
  < topo.json \
  tracts=tracts.json \
  counties=counties.json

geo2topo \
  tracts=tracts.json \
  counties=counties.json \
  | topoquantize 1e5 \
  > topo.json

rm tracts.json counties.json