A few days ago I showed you how I made a US State choropleth. That's a nice map, but sometimes showing the shapes and areas of states can make it difficult to interpret the information you're trying to present.
In those cases, one common and easy technique you can use is the grid choropleth, where you assign the places you're mapping to a grid, and map them each with equal size.
A technique I'd like to get to, but won't cover in this post, is the grid cartogram¹, where you use shapes on the map with area proportional to the population contained within.
states = {
AK: { name: "Alaska", key: "AK" },
ME: { name: "Maine", key: "ME" },
VT: { name: "Vermont", key: "VT" },
NH: { name: "New Hampshire", key: "NH" },
MA: { name: "Massachusetts", key: "MA" },
// ...etc
And then declare how we want them to be shown in a grid:
grid = [
["AK", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "ME"],
[" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "VT", "NH"],
[" ", "WA", "ID", "MT", "ND", "MN", "IL", "WI", "MI", "CT", "MA", "RI"],
[" ", "OR", "NV", "WY", "SD", "IA", "IN", "OH", "PA", "NY", "NJ", " "],
[" ", "CA", "UT", "CO", "NE", "MO", "KY", "WV", "VA", "MD", "DE", " "],
[" ", " ", "AZ", "NM", "KS", "AR", "TN", "NC", "SC", "DC", " ", " "],
[" ", " ", " ", " ", "OK", "LA", "MS", "AL", "GA", " ", " ", " "],
["HI", " ", " ", " ", "TX", " ", " ", " ", " ", "FL", " ", " "],
];
There's no such thing as a perfect grid (is Connecticut on the great lakes?), but this will get the job done. We're just trying to communicate relative values here, not to give a perfect geographical picture.
Then we need a little function to join the grid to the states object, so our program knows where to put each state's square:
function match(grid, states) {
for (row = 0; row < grid.length; row++) {
for (col = 0; col < grid[0].length; col++) {
if (grid[row][col] !== " ") {
states[grid[row][col]].y = row;
states[grid[row][col]].x = col;
}
}
}
}
match(grid, states);
In previous maps, our mapping function converted geographic data into SVG paths. This time our job is a little easier, since all we have to do now is draw a square for each state, fill it in appropriately, and add a label. The full function, with annotations, follows:
function map(populationData) {
const width = 975,
height = 610,
// the amount of padding between the squares
padding = 2,
// I added an explicit range. Since we no longer have borders like we did
// before, if we allow the least populous state to go down to zero color,
// it becomes invisible, so make sure every state gets at least 0.3 color
scale = scaleLog()
.domain(extent(Object.values(populationData)))
.range([0.3, 1]),
// Let's make it blue instead of grey, it's more fun
colorScale = (d) => interpolateBlues(scale(d)),
// Calculate the size of the squares that best fills in the map
cols = grid[0].length,
rows = grid.length,
squareSize = min([
(width - cols * padding * 2) / cols,
(height - rows * padding * 2) / rows,
]);
// create our svg, just as before
const svg = select("#map")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "width: 100%; height: auto; height: intrinsic;");
// Create an SVG group (the "g" element:
// https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g )
// for each state; we'll append a square and a text label to each
const statesg = svg
.append("g")
.selectAll("g")
.data(Object.values(states))
.join("g");
// Add the state's square to the SVG
statesg
.append("rect")
.attr("x", (d) => d.x * (squareSize + padding * 2))
.attr("y", (d) => d.y * (squareSize + padding * 2))
.attr("width", squareSize)
.attr("height", squareSize)
.attr("state", (d) => d.name)
.attr("fill", (d) => colorScale(populationData[d.name]));
// Add a text label for each state
statesg
.append("text")
.attr("x", (d) => d.x * (squareSize + padding * 2) + squareSize / 2)
.attr("y", (d) => d.y * (squareSize + padding * 2) + squareSize / 2)
.attr("fill", "white")
.style("text-anchor", "middle")
.attr("dominant-baseline", "central")
.text((d) => d.key);
}
The full code for this example is available here.
¹: The terminology used on the web seems to be all over the place, so I'm not certain I'm using this definition right. I'd like to explore this further in future posts, but I'm calling it good enough for now