Skip to main content

Populated places - optimizing place labels

Points of Interest (POIs) must be visible and not overcrowded to allow users to quickly distinguish each point. They are likely going to be labels on a map, or points or circles with labels. This example recipe uses a global point dataset of populated places (center-points of cities) from Natural Earth Data to build a label layer.

{
"version": 1,
"layers": {
"city_labels": {
"source": "mapbox://tileset-source/{username}/populated-places",
"minzoom" : 0,
"maxzoom" : 7,
"features" : {
"attributes" : {
"allowed_output" : [
"name_en"
]
}
},
"tiles" : {
"limit" : [
[
"lowest_where_in_distance",
true,
64,
"SCALERANK"
]
]
}
}
}
}

The recipe options defined above allow you to manipulate your data in three ways:

  1. Reduce feature density. Rather than seeing all your point data at every zoom level, in this example, we use the limit and lowest_where_in_distance recipe options to first subdivide each tile at every zoom level into many equally spaced regions (64 in this case), and then only keep the highest-ranked feature in each region (defined by a numeric SCALERANK attribute attached to each feature in the data). If you have dense data, this option allows you to only keep your most important features at each zoom level, while ensuring they are spatially distributed.
  2. Visual spacing for place labels. Because we're limiting the features based on the space available in each tile, and the number of tiles increases at a quadratic rate with each subsequent zoom level, the number of features kept in your tiles naturally increases with zoom level. And because the attribute we're using to rank features (SCALERANK) indicates the importance of each place based on population, we only keep the most populated places at each zoom level, enabling "space" on the map for labels that are relevant to the scale and geographic area in view.
  3. Improve performance by removing unneeded feature attributes. The source data for Populated Places contains over 20 attributes per feature, which can add a lot of unnecessary weight to tiles if each attribute isn't needed. For this example map, the only attribute we need for each point are the name_en attribute, to display the English name of each point as a label. All other properties are removed from each feature. Excluding properties via allowed_output does not prevent them from being used in filter expressions and other steps throughout the publish job. SCALERANK is used to limit feature density, but features does not contain this data in the final tileset.

The map on the left uses a tileset that was generated without any data filtering options in its recipe. The map on the right uses a tileset generated with the sample recipe above.

Using the Tilesets CLI to generate a tileset

This section describes how to use the Tilesets CLI to generate a tileset.

  1. Download the data you'll use to create the tileset:
arrow-downDownload line-delimited GeoJSON
  1. Create a tileset source named populated-places with the data you downloaded
tilesets upload-source username populated-places ~/your/local/path/populated-places.ldgeojson
  1. Create your recipe as a local JSON file (for example populated-places-recipe.json) with your populated-places tileset source
{
"version": 1,
"layers": {
"city_labels": {
"source": "mapbox://tileset-source/{username}/populated-places",
"minzoom" : 0,
"maxzoom" : 7,
"features" : {
"attributes" : {
"allowed_output" : [
"name_en"
]
}
},
"tiles" : {
"limit" : [
[
"lowest_where_in_distance",
true,
64,
"SCALERANK"
]
]
}
}
}
}
  1. Create a tileset with the id username.populated-places-label-rank using your recipe and name it "populated places label rank". This will be an empty tileset to start, until you publish it.
tilesets create username.populated-places-label-rank --recipe ~/your/local/path/populated-places-recipe.json --name "populated places label rank"
  1. Now you're ready to publish your tileset and start processing your data
tilesets publish username.populated-places-label-rank

Preview your tileset

  1. You can now add your new tileset to a map style to visualize its data. Below is an example of how to add the username.populated-places-label-rank tileset as a vector tile source to a Mapbox GL JS map style. Below is an example of how to use GL-JS to view the tileset.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Populated places - optimizing place labels</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<link href="https://api.mapbox.com/mapbox-gl-js/v3.5.1/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v3.5.1/mapbox-gl.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
// TO MAKE THE MAP APPEAR YOU MUST
// ADD YOUR ACCESS TOKEN FROM
// https://account.mapbox.com
mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/light-v10',
zoom: 3,
center: [-71.4935, 41.5852]
});

map.on('load', () => {
map.addSource('populated-places-feature-rank', {
type: 'vector',
url: 'mapbox://examples.populated-places-label-rank'
});
map.addLayer({
'id': 'populated-places-feature-rank-id',
'type': 'circle',
'source': 'populated-places-feature-rank',
'source-layer': 'city_labels',
'paint': {
'circle-radius': 4,
'circle-color': '#ff69b4'
}
});
map.addLayer({
'id': 'populated-places-feature-rank-labels',
'type': 'symbol',
'source': 'populated-places-feature-rank',
'source-layer': 'city_labels',
'layout': {
'text-field': ['get', 'name_en'],
'text-variable-anchor': ['top', 'bottom', 'left', 'right'],
'text-radial-offset': 0.5,
'text-justify': 'auto',
'icon-image': ['concat', ['get', 'icon'], '-15']
}
});
});
</script>

</body>
</html>

Recipe options used

FieldDescriptionData type
** source**The source data to use for this layer. Tileset sources are created with the Create a tileset source endpoint of Mapbox Tiling Service (MTS).String
minzoomSpecify the minimum zoom at which the data in your tileset will be available. A value of 0 here ensures that cities will be visible at a global level.Integer
maxzoomSpecify the maximum zoom at which the data in your tileset will be tiled. Your map can be viewed past this zoom level because of overzooming, but the data at zoom levels greater than the maxzoom will be less precise. A value of 7 here allows users to zoom in and will show more cities, but we have no need for these labels at higher zoom levels in this particular case.Integer
features.attributes.allowed_outputSpecify which data attributes to carry through to the final tileset. Here we only keep the attribute name_en to keep tile sizes low (we don't need other fields for a label tileset).Array<String>
tiles.limitUse the limit filter to keep the number of cities per tile low. This property is used to keep the number of points in each tile to a minimum, reducing the density of point data. 64 features per tile ensures space for labels and other visuals. This value must be a power of 4 to match the tile grid. The accepted values are: 4, 16, 64, 256, 1024, 4096, or 16384.Array<Expression>
Was this example helpful?