Tutorials and hands-on lessons for learning

Data Joins

Add tabular data to a vector tileset using client-side data-joins.

View fullscreen demo

When adding custom data to a map, you may find that some of your data exist in a source that is separate from your geospatial data. For example, you might have a table of population data by country but the only spatial information included is the country name. In this case you need a way to ‘join’ the data to geometries, like country boundaries, to be able to visualize them on a map. In some cases, it can be tricky to prepare joined data before adding it to a map, for example, if the data are updating often, if the data file is large, or if you can’t directly edit the geometries source that you want to use (like a Mapbox Boundaries tileset).

Using a ‘client-side data-join’ lets you combine vector tile geometries with other data dynamically when your web map loads. In this tutorial, you will learn how to use the Feature State method to join a vector tile to an external data file, and then use data-driven style notation to visualize the data.

Getting started

There are a few resources you'll need to get started:


Tools we'll use:

  • CSV to create and store your event data
  • Text Editor (Atom, VSCode, JSFiddle) to write and edit your code.‍
  • Mapbox account to access: ‍‍‍

    Mapbox Studio to create your map style.

    Mapbox GL JS to add interactivity to your map and publish it on the web‍‍.

  • Papa Parse to download and parse data from a CSV

Start your map code

Open a text editor and create a file called index.html. Set up the document by copying and pasting this template code into your new HTML file.

Add your Mapbox access token

Without an access token, the rest of the code will not work.‍

Login or create a free Mapbox account. Find your access token on your Access tokens page or the main page you sign into your Mapbox account.

Note: We recommend using the URL restriction feature on the token to avoid token misuse and want to emphasize that only public tokens should be posted to public repositories. You can find out more information about how to securely manage your access tokens here.

//YOUR TURN: Replace with your Mapbox Token
mapboxgl.accessToken =

Add a basemap style

There are several Mapbox-designed map styles that you can choose "out of the box" or you can design your own using the Mapbox Studio style editor. Let’s use Mapbox Light:

Add the Light Style to your map by replacing 'Replace with Mapbox style URL' with mapbox://styles/mapbox/light-v10.

//YOUR TURN: Replace with your Mapbox Token
mapboxgl.accessToken =
var map = new mapboxgl.Map({
  container: 'map',
  style: 'mapbox://styles/mapbox/light-v10', // YOUR TURN: choose a style: https://docs.mapbox.com/api/maps/#styles
  zoom: 4.5, // zoom level
  center: [-120.4, 36.0], // center ][lng, lat]
  transformRequest: transformRequest

Connect your spreadsheet

The code uses Papa Parse to download and parse through the CSV export. In this example, we are pulling data stored in a GitHub repository. You can find the sample data here.

To connect your CSV, replace the ‘URL’ value with your CSV export link.

//YOUR TURN: Replace with a link to your CSV
const csvUrl =

To join the CSV data to the vector tiles, you'll need a field or column in your data that uniquely identifies each row or record AND can be used to match the corresponding boundary in the vector tileset. This field is referred to as the ‘primary key’. This example uses the field ‘district’ from the CSV data to match the district boundaries field ‘GEOID’ in the vector tileset.

Next, use map.setFeatureState to:

  1. Add the tileset source: ‘ca-districts-2018’. This is the name of the vector tileset that will be joined to the tabular data.
  2. Add the tileset source layer: ‘ca-districts-2018’.This is the name of the layer within the vector tileset used for the join. In this example, ‘ca-districts-2018’ only has one layer.
  3. Promote the field from the CSV that will be used as the primary key: row.district
  4. Promote the fields from the CSV data that will be styled or interacted with: row.party, row.candidate
map.on('load', function () {
        csvPromise.then(function (results) {
          results.data.forEach((row) => {
                //YOUR TURN: Replace with your source tileset and source layer
                source: 'ca-districts-2018',
                sourceLayer: 'ca-districts-2018',
                //YOUR TURN: Replace with unqiue ID row name
                id: row.district,
              //YOUR TURN: Add rows you want to style/interact with
                party: row.party,
                candidate: row.candidate,

Add source layer

Add the vector tile source layer to your map using map.addSource(‘ca-districts-2018’). Use the promoteId property to join the vector data to the CSV data.

//YOUR TURN: Add source layer
map.addSource('ca-districts-2018', {
  type: 'vector',
  url: 'mapbox://mjdanielson.ca-districts-2018',
  promoteId: 'GEOID'

Data driven-styling with expressions

Add a new layer using map.addLayer(). The source will be ‘ca-districts-2018’, and the source-layer will be ‘ca-districts-2018’.

You can use Mapbox GL JS ‘expressions’ to set the fill color of each feature according to the values found in the feature state. (Expressions use 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:

  • match: Selects the output whose label value matches the input value, or the fallback value if no match is found.
  • feature-state: Use the feature-state expression to retrieve the value of the party property in the current feature's state and to assign a fill color to two different values of party: democrat "#6BA0C7", republican "#CE575E", and the default value is ‘#CCC”

When you put all these expressions together, your code will look like this:

//YOUR TURN: Add layers to the map
  id: 'ca-district-fill',
  type: 'fill',
  source: 'ca-districts-2018',
  'source-layer': 'ca-districts-2018',
  layout: {},
  paint: {
    'fill-color': [
      ['feature-state', 'party'],
    'fill-opacity': 0.9

Final product

You created a choropleth map using data-joins. Now you can add popups, or additional layers such as district-lines to add more context to your map.

Publish your map

You’ve made a web map! But it isn’t a webpage yet… to do that we need some way to host a webpage. There are many different ways to host a webpage. Read this guide to learn how to publish your finished map on GitHub, Netlify, or Glitch.

Was this page helpful?