block by almccon 113e1d690bfe61ab10c1612fdf063c85

Washington Population Density by block group

Full Screen

A variation of my Mike Bostock’s California population density map using block groups rather than census tracts. The example exhibits how useful the Census API is: the prepublish script here can automatically grabs the list of counties for the desired state and then the population data for each block group.

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.blockgroups).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-scale": "^1.0.4",
    "d3-scale-chromatic": "^1.1.0",
    "d3-geo-projection": "^1.2.1",
    "ndjson-cli": "^0.3.0",
    "shapefile": "^0.5.9",
    "topojson-server": "^2.0.0",
    "topojson-client": "^2.1.0",
    "topojson-simplify": "^2.0.0"
  }
}

prepublish

#!/bin/bash

# EPSG:3310 California Albers
# Albers adapted for Washington
PROJECTION='d3.geoAlbers().parallels([47, 48]).rotate([120, 0])'

# The state FIPS code. (Washington)
STATE=53

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

# The display size.
WIDTH=960
HEIGHT=1100

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

# Download the list of counties.
if [ ! -f cb_${YEAR}_${STATE}_counties.json ]; then
  curl -o cb_${YEAR}_${STATE}_counties.json \
    "http://api.census.gov/data/${YEAR}/acs5?get=NAME&for=county:*&in=state:${STATE}&key=${CENSUS_KEY}"
fi

# Download the census block group population estimates for each county.
if [ ! -f cb_${YEAR}_${STATE}_bg_B01003.ndjson ]; then
  for COUNTY in $(ndjson-cat cb_${YEAR}_${STATE}_counties.json \
      | ndjson-split \
      | tail -n +2 \
      | ndjson-map 'd[2]' \
      | cut -c 2-4); do
    echo ${COUNTY}
    if [ ! -f cb_${YEAR}_${STATE}_${COUNTY}_bg_B01003.json ]; then
      curl -o cb_${YEAR}_${STATE}_${COUNTY}_bg_B01003.json \
        "http://api.census.gov/data/${YEAR}/acs5?get=B01003_001E&for=block+group:*&in=state:${STATE}+county:${COUNTY}&key=${CENSUS_KEY}"
    fi
    ndjson-cat cb_${YEAR}_${STATE}_${COUNTY}_bg_B01003.json \
      | ndjson-split \
      | tail -n +2 \
      >> cb_${YEAR}_${STATE}_bg_B01003.ndjson
  done
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 \
  blockgroups=<(ndjson-join 'd.id' \
    <(shp2json cb_${YEAR}_${STATE}_bg_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-map < cb_${YEAR}_${STATE}_bg_B01003.ndjson '{id: d[2] + d[3] + d[4], 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=blockgroups \
  | topomerge --mesh -f 'a !== b' counties=counties \
  | topomerge -k 'd.properties.density' blockgroups=blockgroups \
  | 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 \
  blockgroups=blockgroups.json \
  counties=counties.json

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

rm blockgroups.json counties.json