Skip to content

Instantly share code, notes, and snippets.

@mbostock
Last active Jun 29, 2017
Embed
What would you like to do?
California Population Density
license: gpl-3.0
height: 1100
border: no
.DS_Store
node_modules
cb_*

Source: American Community Survey, 2014 5-Year Estimate

This map was inspired by a similar map found on Wikipedia. I wasn’t wild about the diverging color scale, so I thought it would be a fun challenge to recreate. Amazingly, a single command is used to join the Census Bureau’s shapefile and CSV, and then to project, convert to TopoJSON, and simplify:

geo2topo -n \
  tracts=<(ndjson-join 'd.id' \
    <(shp2json cb_2014_06_tract_500k.shp \
      | geoproject 'd3.geoAlbers().parallels([34, 40.5]).rotate([120, 0]).fitExtent([[10, 10], [950, 1090]], d)' \
      | ndjson-split 'd.features' \
      | ndjson-map 'd.id = d.properties.GEOID.slice(2), d') \
    <(ndjson-cat cb_2014_06_tract_B01003.json \
      | ndjson-split 'd.slice(1)' \
      | ndjson-map '{id: d[2] + d[3], B01003: +d[0]}') \
    | ndjson-map 'd[0].properties = {density: Math.floor(d[1].B01003 / d[0].properties.ALAND * 2589975.2356)}, d[0]') \
  | toposimplify -p 1 -f \
  | topomerge -k 'd.id.slice(0, 3)' counties=tracts \
  | topomerge --mesh -f 'a !== b' counties=counties \
  | topoquantize 1e5 \
  > topo.json

The Census Bureau is great, though the user interface of the American FactFinder is somewhat onerous. Here are the nineteen steps to download the data:

  1. Go to factfinder2.census.gov.
  2. Find where it says “American Community Survey” and click “get data »”.
  3. Click the blue “Geographies” button on the left.
  4. In the pop-up, select census tract in the “geographic type” menu.
  5. Select California in the resulting “state” menu.
  6. Click “All Census Tracts within California”.
  7. Click the “ADD TO YOUR SELECTIONS” button.
  8. Click “CLOSE” to dismiss the pop-up.
  9. Click the blue “Topics” button on the left.
  10. In the pop-up, expand the “People” submenu.
  11. Expand the “Basic Count/Estimate” submenu.
  12. Click “Population Total”.
  13. Click “CLOSE” to dismiss the pop-up.
  14. In the table, click on the most recent ACS 5-year estimate named “TOTAL POPULATION”.
  15. On the next page, click the “Download” link under “Actions”.
  16. In the pop-up, click “OK”.
  17. Wait for it to “build” your file.
  18. When it’s ready, click “DOWNLOAD”.
  19. Finally, expand the downloaded zip file (ACS_14_5YR_B01003.zip).

Personally, I prefer the Census Bureau’s developer API to retrieve data. It uses simple, readable URLs where you can substitute a year (2014), a FIPS code (06 for California), a census variable name (B01003_001E for total population), and other parameters to download the desired data from the command line without fighting a convoluted user interface. Census Reporter also provides a convenient interface to the data, albeit with some limitations such as dataset size.

The above map shows individual census tracts. For faster download and rendering, and to reduce antialiasing artifacts, you can merge tracts of the same color (population density interval) using topomerge. This is so effective at compressing the data that you can reasonably use block groups instead of tracts for greater resolution. For static maps, consider using geo2svg for even faster rendering.

For more examples of the d3-geo and TopoJSON command-line interface, see us-atlas.

<!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 color(d.properties.density); })
.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>
{
"private": true,
"license": "gpl-3.0",
"author": {
"name": "Mike Bostock",
"url": "https://bost.ocks.org/mike"
},
"scripts": {
"prepublish": "bash prepublish"
},
"devDependencies": {
"d3-geo-projection": "^1.2.0",
"ndjson-cli": "^0.2.0",
"shapefile": "^0.5.8",
"topojson": "^2.0.0",
"topojson-client": "^2.1.0",
"topojson-simplify": "^2.0.0"
}
}
#!/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 'd[0].properties = {density: Math.floor(d[1].B01003 / d[0].properties.ALAND * 2589975.2356)}, d[0]') \
| toposimplify -p 1 -f \
| topomerge -k 'd.id.slice(0, 3)' counties=tracts \
| topomerge --mesh -f 'a !== b' counties=counties \
| topoquantize 1e5 \
> topo.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment