Handling non-contiguous topoJSON features in Leaflet

I received an interesting bug report for the Quality of Life project from somebody out in California. He didn’t know it was a bug report, and at first I didn’t either.

I had a pretty hacky way to assign the id of a polygon to a data attribute in the SVG. I even commented it with // Eyes wide open for the gnarly hack. To make a tragic story short, I was relying on Leaflet to load the topojson in order, and then using that order in a loop to assign a data-id attribute to the SVG path for D3 to fiddle about with.

Yes, I know. Stupid. But it worked, and I was not clever enough to figure out the proper way to do it.

But California was having a problem, which they assumed was them, which was also my assumption, because I always assume it’s them. Their polygons were showing up but not painting as they should. I tried loading their data and found the same thing. Something was wrong with the SVG path data-id assignment. I did a count and found there were 439 features in the topoJSON but 499 SVG paths in Leaflet. And then it dawned on me.

non-contiguous polygons

The coast. They have non-contiguous features. Bloody hell.

This didn’t turn out to be very difficult to fix, and by fixing it I not only got rid of the gnarly hack, it sped up initial map painting slightly.

First I decided to let Leaflet handle setting my SVG path classes. Leaflet can do that in the style object when loading geoJSON. Use className (because class has meaning in JavaScript) and put in your list of classes.

1
2
3
4
5
6
7
8
9
10
11
// ****************************************
// Style GeoJSON on load
// ****************************************
function jsonStyle(feature) {
return {
"fillColor": "rgba(0,0,0,0)",
"color": "none",
"fillOpacity": 1,
className: "geom metric-hover",
};
}

That cleaned things up a little and got rid of a DOM loop. The big problem was assigning a data-id attribute to the SVG path with the feature id. You have to do that after the geoJSON is loaded so there are SVG paths to add data attributes to. Leaflet’s eachLayer will let you iterate through the SVG paths, but the trick is for multi-polygon features, it returns another object containing those features. You have to check what you are getting before you try to do anything with it. Or as I like to think of it, you have to do the Ickey shuffle.

1
2
3
4
5
6
7
8
9
10
11
// add data-id attribute to SVG paths
// the if-then is to handle non-contiguous polygon features (hi coastal areas!)
d3Layer.eachLayer(function (layer) {
if (layer._path) {
layer._path.setAttribute("data-id", layer.feature.id);
} else {
layer.eachLayer(function (noncontig) {
noncontig._path.setAttribute("data-id", layer.feature.id);
});
}
});

Non-contiguous features are good to go, and in the course of doing that my code is now both cleaner and faster. I also fixed/simplified the click selection event along the way because stupid.

Even if you are not a coder, you can contribute immensely to open source projects just by reporting bugs and asking questions. Now I owe somebody in California a beer. Which means I can’t travel to California anymore.