Show changes over time with Mapbox GL JS

This guide will show you how to build a map that shows how cities 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

To start visualizing geospatial data, you need to create a map. 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.33.1/mapbox-gl.js'></script>
  <link href='https://api.mapbox.com/mapbox-gl-js/v0.33.1/mapbox-gl.css' rel='stylesheet' />

Next, create a map container and an info window 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>

Then apply some CSS to create the page layout. Create a pair of <style> tags at the end of your <head>, and 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 two.

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 an external GeoJSON file, collisions1601.geojson.

Instead of styling all of the data the same way, though, you can use data-driven styling to change the style of the points based on a property in the data. 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 more about data-driven styles in this 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, you can 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 drawn. 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 show collisions that happened over time.

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”. In this case, 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 to the same range, 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 you’ll connect the slider with the map. Begin by adding an event listener to the slider called onInput, which listens for any change in its value. You’ll also need the map instance setFilter(layer, filter) to update the layer dynamically. In addition, the second half of the code updates the label in the info window by converting it to the AM/PM format with a bit of math.

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:

View demo three.

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

To begin with, add this 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 after the slider event. As the Day property is categorical but not number values, you can use a different filter: ['in', key, v1, v2, ..., vn] to detect if the feature value equals to any of those listed. Likewise !in means not in any of the listed values. 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, right now 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 map onLoad. Also you’ll 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.

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.

Next article:

Make a choropleth map with Mapbox part 1: create a style with Mapbox Studio

Additional questions? Ask our support team or learn more about How Mapbox Works.