Dynamic Label Placement with Mapbox GL JS + Turf + PolyLabel

Yi Xu
4 min readApr 6, 2018

--

Mapbox Studio has an amazing feature. When you upload polygon features to the Mapbox Studio, you change the data type to line, circle, and even symbol.

You can select “symbol” even if your data source is polygon

This feature can facilitate the label placement for polygon features. You don’t need to turn your polygon features to point features and use the converted point features to place labels.

However, there is a problem about it. It is duplicate labels.

For example, I uploaded the data for all neighborhoods in New York City to Mapbox Studio and changed the type to “symbol”.

NYC neighborhoods

Zoom into Greenpoint. You can see there are duplicate labels in the same neighborhood.

duplicate lables

How does that happen? It is a result of tile boundaries.

Let’s turn on the tile boundaries in the debug mode.

The red lines are tile boundaries

You can see that the polygon of Greenpoint is divided to 5 parts by the tile boundaries. In each tile, Mapbox Studio finds the centroid of a part of Greenpoint and places the label. The advantage of this method is that you can always see the labels however you move the map, as long as the polygon is within the map view. This is dynamic label placement. It is quite useful because it always tells you where you are. You don’t lose any information when you move the map.

The dynamic labels are cool but too many duplicate labels are annoying. How do we fix that? We can use Mapbox GL JS, Turf.js and an opensource JS library called polylabel from Mapbox to create our own dynamic labeling application.

Step 1: Get the polygons sliced by tile boundaries

Before creating dynamic labels, you need to know what is within the map view. Use map.queryRenderedFeatures to get the features in the map view.

map.queryRenderedFeatures({
layers: [polygon-layer]
});

The returned results of that function are polygons. But they are not polygons within the map view. Let’s draw these polygons to see why.

You can see in the image below that the returned results are actually polygons in the tiles within the map view not technically the polygons within the map view. This is because Mapbox GL JS renders data based on tiles. You can imagine tiles as books and data as the content in books. When you query rendered data from the map view, Mapbox GL JS will pick the books that are in the map view for you. You need to figure out which contents are out of the map view.

Red line: tile boundaries, Green line: map view boundary, Blue line: results of map.queryRenderedFeatures

We can use Turf.intersect to do get the polygons in the map view.

var intersection = turf.intersect(mapViewBound, polygon);

Step 2: Choose a visual center for each sliced polygon

The intersection you got from Turf.intersect is a polygon feature. The next step is getting a visual center of the polygon. I recommend using polylabel.js. For information about this library, check this amazing article out. You can also use Turf.centroid or Turf.centerOfMass.

After completing this step, you get the result pretty similar to the one you get from Mapbox Studio which is a bunch of duplicate labels. You need one more step to get the best result.

Step 3: Get a center of visual centers

You may get several visual centers from the last step because of, as is demonstrated, tile boundaries. You can use Turf.center to do the job. You can also try other algorithms to get a better result. The goal of this step is removing duplicates. Another alternative is using Turf.area to keep the label which is in the largest polygon. To do this, unlike Turf.center, the label you get will always in the polygon.

Step 4: Use event listener to monitor the movement of the map

This will make your map truly dynamic. Mapbox GL JS provides you with a bunch of event listeners. The one we are going to use is moveend.

map.on('moveend', function() {
your dynamic labeling function
});

Your dynamic labeling function will be invoked just after the map completes a transition from one view to another (including pan and zoom).

Final product:

Demo:

You will need to move the map to see the effect. I also add a fixed label layer to make the result look better.

The source code of this project is here:

Happy Mapping!

--

--