Creating a Dynamic Legend

2015-09-04

Maps that show a lot of data are cool. But try to show too many things at once and things get confusing. Recently I tried to help this problem this by creating a dynamic legend - a legend that only shows a layer if it has features visible in the current map view.

For maps with just a few simple layers, this is pretty easy and something you can do on the client with a library like turf-intersect. But for this project, I had hundreds of complex layers covering most of the United States and Canada. I needed to come up with the fastest way possible to check every one of these layers for features in the current map view.

This first thing I did was create a function that looped through every layer and ran a simple query to count the number of geometries that intersected the map's bounding box. I aggregated these results up and sent it back, where the client would hide a layer if there were zero intersecting geometries. This worked, but it was agonizingly slow. Every map pan caused a 20-30 second request to fire, usually not even finishing before you wanted to pan the map again.

Three things needed to be done to make this legend faster:

  1. Reduce the number of layers that needed to be checked.
  2. Reduce the number of queries that needed to be run for layers that did need to be checked.
  3. Make the query that checks for intersection as fast as possible.

For step 1, I pre-calculated each layer's bounding box and sent that to the client on the initial map load. Now I could loop through and check if each layers' bbox intersected the map's. If it didn't intersect, I wouldn't have to go back to the server to check that layer. In many cases this reduced the number of layers to check by nearly half.

For step 2, instead of looping and running an intersects query for each layer, I unioned each query together. This eliminated all the overhead of starting and executing hundreds of queries on each map move.

Finally, I had to make the actual query as fast as possible. I realized that I didn't need to return the actual number of geometries that intersected, I just need a true or false. For some layers with 1M+ points and a large bounding box, counting was a big deal. Instead of selecting count(), I just selected "layerName" and added a limit of 1. Only the tables with intersecting geometries would be returned. The final query turned into something like this:

(select 'layerA' from schema.layerA where st_intersects(geom, st_geometryfromtext(bbox, 4326)) limit 1)
union
(select 'layerB' from schema.layerB where st_intersects(geom, st_geometryfromtext(bbox, 4326)) limit 1)
union
(select 'layerC' from schema.layerC where st_intersects(geom, st_geometryfromtext(bbox, 4326)) limit 1)

The legend now was pretty fast at responding on every map pan. The request was down to 2s instead of 20-30s previously. We now had a legend that always showed only what was needed, and a much more usable map.

I am still trying to make this legend faster. If you have any ideas or a better way to do this, let me know on twitter.