Getting states borders from counties using TopoJSON's merge function

September 27, 2015 |

I use the immortal Mike Bostock's TopoJSON for every map I make at TIME. Whenever we're looking at geographies more granular than states—usually counties, congressional districts or PUMAs—I like to have state boundaries drawn over the map to help orient the viewer.

To do this without loading separate set of coordinates for the state boundaries (which rarely align perfectly at high zoom levels), I used to use TopoJSON's .mesh function to impute the boundaries, which boths saves on overhead and perfectly aligns at any scale. But this doesn't include international boundaries, which doesn't always look great:

svg_meshed.append("path")
    .datum(topojson.mesh(
        topology,
        topology.objects.counties,
        function(a, b) {
            return a.id - a.id % 1000 !== b.id - b.id % 1000;
        }
    ))
    .attr("class", "state")
    .attr("d", path);

It's not very difficult to add international boundaries as well, but at this point the amount of code is accumulating. More importantly, the states boundaries themselves exist as one very long <path>, not discrete elements that one can manipulate with mouseovers and so forth.

I recently came across Mike's example of merging geographies, and decided it might be a better way to make states out of counties. Here's what that looks like:

// let's get an organic list of fips
let fips = {};
counties.forEach(function(county) {
    fips[county.id - county.id % 1000] = 1;
});

let states = [];
// and merge by fips
Object.keys(fips).forEach(function(fip) {
    let state = topojson.merge(
        topology,
        topology.objects.counties.geometries.filter(function(d) {
            return d.id - d.id % 1000 == fip;
        })
    );
    states.push(state);
});

svg_merged.append("g")
    .attr("id", "states")
    .selectAll(".state")
    .append("path")
    .data(states)
    .enter()
    .append("path")
    .attr("class", "state")
    .attr("d", function(d) {
        return path(d);
    });

Better, right? Now you have an object for every state plus international borders. Here's the full Observable.