In this tutorial you'll learn how to write expressions in Mapbox GL JS to style custom data based on a data property and by zoom level.
The final product will be a map of landmark data styled with circles. The radius of each circle will be based on the age of the landmark, and the size of the circle will change based on the zoom level.
In the Mapbox Style Specification, the value for any layout
property, paint
property, or filter
may be specified as an expression. Expressions define how one or more feature property value and/or the current zoom level are combined using logical, mathematical, string, or color operations to produce the appropriate style property value or filter decision.
A property expression is any expression defined using a reference to feature property data. Property expressions allow the appearance of a feature to change with its properties. They can be used to visually differentiate types of features within the same layer or create data visualizations.
There are several ways to apply property expressions to your application, including:
Mapbox GL JS expressions uses a Lisp-like syntax, represented using JSON arrays. Expressions follow this format:
[expression_name, argument_0, argument_1, ...]
The expression_name
is the expression operator, for example, you would use '*'
to multiply two arguments or 'case'
to create conditional logic. For a complete list of all available expressions see the Mapbox Style Specification.
The arguments are either literal (numbers, strings, or boolean values) or else themselves expressions. The number of arguments varies based on the expression.
Here's one example using an expression to calculate an arithmetic expression (π * 32):
["*", ["pi"], ["^", 3, 2]]
This example uses a *
expression to multiply two arguments. The first argument is 'pi'
, which is an expression that returns the mathematical constant pi. The second argument is another expression: a '^'
expression with two arguments of its own. It will return 32, and the result will be multiplied by π.
Now that you've had an introduction to the uses and syntax for expressions, it's time to test it out yourself! Initialize a map with Mapbox GL JS and add the custom data as a circle layer.
In this guide, you'll use a vector tileset to display data in your application. You can create a vector tileset by uploading the CSV you downloaded earlier to Mapbox Studio:
In a text editor, create a new index.html
file and use the following code to initialize your map. You will need to:
mapboxgl.accessToken
is set equal to one of your access tokens.your-tileset-id-here
with the tileset ID for the tileset you created.your-source-layer-here
with the name of the source layer in your tileset.<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Get started with Mapbox GL JS expressions</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.js"></script>
<link
href="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css"
rel="stylesheet"
/>
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN ';
const map = new mapboxgl.Map({
container: 'map', // container id
style: 'mapbox://styles/mapbox/light-v11', // stylesheet location
center: [-93.261, 44.971], // starting position [lng, lat]
zoom: 10 // starting zoom
});
map.on('load', () => {
map.addLayer({
id: 'historical-places',
type: 'circle',
source: {
type: 'vector',
url: 'mapbox://your-tileset-id-here'
},
'source-layer': 'your-source-layer-here'
});
});
</script>
</body>
</html>
Run your application in a browser, and you will see a light colored map centered on Minneapolis with black dots scattered across the city.
Next, you'll write an expression to style the radius of each circle based on the age of each historic landmark. In this data file, provided by the City of Minneapolis's open data portal, the age of the historic landmark is not provided, but the year of construction is provided. You can use arithmetic to calculate the age based on the current year and style the circle-radius
paint property based on the age.
Start by calculating the age of the landmark. Here's the expression you'll use:
['-', 2017, ['number', ['get', 'Constructi'], 2017]]
Use a '-'
expression with two arguments. The first argument is a number (the current year, 2017
). The second argument is another expression (a 'number'
expression) with two arguments. This expression will convert the first argument to a number. If the first argument doesn't have a value, then the second argument, the current year 2017
, will be used.
The first argument for the 'number'
expression is yet another expression — this time a 'get'
expression that retrieves the object property value of its only argument, 'Constructi'
. ('Constructi'
is the 'Construction_Date'
from the original CSV file, but the name of the field was shortened in the upload process.) The value that is retrieved using 'get'
is turned into a 'number'
.
Ultimately, this expression results in the age of the landmark, essentially 2017 - year of construction
.
Use this expression inside the existing addLayer
function in your code:
map.on('load', () => {
map.addLayer({
id: 'historical-places',
type: 'circle',
source: {
type: 'vector',
url: 'mapbox://your-tileset-id-here'
},
'source-layer': 'your-source-layer-here',
paint: {
'circle-radius': ['-', 2017, ['number', ['get', 'Constructi'], 2017]],
'circle-opacity': 0.8,
'circle-color': 'rgb(171, 72, 33)'
}
});
});
Refresh your browser and you'll see that the circle radius has changed dramatically. With the current expression, the radius of a circle representing a landmark that was built in 1950 would be 67 pixels. Next, you'll adjust the radius to be more appropriate in this context.
Wrap the expression in one other expression to make the size of the circles look better in this context. In this case, you'll divide the age of the landmark by 10
.
map.on('load', () => {
map.addLayer({
id: 'historical-places',
type: 'circle',
source: {
type: 'vector',
url: 'mapbox://your-tileset-id-here'
},
'source-layer': 'your-source-layer-here',
paint: {
'circle-radius': [
'/',
['-', 2017, ['number', ['get', 'Constructi'], 2017]],
10
],
'circle-opacity': 0.8,
'circle-color': 'rgb(171, 72, 33)'
}
});
});
Refresh your browser and you'll see an updated map.
The circles are still overlapping at the starting zoom level, 10.5
, but they look good at higher zoom levels. You can use a zoom expression to address this issue. A zoom expression is any expression defined using ['zoom']
. Such expressions allow the appearance of a layer to change with the map’s zoom level. Zoom expressions can be used to create the illusion of depth and control data density.
Use an 'interpolate'
expression. An 'interpolate'
expression produces continuous, smooth results by interpolating between pairs of input and output values ("stops"). In this case, use ['linear']
to interpolate linearly between a pair of stops slightly less than and slightly greater than the input. Then, specify that the radius should be the age of the landmark / 30
at zoom level 10
and age of the landmark / 10
at zoom level 13
.
map.on('load', () => {
map.addLayer({
id: 'historical-places',
type: 'circle',
source: {
type: 'vector',
url: 'mapbox://your-tileset-id-here'
},
'source-layer': 'your-source-layer-here',
paint: {
'circle-radius': [
'interpolate',
['linear'],
['zoom'],
10,
['/', ['-', 2017, ['number', ['get', 'Constructi'], 2017]], 30],
13,
['/', ['-', 2017, ['number', ['get', 'Constructi'], 2017]], 10]
],
'circle-opacity': 0.8,
'circle-color': 'rgb(171, 72, 33)'
}
});
});
If you've worked with property
functions
before, notice that interpolate
expressions allow you to achieve the same
effect as stop functions.
Refresh your browser and zoom into the map to see the resulting effect.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Minneapolis Landmarks</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.js"></script>
<link
href="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css"
rel="stylesheet"
/>
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN ';
const map = new mapboxgl.Map({
container: 'map', // container id
style: 'mapbox://styles/mapbox/light-v11', // stylesheet location
center: [-93.261, 44.971], // starting position [lng, lat]
zoom: 10.5 // starting zoom
});
map.on('load', () => {
map.addLayer({
id: 'historical-places',
type: 'circle',
source: {
type: 'vector',
url: 'mapbox://your-tileset-id-here'
},
'source-layer': 'your-source-layer-here',
paint: {
'circle-radius': [
'interpolate',
['linear'],
['zoom'],
10,
['/', ['-', 2017, ['number', ['get', 'Constructi'], 2017]], 30],
13,
['/', ['-', 2017, ['number', ['get', 'Constructi'], 2017]], 10]
],
'circle-opacity': 0.8,
'circle-color': 'rgb(171, 72, 33)'
}
});
});
</script>
</body>
</html>
You've styled custom data using expressions in Mapbox GL JS!
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Demo: Get started with Mapbox GL JS expressions</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.js"></script>
<link
href="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css"
rel="stylesheet"
/>
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
mapboxgl.accessToken = '{{MAPBOX_ACCESS_TOKEN}}';
const map = new mapboxgl.Map({
container: 'map', // container id
style: 'mapbox://styles/mapbox/light-v11', // stylesheet location
center: [-93.261, 44.971], // starting position [lng, lat]
zoom: 10.5 // starting zoom
});
map.on('load', () => {
map.addLayer({
'id': 'historical-places',
'type': 'circle',
'source': {
'type': 'vector',
'url': 'mapbox://examples.8ribcg3i'
},
'source-layer': 'HPC_landmarks-a88vge',
'paint': {
'circle-radius': [
'interpolate',
['linear'],
['zoom'],
10,
['/', ['-', 2017, ['number', ['get', 'Constructi'], 2017]], 30],
13,
['/', ['-', 2017, ['number', ['get', 'Constructi'], 2017]], 10]
],
'circle-opacity': 0.8,
'circle-color': 'rgb(171, 72, 33)'
}
});
});
</script>
</body>
</html>
Congratulations! You've successfully styled custom data using expressions in Mapbox GL JS.
There are many other ways you can use expressions in Mapbox GL JS. For more information: