> For the complete documentation index, see [llms.txt](https://docs.mapbox.com/help/llms.txt)

# Data-joins with Mapbox Boundaries v4

This guide covers how to create a data-join using Mapbox Boundaries with Mapbox GL JS to style a choropleth map.

Mapbox users who have access to Mapbox Boundaries can add global administrative, postal, and statistical boundaries to their maps and data visualizations.

![final choropleth map using Boundaries, local data, and expressions](https://docs.mapbox.com/help/assets/ideal-img/tutorials--boundaries-data-join-final.0d0dd0f.480.png)

## Prerequisites

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](https://www.mapbox.com/contact/sales/).
    -   This includes a Mapbox access token with special permissions to access Boundaries tilesets.
-   [**Mapbox GL JS**](https://docs.mapbox.com/mapbox-gl-js/api/). The Mapbox JavaScript API for building web maps.
-   **Familiarity with front-end development.** Beginner experience with CSS, HTML and JavaScript.

## What is a data-join?

A data-join is a technique that 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.

In this tutorial, 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.

1.  Make sure the access token you are using is from **your account with access to Mapbox Boundaries**.
2.  Add the following code into your project:

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Join local JSON data with Mapbox Boundaries v4</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <script src="https://api.mapbox.com/mapbox-gl-js/v3.25.0/mapbox-gl.js"></script>
    <link
      href="https://api.mapbox.com/mapbox-gl-js/v3.25.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>
      // Initialize a map
      const map = new mapboxgl.Map({
        accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN ',
        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

Once you've created a map, we can begin to work with the local data.

In this example, you'll join unemployment data for the US to the admin-1 Boundaries tileset:

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:

```js
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 [Mapbox Boundaries v4 Reference](https://docs.mapbox.com/data/boundaries/reference/#metadata).

The sample below shows how the Mapbox Boundaries v4 lookup CSV 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`](https://www.wikidata.org/wiki/Wikidata:Identifiers) and `name`.

```csv
iso_3166_1,layer_type,layer_level,layer_name,mapbox_id,worldview,unit_code,wikidata_id,name,name_en,name_translations,description,unit_code_description,source_date,iso_3166_1_alpha_3,grouping_attributes,join_attributes,parent_0,parent_1,parent_2,parent_3,parent_4,polygon_tileset_id,polygon_source_layer,point_tileset_id,point_source_layer,polygon_z_min,point_z_min,area_sqkm,centroid,bbox
US,admin,1,adm1,dXJuOm1ieGJuZDpBUkxwOnY0,all,01,Q173,Alabama,,"{""en"": [""AL"", ""Ala"", ""Ala."", ""Alabama"", ""Bama"", ""Heart of Dixie"", ""State of Alabama"", ""The Yellowhammer State""]}",state,"",2018,USA,"{ ""US Census Division"" : [""East South Central""], ""US Census Region"" : [""South""] }","{ ""iso_3166_2"" : ""US-AL"" }",dXJuOm1ieGJuZDo4aERwOnY0,,,,,mapbox.boundaries-adm1-v4,boundaries_admin_1,mapbox.boundaries-admPoints-v4,points_admin_1,0,0,159864,"[-86.7411,32.6483]","[-88.4732,30.1941,-84.8882,35.008]"
...
```

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 `./mapbox-boundaries-adm1-v4_1.csv` with the path to the adm1 lookup table, available in the reference documentation that is provided with Boundaries access.

```js
async function fetchLookupTable() {
  const response = await fetch('./mapbox-boundaries-adm1-v4_1.csv', {
    method: 'GET'
  });
  return await response.text();
}

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 lookupTableData = lookupTable;

  // Print the loaded CSV
  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](https://docs.mapbox.com/mapbox-gl-js/api/#map#setfeaturestate) 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](https://en.wikipedia.org/wiki/Federal_Information_Processing_Standard_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.

```js
const lookupData = buildLookupData(lookupTable);

// Build a lookup of every US state code and corresponding mapbox_id
// from the Mapbox Boundaries Lookup Tables for the data join
function buildLookupData(lookupTable) {
  const lookupData = {};
  const rows = lookupTable.split('\n');

  // Iterate through the rows of the CSV lookup table
  rows.forEach((row, i) => {
    const columns = row.split(',');

    // Filter to only the rows containing the country code as 'US'
    if (columns[rows[0].split(',').indexOf('iso_3166_1')] === 'US') {
      const mapbox_id = columns[rows[0].split(',').indexOf('mapbox_id')];
      const unit_code = columns[rows[0].split(',').indexOf('unit_code')];

      // Create a dictionary of US state codes and corresponding mapbox_id
      lookupData[unit_code] = mapbox_id;
    }
  });

  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`.

> **Note**
> 
> Find the complete list of Mapbox Boundaries v4 tilesets in the [reference documentation](https://docs.mapbox.com/data/boundaries/reference/mapbox-boundaries-v4/#tileset-ids).

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].mapbox_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 `mapbox_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.

```js
function createViz(lookupData) {
  const dataValues = lookupData.data;

  // Add Mapbox Boundaries v4 admin-1 source for US state polygons.
  map.addSource('statesData', {
    type: 'vector',
    url: 'mapbox://mapbox.boundaries-adm1-v4',
    promoteId: 'mapbox_id'
  });

  // Join the JSON unemployment data with the corresponding boundaries vector feature
  // by using the lookupData dicttionary of STATE_IDs and mapbox_id
  function setStates() {
    for (const item of localData) {
      map.setFeatureState(
        {
          source: 'statesData',
          sourceLayer: 'boundaries_admin_1',
          id: lookupData[item.STATE_ID].mapbox_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 [`'*'`](https://docs.mapbox.com/style-spec/reference/expressions/#*) to multiply two arguments or [`'case'`](https://docs.mapbox.com/style-spec/reference/expressions/#case) to create conditional logic. For a complete list of all available expressions see the [Mapbox Style Specification](https://docs.mapbox.com/style-spec/reference/expressions/).

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`](https://docs.mapbox.com/style-spec/reference/expressions/#case): Use the `case` 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 of `rgba(255, 255, 255, 0)`.
-   [`feature-state`](https://docs.mapbox.com/style-spec/reference/expressions/#feature-state): Use the `feature-state` expression to retrieve the value of the unemployment property in the current feature's state.
-   [`!=`](https://docs.mapbox.com/style-spec/reference/expressions/#!=): Use the `!=` expression to check if the feature state unemployment property is not equal to null.
-   [`interpolate`](https://docs.mapbox.com/style-spec/reference/expressions/#interpolate): Use the `interpolate` 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:

```js
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.

![final choropleth map using Boundaries, local data, and expressions](https://docs.mapbox.com/help/assets/ideal-img/tutorials--boundaries-data-join-final.0d0dd0f.480.png)

Here's the full code:

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Join local JSON data with Mapbox Boundaries v4</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <script src="https://api.mapbox.com/mapbox-gl-js/{{VERSION_MAPBOXGLJS}}/mapbox-gl.js"></script>
    <link
      href="https://api.mapbox.com/mapbox-gl-js/{{VERSION_MAPBOXGLJS}}/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>
      const map = new mapboxgl.Map({
        accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN ',
        container: 'map',
        style: 'mapbox://styles/mapbox/light-v11',
        center: [-99.9, 41.5],
        zoom: 1
      });

      // Local JSON data to be joined with the vector tile geometry
      // US unemployment rate by state 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-v4_1.csv', {
          method: 'GET'
        });
        return await response.text();
      }

      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 = buildLookupData(lookupTable);

        // Build a lookup of every US state code and corresponding mapbox_id
        // from the Mapbox Boundaries Lookup Tables for the data join
        function buildLookupData(lookupTable) {
          const lookupData = {};
          const rows = lookupTable.split('\n');

          // Iterate through the rows of the CSV lookup table
          rows.forEach((row, i) => {
            const columns = row.split(',');

            // Filter to only the rows containing the country code as 'US'
            if (columns[rows[0].split(',').indexOf('iso_3166_1')] === 'US') {
              const mapbox_id =
                columns[rows[0].split(',').indexOf('mapbox_id')];
              const unit_code =
                columns[rows[0].split(',').indexOf('unit_code')];

              // Create a dictionary of US state codes and corresponding mapbox_id
              lookupData[unit_code] = { mapbox_id: mapbox_id };
            }
          });

          return lookupData;
        }

        // Add Mapbox Boundaries source for state polygons.
        map.addSource('statesData', {
          type: 'vector',
          url: 'mapbox://mapbox.boundaries-adm1-v4',
          promoteId: 'mapbox_id'
        });

        // 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].mapbox_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.

Explore our other Mapbox Boundaries tutorials:

-   [Point-in-polygon query with Mapbox Boundaries](https://docs.mapbox.com/help/tutorials/point-in-polygon-query-with-mapbox-boundaries/): Determine what polygons exist at a single point using the Mapbox Tilequery API.

You can also explore this [example](https://demos.mapbox.com/boundaries-explorer/), which uses the concepts outlined in both this data-join tutorial and the [Point-in-polygon query](https://docs.mapbox.com/help/tutorials/point-in-polygon-query-with-mapbox-boundaries/) tutorial to create an application that features an interactive choropleth map.

Your browser doesn't support HTML5 video. Open [link to the video](https://docs.mapbox.com/help/help/assets/medias/tutorials--data-joins-with-mapbox-boundaries--choropleth-demo-75969d4a96c17ba855302dbbddb47ad0.mp4).