advanced
JavaScript
Show changes over time with Mapbox GL JS
Prerequisite
Familiarity with front-end development concepts. Some advanced JavaScript required.

This tutorial will demonstrate how to build a map that shows data change over time using Mapbox GL JS. The source data you’ll be working with in this guide is from from NYC OpenData and contains more than 15,000 motor vehicle collisions in New York City that occurred in January 2016.

This guide takes a deep dive into JavaScript with Mapbox GL JS to build an interactive web map. If you’re new to Mapbox GL JS you might want to check out Mapbox GL JS fundamentals first.

Get started

There are a few resources you’ll need before getting started:

  • An access token from your Mapbox account. The access token is used to associate a map with your account and can be found on your Account page.
  • Data. This GeoJSON file contains 15,273 geocoded motor vehicle collision incidents from January 2016, pulled from NYC OpenData. Download NYC collisions data
  • A text editor for writing HTML, CSS, and JavaScript.

Add a map

Before you start adding your NYC collision data, you need to create a map to put it on. Start by creating an HTML file and then initialize the map object on the page. This guide will require several files, so we recommend creating a project folder on your computer to keep them together.

Create an HTML file

In your project folder, create an index.html file. Set up the document by adding Mapbox GL JS and CSS to your <head>.

  <meta charset='utf-8' />
  <title>NYC motor vehicle collisions</title>
  <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
  <script src='https://api.mapbox.com/mapbox-gl-js/v0.44.1/mapbox-gl.js'></script>
  <link href='https://api.mapbox.com/mapbox-gl-js/v0.44.1/mapbox-gl.css' rel='stylesheet' />

Next, create a map container and a container for your legend and data interactivity elements in <body>.

<div id='map'></div>
<div id='console'>
  <h1>Motor vehicle collisions</h1>
  <p>Data: <a href='https://data.cityofnewyork.us/Public-Safety/NYPD-Motor-Vehicle-Collisions/h9gi-nx95'>Motor vehicle collision injuries and deaths</a> in NYC, Jan 2016</p>
</div>

Apply some CSS to create the page layout. Create a pair of <style> tags at the end of your <head>, then add:

body {
  margin: 0;
  padding: 0;
  font-family: 'Helvetica Neue', Helvetica, Arial, Sans-serif;
}

#map {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
}

h1 {
  font-size: 20px;
  line-height: 30px;
}

h2 {
  font-size: 14px;
  line-height: 20px;
  margin-bottom: 10px;
}

a {
  text-decoration: none;
  color: #2dc4b2;
}

#console {
  position: absolute;
  width: 240px;
  margin: 10px;
  padding: 10px 20px;
  background-color: white;
}

Initialize the map

Once you have the basic structure done, you can initialize the map with Mapbox GL JS. Insert a pair of <script> tags at the end of <body> — you will write all of the following code (JavaScript) in between these tags.

Start by creating a new map object using new mapboxgl.Map() and store it in a variable called map.

mapboxgl.accessToken = '<your access token here>';

var map = new mapboxgl.Map({
  container: 'map', // container element id
  style: 'mapbox://styles/mapbox/light-v9',
  center: [-74.0059, 40.7128], // initial map center in [lon, lat]
  zoom: 12
});

If you load the page, it should look like this:

View demo one.

Load your data

Once you get the page structure and map done and onto the page, it’s time to load in your data, get it styled, and add a legend. Fortunately, Mapbox GL JS has some handy functions that can help! First, make sure that the collisions1601.geojson file you downloaded is in your project folder.

Add a layer

In order to add data to your map using Mapbox GL JS, you will need to add a layer that includes a source. In this example, you will use the GeoJSON file you downloaded above as your source.

In this example, you’ll use expressions to set the style of the points based on a property in the data. This kind of data-driven styling allows you to add an extra dimension to you map visualization, helping convey a particular insight or message to your readers. In this case, you can make the map more informative by applying styles based on the Casualty property in the data, which is the total number of people injured or killed in the collision. The code below changes both the size and color of the points based on the Casualty field.

