Skip to content

Instantly share code, notes, and snippets.

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

A variation of my California population density map using California’s 23,198 block groups rather than its 8,043 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.

<!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>
{
"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"
}
}
#!/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 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 \
"http://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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment