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:

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.38.0/mapbox-gl.js'></script>
  <link href='https://api.mapbox.com/mapbox-gl-js/v0.38.0/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 data-driven styling to set the style of the points based on a property in the data. 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 data-driven 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. You can learn in our data-driven styles guide.

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': {
        property: 'Casualty',
        stops: [
          [0, 3],
          [5, 15]
        ]
      },
      'circle-color': {
        property: 'Casualty',
        stops: [
          [0, '#2DC4B2'],
          [1, '#3BB3C3'],
          [2, '#669EC4'],
          [3, '#8B88B6'],
          [4, '#A2719B'],
          [5, '#AA5E79']
        ]
      },
      'circle-opacity': 0.8
    }
  }, 'admin-2-boundaries-dispute'); // place the layer beneath this layer in the basemap
});

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

By adding a filter with the structure ['==', 'key', value], you can single out all features where the “key” is equal to the “value”. Add the following code to the end of the addLayer() options you wrote earlier:

filter: ['==', '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) {
  // get the current hour as an integer
  var hour = parseInt(e.target.value);
  // map.setFilter(layer-name, filter)
  map.setFilter('collisions', ['==', '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 onChange event in your code, then place it after the slider event. The Day property is categorical rather than numerical, so you can use a different filter: ['in', key, v1, v2, ..., vn] to detect if the feature value equals to any of those listed. The !in filter (with an ! character) returns values that do not meet your filter’s criteria. 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;
  var filterDay;
  if (day === 'all') {
    filterDay = null;
  } else if (day === 'weekday') {
    filterDay = ['!in', 'Day', 'Sat', 'Sun'];
  } else if (day === 'weekend') {
    filterDay = ['in', 'Day', 'Sat', 'Sun'];
  } else {
    console.log('error');
  }
  map.setFilter('collisions', 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 filter array starting with one of the three logical operators: all, any, or none.

To make this even more streamlined, you can add two variables at the beginning of map onLoad 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 onLoad event listener you created earlier. You’ll also need to use an alternative filter for ‘all days’ here since you cannot have null in a combining filter.

var filterHour = ['==', 'Hour', 12];
var filterDay = ['!=', 'Day', 'Bob'];

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

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

filterHour = ['==', '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 = ['!=', 'Day', 'Bob'];
  }
  /* the rest of the if statement */
  map.setFilter('collisions', 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.