Both of the expressions used below are 'interpolate' expressions. This kind of expression interpolates continuously between pairs of input and output values. In this instance, ‘linear’ interpolation can be used for circle-radius and circle-color to ensure that interpolation occurs smoothly and linearly between the pairs of stops.

Also, note the map.on('load', function(){}) code below — this tells your browser to wait until the map is finished loading before trying to add new things to it. Any code that adds layers should be inside of this callback function.

  map.on('load', function() {
    map.addLayer({
      id: 'collisions',
      type: 'circle',
      source: {
        type: 'geojson',
        data: './collisions1601.geojson' // replace this with the url of your own geojson
      },
      paint: {
        'circle-radius': [
          'interpolate',
          ['linear'],
          ['number', ['get', 'Casualty']],
          0, 4,
          5, 24
        ],
        'circle-color': [
          'interpolate',
          ['linear'],
          ['number', ['get', 'Casualty']],
          0, '#2DC4B2',
          1, '#3BB3C3',
          2, '#669EC4',
          3, '#8B88B6',
          4, '#A2719B',
          5, '#AA5E79'
        ],
        'circle-opacity': 0.8
      }
    }, 'admin-2-boundaries-dispute');
  });

Create a legend

To describe the data you just added, create a legend to communicate what each color and size means. Begin by creating a new div with class session inside the console div you added earlier.

<div class='session'>
  <h2>Casualty</h2>
  <div class='row colors'>
  </div>
  <div class='row labels'>
    <div class='label'>0</div>
    <div class='label'>1</div>
    <div class='label'>2</div>
    <div class='label'>3</div>
    <div class='label'>4</div>
    <div class='label'>5+</div>
  </div>
</div>

Next, apply some CSS to define how the new session class should be styled. Create a color gradient using the same colors as the stops of the circle-color values you defined in your layer. This should go inside the style tags you added earlier.

.session {
  margin-bottom: 20px;
}

.row {
  height: 12px;
  width: 100%;
}

