Data-joins with Mapbox Boundaries v3
Familiarity with front-end development and access to Mapbox Boundaries.
Access to the Boundaries tilesets are controlled by Mapbox account access token. If you do not have access on your account, contact a Mapbox sales representative to request access to Boundaries tilesets.
Mapbox users who have access to Mapbox Boundaries can add global administrative, postal, and statistical boundaries to their maps and data visualizations. This guide covers how to create a data-join using Mapbox Boundaries with Mapbox GL JS to style a choropleth map.
Getting started
Here are a few resources you'll need throughout this tutorial:
- Access to Mapbox Boundaries. Access to the Mapbox Boundaries tilesets is controlled by your Mapbox account access token. To request access to Mapbox Boundaries, contact Mapbox sales.
- Mapbox GL JS. The Mapbox JavaScript API for building web maps.
About data-joins
The data-join technique involves inner joins between your custom local data, such as the unemployment rate by US state, to vector tile features, such as state boundaries in the appropriate Mapbox Boundaries tileset, using data-driven style notation.
Create a data-join with Mapbox Boundaries
Below you will use Mapbox GL JS, local data, Mapbox Boundaries, feature state, and data-driven styling with expressions to join local data to a vector tile source and style a choropleth map.
Create a map with Mapbox GL JS
Begin by initializing a map with Mapbox GL JS. Make sure the access token you are using is from your account with access to Mapbox Boundaries.
Here's the starter code for this example:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Join local JSON data with Boundaries</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 ';
// Initialize a map
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/light-v11',
center: [-99.9, 41.5],
zoom: 1
});
// Local data code from the next step will go here.
</script>
</body>
</html>
Local data
In this example, you'll join unemployment data for the US to the admin-1 Boundaries tileset. Here you'll set the array of objects equal to a constant called localData
. In your own application, you can pull local data into your application how you'd like.
Here's the data used in this example as a JavaScript constant called localData
that can be directly added to the HTML file's script
tags alongside the code used to initialize the map:
const localData = [
{ STATE_ID: '01', unemployment: 13.17 },
{ STATE_ID: '02', unemployment: 9.5 },
{ STATE_ID: '04', unemployment: 12.15 },
{ STATE_ID: '05', unemployment: 8.99 },
{ STATE_ID: '06', unemployment: 11.83 },
{ STATE_ID: '08', unemployment: 7.52 },
{ STATE_ID: '09', unemployment: 6.44 },
{ STATE_ID: '10', unemployment: 5.17 },
{ STATE_ID: '12', unemployment: 9.67 },
{ STATE_ID: '13', unemployment: 10.64 },
{ STATE_ID: '15', unemployment: 12.38 },
{ STATE_ID: '16', unemployment: 10.13 },
{ STATE_ID: '17', unemployment: 9.58 },
{ STATE_ID: '18', unemployment: 10.63 },
{ STATE_ID: '19', unemployment: 8.09 },
{ STATE_ID: '20', unemployment: 5.93 },
{ STATE_ID: '21', unemployment: 9.86 },
{ STATE_ID: '22', unemployment: 9.81 },
{ STATE_ID: '23', unemployment: 7.82 },
{ STATE_ID: '24', unemployment: 8.35 },
{ STATE_ID: '25', unemployment: 9.1 },
{ STATE_ID: '26', unemployment: 10.69 },
{ STATE_ID: '27', unemployment: 11.53 },
{ STATE_ID: '28', unemployment: 9.29 },
{ STATE_ID: '29', unemployment: 9.94 },
{ STATE_ID: '30', unemployment: 9.29 },
{ STATE_ID: '31', unemployment: 5.45 },
{ STATE_ID: '32', unemployment: 4.21 },
{ STATE_ID: '33', unemployment: 4.27 },
{ STATE_ID: '34', unemployment: 4.09 },
{ STATE_ID: '35', unemployment: 7.83 },
{ STATE_ID: '36', unemployment: 8.01 },
{ STATE_ID: '37', unemployment: 9.34 },
{ STATE_ID: '38', unemployment: 11.23 },
{ STATE_ID: '39', unemployment: 7.08 },
{ STATE_ID: '40', unemployment: 11.22 },
{ STATE_ID: '41', unemployment: 6.2 },
{ STATE_ID: '42', unemployment: 9.11 },
{ STATE_ID: '44', unemployment: 10.42 },
{ STATE_ID: '45', unemployment: 8.89 },
{ STATE_ID: '46', unemployment: 11.03 },
{ STATE_ID: '47', unemployment: 7.35 },
{ STATE_ID: '48', unemployment: 8.92 },
{ STATE_ID: '49', unemployment: 7.65 },
{ STATE_ID: '50', unemployment: 8.01 },
{ STATE_ID: '51', unemployment: 7.62 },
{ STATE_ID: '53', unemployment: 7.77 },
{ STATE_ID: '54', unemployment: 8.49 },
{ STATE_ID: '55', unemployment: 9.42 },
{ STATE_ID: '56', unemployment: 7.59 }
];
Mapbox Boundaries lookup table
Each Boundaries tileset has its own feature lookup table in which each feature in that tileset is indexed. Lookup tables are designed to be used locally in your application. You can read more about feature lookup tables in the Get started with Mapbox Boundaries v3 guide.
The sample below shows how the lookup JSON is structured for the administrative level-1 table which includes the boundary metadata for the US states. Note the unit_code
property, which contains the state FIPS code that you will use to create the data-join with the sample data. Other identifiers useful for data-joins with custom data are the wikidata_id
and name
.
{
"adm1": {
"type": "admin",
"level": 1,
"PolyTilesetName": "mapbox.boundaries-adm1-v3",
"PolyLayerName": "boundaries_admin_1",
"PointTilesetName": "mapbox.boundaries-admPoints-v3",
"PointLayerName": "points_admin_1",
"data": {
"all": {
"USA101": {
"feature_id": 70377,
"wikidata_id": "Q173",
"worldview": "all",
"unit_code": "01",
"name": "Alabama",
"names": {
"en": ["Alabama", "State of Alabama", "AL", "Heart of Dixie", "The Yellowhammer State"]
},
"description": "state",
"source_date": "2018",
"iso_3166_1_alpha_3": "USA",
"iso_3166_1": "US",
"z_min": 0,
"area_sqkm": 159864
},
...
The code snippet below illustrates how to import the feature lookup table from a file hosted in the application, then make the API request when the map loads, retrieve the contents of the lookup table, and print the response in the console. You will need to replace ./path/to/adm1/lookup/table
with the path to the adm1 lookup table, available in the reference documentation that is provided with Boundaries access.
const lookupTable = async () => {
const query = await fetch('./path/to/lookup/table', { method: 'GET' });
return await query.json();
};
map.on('load', () => {
createViz(lookupTable);
});
function createViz(lookupTable) {
const lookupTableData = lookupTable.adm1.data;
console.log(lookupTableData);
}
Explore the response in the console to learn more about what is included in the lookup tables and better understand how you'll be using it in the next step.
Feature state
Feature state is a set of attributes that can be dynamically assigned to a feature on the map. The Mapbox GL JS feature state API can be used to dynamically style the features of a vector or GeoJSON source, enabling new ways to handle map interactivity, data joins, and time series animations. Using feature state requires knowing the unique feature id
property for each boundary in the vector tiles to manipulate its rendering. In the Mapbox Boundaries lookup tables, feature_id
is this unique integer-only identifier.
To join the local unemployment data to the vector tiles, you'll need a property in your local data that can be used to match the corresponding boundary in the lookup table and find its feature_id
. Inspect the lookupTableData
and compare the properties for any state with the unemployment data to see if there is a match of values.
// Alabama in unemployment data
{ STATE_ID: '01', unemployment: 13.17 }
// Alabama in lookupTableData
"USA101": {
"feature_id": 70377,
"wikidata_id": "Q173",
"worldview": "all",
"unit_code": "01",
"name": "Alabama",
"names": {
"en": ["Alabama", "State of Alabama", "AL", "Heart of Dixie", "The Yellowhammer State"]
}
We can see that STATE_ID
which is the FIPS state code is present as the unit_code
value in the lookup table. It is now possible to find the feature_id
of any boundary feature in the tiles using a STATE_ID
-> unit_code
lookup.
Boundary lookup tables can contain millions of rows as they include features for all countries. Filtering the lookup object by the country of interest and keying object by the matching property can optimize performance. The function below traverses the adm1 lookup table and creates a new lookupData
object that can be used for our join.
const lookupTable = async () => {
const query = await fetch('./path/to/lookup/table', { method: 'GET' });
return await query.json();
};
const lookupData = filterLookupTable(lookupTable);
// Filters the lookup table to features with the 'US' country code
// and keys the table using the `unit_code` property that will be used for the join
function filterLookupTable(lookupTable) {
const lookupData = {};
for (const layer of lookupTable)
for (const worldview of lookupTable[layer].data)
for (const feature of lookupTable[layer].data[worldview]) {
const featureData = lookupTable[layer].data[worldview][feature];
// Filter the lookup data for the US
if (featureData.iso_3166_1 === 'US') {
// Use `unit_code` property that has the FIPS code as the lookup key
lookupData[featureData['unit_code']] = featureData;
}
}
return lookupData;
}
Building on the createViz
function that was defined in the previous step, add the Mapbox Boundaries admin-1 tileset as a source named statesData
.
Find the complete list of Mapbox Boundaries tilesets in the reference documentation.
Then, create a new function called setStates
within createViz
to set the feature state. To get the vector tile feature id
to set the feature state, you end up with id: lookupData[item.STATE_ID].feature_id
to find the boundary in lookupData
from the lookup table with the unit_code
to STATE_ID
and then getting the value of the feature_id
property.
Finally, you'll wait until the statesData
source has been added to the map before calling your custom setState
function to set the feature state.
function createViz(lookupData) {
const dataValues = lookupData.data;
// Add Mapbox Boundaries source for state polygons.
map.addSource('statesData', {
type: 'vector',
url: 'mapbox://mapbox.boundaries-adm1-v3'
});
// Join the JSON unemployment data with the corresponding vector features where
// feature.unit_code === `STATE_ID`.
function setStates() {
for (const item of localData) {
map.setFeatureState(
{
source: 'statesData',
sourceLayer: 'boundaries_admin_1',
id: lookupData[item.STATE_ID].feature_id
},
{
unemployment: item.unemployment
}
);
}
}
// Check if `statesData` source is loaded.
function setAfterLoad(event) {
if (event.sourceID !== 'statesData' && !event.isSourceLoaded) return;
setStates();
map.off('sourcedata', setAfterLoad);
}
// If `statesData` source is loaded, call `setStates()`.
if (map.isSourceLoaded('statesData')) {
setStates();
} else {
map.on('sourcedata', setAfterLoad);
}
}
Data-driven styling with expressions
Now that the local data and the vector data in the Mapbox Boundaries tileset have been joined, you can style the features in the Boundaries tileset according to the unemployment value from your local data. Inside the createViz
function, add a new layer using map.addLayer()
. The source will be statesData
(added in the previous step), and the source-layer
will be boundaries_admin_1
.
You can use expressions to set the fill color of each feature according to the unemployment value found in the feature state. 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.
In this example, you'll use a combination of expressions to style the data as a choropleth map:
case
: Use thecase
expression to (1) check if the unemployment feature state property is not null, (2) if unemployment is not null, you'll assign the fill color according to the value of unemployment, (3) if it is null, you'll assign a fill color ofrgba(255, 255, 255, 0)
.feature-state
: Use thefeature-state
expression to retrieve the value of the unemployment property in the current feature's state.!=
: Use the!=
expression to check if the feature state unemployment property is not equal to null.interpolate
: Use theinterpolate
expression to assign a fill color to two different values of unemployment and infer a continuous, smooth set of fill colors between the stops.
When you put all these expressions together, your code will look like this:
map.addLayer(
{
id: 'states-join',
type: 'fill',
source: 'statesData',
'source-layer': 'boundaries_admin_1',
paint: {
'fill-color': [
'case',
['!=', ['feature-state', 'unemployment'], null],
[
'interpolate',
['linear'],
['feature-state', 'unemployment'],
4,
'rgba(222,235,247,1)',
14,
'rgba(49,130,189,1)'
],
'rgba(255, 255, 255, 0)'
]
}
},
'waterway-label'
);
Final product
You created a choropleth map using data-joins and Mapbox Boundaries.
Here's the full code:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Join local JSON data with Boundaries</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',
style: 'mapbox://styles/mapbox/light-v11',
center: [-99.9, 41.5],
zoom: 1
});
// Join local JSON data with vector tile geometry
// unemployment rate in 2009
// Source https://data.bls.gov/timeseries/LNS14000000
const localData = [
{ STATE_ID: '01', unemployment: 13.17 },
{ STATE_ID: '02', unemployment: 9.5 },
{ STATE_ID: '04', unemployment: 12.15 },
{ STATE_ID: '05', unemployment: 8.99 },
{ STATE_ID: '06', unemployment: 11.83 },
{ STATE_ID: '08', unemployment: 7.52 },
{ STATE_ID: '09', unemployment: 6.44 },
{ STATE_ID: '10', unemployment: 5.17 },
{ STATE_ID: '12', unemployment: 9.67 },
{ STATE_ID: '13', unemployment: 10.64 },
{ STATE_ID: '15', unemployment: 12.38 },
{ STATE_ID: '16', unemployment: 10.13 },
{ STATE_ID: '17', unemployment: 9.58 },
{ STATE_ID: '18', unemployment: 10.63 },
{ STATE_ID: '19', unemployment: 8.09 },
{ STATE_ID: '20', unemployment: 5.93 },
{ STATE_ID: '21', unemployment: 9.86 },
{ STATE_ID: '22', unemployment: 9.81 },
{ STATE_ID: '23', unemployment: 7.82 },
{ STATE_ID: '24', unemployment: 8.35 },
{ STATE_ID: '25', unemployment: 9.1 },
{ STATE_ID: '26', unemployment: 10.69 },
{ STATE_ID: '27', unemployment: 11.53 },
{ STATE_ID: '28', unemployment: 9.29 },
{ STATE_ID: '29', unemployment: 9.94 },
{ STATE_ID: '30', unemployment: 9.29 },
{ STATE_ID: '31', unemployment: 5.45 },
{ STATE_ID: '32', unemployment: 4.21 },
{ STATE_ID: '33', unemployment: 4.27 },
{ STATE_ID: '34', unemployment: 4.09 },
{ STATE_ID: '35', unemployment: 7.83 },
{ STATE_ID: '36', unemployment: 8.01 },
{ STATE_ID: '37', unemployment: 9.34 },
{ STATE_ID: '38', unemployment: 11.23 },
{ STATE_ID: '39', unemployment: 7.08 },
{ STATE_ID: '40', unemployment: 11.22 },
{ STATE_ID: '41', unemployment: 6.2 },
{ STATE_ID: '42', unemployment: 9.11 },
{ STATE_ID: '44', unemployment: 10.42 },
{ STATE_ID: '45', unemployment: 8.89 },
{ STATE_ID: '46', unemployment: 11.03 },
{ STATE_ID: '47', unemployment: 7.35 },
{ STATE_ID: '48', unemployment: 8.92 },
{ STATE_ID: '49', unemployment: 7.65 },
{ STATE_ID: '50', unemployment: 8.01 },
{ STATE_ID: '51', unemployment: 7.62 },
{ STATE_ID: '53', unemployment: 7.77 },
{ STATE_ID: '54', unemployment: 8.49 },
{ STATE_ID: '55', unemployment: 9.42 },
{ STATE_ID: '56', unemployment: 7.59 }
];
async function fetchLookupTable() {
const response = await fetch('./mapbox-boundaries-adm1-v3_4.json', {
method: 'GET'
});
return await response.json();
}
map.on('load', () => {
// Wait for the lookup table to load before proceeding with the data join
fetchLookupTable().then((lookupTable) => {
createViz(lookupTable);
});
});
function createViz(lookupTable) {
const lookupData = filterLookupTable(lookupTable);
// Filters the lookup table to features with the 'US' country code
// and keys the table using the `unit_code` property that will be used for the join
function filterLookupTable(lookupTable) {
const lookupData = {};
for (const layer in lookupTable)
for (const worldview in lookupTable[layer].data)
for (const feature in lookupTable[layer].data[worldview]) {
const featureData = lookupTable[layer].data[worldview][feature];
// Filter the lookup data for the US
if (featureData.iso_3166_1 === 'US') {
// Use `unit_code` property that has the FIPS code as the lookup key
lookupData[featureData['unit_code']] = featureData;
}
}
return lookupData;
}
// Add Mapbox Boundaries source for state polygons.
map.addSource('statesData', {
type: 'vector',
url: 'mapbox://mapbox.boundaries-adm1-v3'
});
// Add layer from the vector tile source with data-driven style
// Use a feature-state dependent expression to compute the green color band based on
// the unemployment percentage
map.addLayer(
{
id: 'states-join',
type: 'fill',
source: 'statesData',
'source-layer': 'boundaries_admin_1',
paint: {
'fill-color': [
'case',
['!=', ['feature-state', 'unemployment'], null],
[
'interpolate',
['linear'],
['feature-state', 'unemployment'],
4,
'rgba(222,235,247,1)',
14,
'rgba(49,130,189,1)'
],
'rgba(255, 255, 255, 0)'
]
}
},
'waterway-label'
);
// Join the JSON unemployment data with the corresponding vector features where
// feature.unit_code === `STATE_ID`.
function setStates() {
for (const item of localData) {
map.setFeatureState(
{
source: 'statesData',
sourceLayer: 'boundaries_admin_1',
id: lookupData[item.STATE_ID].feature_id
},
{
unemployment: item.unemployment
}
);
}
}
// Check if `statesData` source is loaded.
function setAfterLoad(event) {
if (event.sourceID !== 'statesData' && !event.isSourceLoaded) return;
setStates();
map.off('sourcedata', setAfterLoad);
}
// If `statesData` source is loaded, call `setStates()`.
if (map.isSourceLoaded('statesData')) {
setStates();
} else {
map.on('sourcedata', setAfterLoad);
}
}
</script>
</body>
</html>
Next steps
Learn more about how you can use Mapbox Boundaries.
More Mapbox Boundaries tutorials
Explore our other Mapbox Boundaries tutorials:
- Point-in-polygon query with Mapbox Boundaries: Determine what polygons exist at a single point using the Mapbox Tilequery API.
Advanced use cases
You can also explore this example, which uses the concepts outlined in both this data-join tutorial and the Point-in-polygon query tutorial to create an application that features an interactive choropleth map.