Published
Edited
Jul 25, 2020
Insert cell
Insert cell
Insert cell
Insert cell
// we use the color-legend from https://observablehq.com/@d3/color-legend to render our scale
legend({color: populationColor, width: width - 20, tickFormat: ",d", ticks: 7})
Insert cell
Insert cell
Insert cell
Insert cell
us = d3.json("https://unpkg.com/us-atlas@3/counties-10m.json")
Insert cell
Insert cell
countyShapes = topojson.feature(us, us.objects.counties)//.features
Insert cell
Insert cell
populationData = d3.json("https://api.census.gov/data/2018/pep/population?get=POP&for=county:*")
Insert cell
populationByCounty = {
// first we get rid of the first row (the header)
let rows = populationData.slice(1)
// each row is currently [Population, State code, County code ]
// we want to transform each row into [ FIPS Code, Population ]
.map(row => [String(row[1]) + String(row[2]), Number(row[0])])
// finally we return a JavaScript Map which allows us to easily lookup the population given a FIPS code
return new Map(rows)
}
Insert cell
// The smallest and biggest populations give us the "domain" for our color scale
populationExtent = d3.extent(populationData.slice(1), d=> +d[0])
Insert cell
// We use a log color scale
populationColor = d3.scaleSequentialLog(populationExtent, d3.interpolateGreens)
Insert cell
// This is how we format the numbers to use commas.
populationFormat = d3.format(",d")
Insert cell
md`### Binding to shapes
Sometimes it is useful to pre-bind the data directly to the shapefiles for use later. This might be geographic information, a name, or additional datasets.
`
Insert cell
countyShapesWithData = countyShapes.features.map((c, i) => {
console.log({ c, i });
return {
...c,
properties: {
name: c.properties.name,
radius: 10,
population: populationByCounty.get(c.id),
centroid: projection(turf.centroid(c.geometry).geometry.coordinates)
}
};
})
Insert cell
Insert cell
statesByFips = new Map(us.objects.states.geometries.map(d => [d.id, d.properties]))
Insert cell
// The borders of the states are merged so we can render a single line where they would otherwise overlap
statesMesh = topojson.mesh(us, us.objects.states, (a, b) => a !== b)
Insert cell
Insert cell
projection = d3.geoAlbersUsa()
.fitSize([width, height], countyShapes)
// The default projection parameters used by most other county map examples instead of fitSize:
// .scale(1300)
// .translate([975/2, 610/2])
Insert cell
md`## d3.simulation`
Insert cell
nodePadding = 1
Insert cell
simulatedCounties = applySimulation(countyShapes.features)
Insert cell
applySimulation = nodes => {
const simulation = d3
.forceSimulation(nodes)
.force(
"cx",
d3
.forceX()
.x(d => width / 2)
.strength(0.02)
)
.force(
"cy",
d3
.forceY()
.y(d => (width * (5 / 8)) / 2)
.strength(0.02)
)
// .force("x", d3.forceX().x(d => d.properties.centroid ? d.properties.centroid[0] : 0).strength(0.3))
// .force("y", d3.forceY().y(d => d.properties.centroid ? d.properties.centroid[1] : 0).strength(0.3))
.force("charge", d3.forceManyBody().strength(-1))
// .force("collide", d3.forceCollide().radius(d => d.properties.radius + nodePadding).strength(1))
.stop();

let i = 0;
while (simulation.alpha() > 0.01 && i < 20) {
simulation.tick();
i++;
console.log(`${Math.round((100 * i) / 200)}%`);
}

return simulation.nodes();
}
Insert cell
simpleSVGForceMap = {
// we create an SVG with the width and height specified
const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height);

svg
.append("g")
.selectAll("path")
.data(countyShapes.features)
.join("path")
// This line renders our population data
.attr("fill", d => populationColor(populationByCounty.get(d.id)))
.attr("d", path)
.append("title")
.text(
d => `${d.properties.name}, ${statesByFips.get(d.id.slice(0, 2)).name}
${populationFormat(populationByCounty.get(d.id))}`
);

// this part renders the state borders over top of our counties
svg
.append("path")
.datum(statesMesh)
.attr("fill", "none")
.attr("stroke", "white")
.attr("stroke-linejoin", "round")
.attr("d", path);

// we need to return a DOM element.
// the .node() function returns the DOM element corresponding to the d3 selection.
return svg.node();
}
Insert cell
path = d3.geoPath(projection)
Insert cell
// width = ... by default the width is set by Observable, and will update if this is embedded
height = width * 0.6256410256410256 // the aspect ratio used in the projection, same as 610/975
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
turf = require("@turf/turf@5")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more