.colors {
  background: linear-gradient(to right, #2dc4b2, #3bb3c3, #669ec4, #8b88b6, #a2719b, #aa5e79);
  margin-bottom: 5px;
}

.label {
  width: 15%;
  display: inline-block;
  text-align: center;
}

After adding the CSS and refreshing your browser, you should see the following:

View demo two.

Add a time slider

With the data symbolized by casualty, your map is already telling a compelling story. To make it more compelling, you can add a time slider to allow your readers to filter the collision data by time of day.

Add a map filter

In addition to providing the value of a paint or layout property, expressions can also be used as filters. By adding a == expression as a filter with the structure "filter": ['==', ['type',['get', 'key']],'value'], you can single out all features where the ‘key’ is equal to the “value” of the specified type. Add the following code to the end of the addLayer() options you wrote earlier:

filter: ['==', ['number', ['get', 'Hour']], 12]

Refresh the map. You should see a lot fewer collisions!

Create a slider bar

You don’t just want to show collisions that happened at noon, though — you want the user to be able to control what hour of the day the map shows. To do this, you can add a time slider in the console div by adding a new session div after legend. There are 24 hours in a day labeled by the integers 0-23 in the dataset, so you’ll set the input’s min and max attributes to 0 and 23, respectively, and the initial value to 12, or 12PM. Add the following code below the other session div you added in a previous step:

<div class='session' id='sliderbar'>
  <h2>Hour: <label id='active-hour'>12PM</label></h2>
  <input id='slider' class='row' type='range' min='0' max='23' step='1' value='12' />
</div>

Add interactivity

Now, add some code to connect the slider with the map. Begin by adding an event listener to the slider called onInput, which listens for any change in the slider’s value. Next, use Mapbox GL JS’s setFilter(layer, filter) method to change your layer’s filter property whenever the input event fires. Finally, add a bit of math to add PM or AM to the time displayed next to your slider.

Add this right after addLayer() in your script:

document.getElementById('slider').addEventListener('input', function(e) {
  var hour = parseInt(e.target.value);
  // update the map
  map.setFilter('collisions', ['==', ['number', ['get', 'Hour']], hour]);

  // converting 0-23 hour to AMPM format
  var ampm = hour >= 12 ? 'PM' : 'AM';
  var hour12 = hour % 12 ? hour % 12 : 12;

  // update text in the UI
  document.getElementById('active-hour').innerText = hour12 + ampm;
});

Here’s what it will look like:

Filter by day of the week

Another useful metric for gathering insights about collisions in NYC is the day of the week when accidents occur. With the following code, you can add radio buttons that filter on days of the week.

Create a radio button group

First, add the following radio button group in your HTML inside the console div.

<div class='session'>
  <h2>Day of the week</h2>
  <div class='row' id='filters'>
    <input id='all' type='radio' name='toggle' value='all' checked='checked'>
    <label for='all'>All</label>
    <input id='weekday' type='radio' name='toggle' value='weekday'>
    <label for='weekday'>Weekday</label>
    <input id='weekend' type='radio' name='toggle' value='weekend'>
    <label for='weekend'>Weekend</label>
  </div>
</div>

Add interactivity

Just like the slider, create a new change event listener in your code, then place it after the slider event. To show collisions on all days, use a != expression that filters for the ‘placeholder’ value. This filter is added so that filterDay never evaluates to null. It will become clearer why this is necessary by the end of the tutorial.

To filter for weekday and weekend values, use a match expression. Match expressions allow you to specify input and output values. The pattern that the expression below follows is:

['match', ['get', property], inputValue, outputValue if match, outputValue if not a match]

Add the following code after the slider event you added in the previous step:

document.getElementById('filters').addEventListener('change', function(e) {
  var day = e.target.value;
  // update the map filter
  if (day === 'all') {
    filterDay = ['!=', ['string', ['get', 'Day']], 'placeholder'];
  } else if (day === 'weekday') {
    filterDay = ['match', ['get', 'Day'], ['Sat', 'Sun'], false, true];
  } else if (day === 'weekend') {
    filterDay = ['match', ['get', 'Day'], ['Sat', 'Sun'], true, true];
  } else {
    console.log('error');
  }
  map.setFilter('collisions', ['all', filterDay]);
});

Refresh your page and try the week of the day buttons.

View demo four.

Combine the filters

As you may have discovered, toggling between weekday and weekend will automatically override the hour filter. You can fix this by using a combining filter (documentation).

Combining filters

To combine multiple filters, you will append them to a new 'all' expression. Using this 'all' expression ensures that both filters result in true.

Add two variables at the beginning of the map load event handler to store the ‘hour’ and ‘day of the week’ filters and apply them independently. This way when you want to update one part of the filter, you can update that variable, then apply setFilter(my layer, ['all', filterHour, filterDay]) to reset the filter.

First, add two variables at the beginning of the map load event handler you created earlier. This is where the placeholder filter becomes necessary since you cannot have null in a combining filter.

var filterHour = ['==', ['number', ['get', 'Hour']], 12];
var filterDay = ['!=', ['string', ['get', 'Day']], 'placeholder'];

Then, in addLayer, replace the value of filter with: ['all', filterHour, filterDay].

Next, in the slider onInput event, replace map.setFilter('collisions', ['==', ['number', ['get', 'Hour']], hour]); with this code:

filterHour = ['==', ['number', ['get', 'Hour']], hour];
map.setFilter('collisions', ['all', filterHour, filterDay]);

Lastly, polish the week of the day onChange event by replacing it with this code:

document.getElementById('filters').addEventListener('change', function(e) {
  var day = e.target.value;
  if (day === 'all') {
    // `null` would not work for combining filters
    filterDay = ['!=', ['string', ['get', 'Day']], 'placeholder'];
  }
  /* the rest of the if statement */
  map.setFilter('collisions', ['all', filterHour, filterDay]);
});

Finished product

Congratulations! You successfully created a map showing NYC collision data from January 2016, complete with data-driven styles, a legend, a time slider, and a day of the week filter. Great job!

View demo five.

Next steps

With this guide, you should now be set with some tools to create your own interactive time series data visualizations with Mapbox GL JS. Check out more Mapbox GL JS resources on our Help page and see the full Mapbox GL JS documentation and examples for more inspiration and guidance.