advanced
JavaScript
Graduated circle map tutorial with Mapbox GL JS data-driven styles
Prerequisite
Familiarity with front-end development concepts. Some advanced JavaScript required.

In this tutorial, you’ll build a graduated circle map showing pedestrian foot traffic in Toronto, Canada using Mapbox GL JS and data-driven styles. The data you’ll use is from the Open Government License, City of Toronto and shows signalized intersections traffic and pedestrian volume. You’ll visualize absolute pedestrian counts at each intersection that has a signal with circles relative to the size of these counts.

Getting started

Before you get started, make sure you have to following:

Load data with Mapbox GL JS

Before uploading the pedestrian traffic data you downloaded above, open it in a text editor and inspect it to get a sense for what kinds of visualizations you can make. There are 298 rows of longitude and latitude point data along with several related columns for each point. Inspect the date, latitude, longitude, 8HrVehVol, and 8HrPedVol columns. The size of the graduated circles will be based on these data columns.

Upload the data to Mapbox Studio

Upload the pedestrian traffic data to Mapbox Studio as a tileset. For details on uploading data, see the adding custom data to Mapbox Studio guide.

When the upload is complete, find the following information from the tileset and your Mapbox account:

  • Map ID. A Map ID is a unique identifier that is created when you create a tileset. In the next step, you’ll use your tileset’s Map ID to add your data to your map.
  • source-layer name. Tilesets can include multiple source layers. You’ll need to specify the one that includes the data you uploaded in order for Mapbox GL JS to load it.

You can find the Map ID and source layer name for your pedestrian traffic data by visiting the info page that was created when you uploaded your data. It should be listed under the Tilesets tab in your Mapbox account.

Copy the code below and paste it into a text editor. Insert your own {source-layer name}, and {map id} inside the script tags. Save the modified code as index.html.

<!DOCTYPE html>
<html>

<head>
    <meta charset='utf-8'>
    <title>Pedestrian foot traffic at intersections</title>
    <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' />
    <link href='https://www.mapbox.com/base/latest/base.css' rel='stylesheet' />
    <style>
    #map {
      position: absolute;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
    }

    #locations {
      position: absolute;
    }
    </style>
</head>

<body>
    <div id='map'></div>
    <script>
    mapboxgl.accessToken = '<your access token here>'; // Put your Mapbox Public Access token here

    // Load a new map in the 'map' HTML div
    var map = new mapboxgl.Map({
      container: 'map',
      style: 'mapbox://styles/mapbox/dark-v9',
      center: [-79.3832, 43.6512],
      zoom: 11
    });

    // Load the vector tile source from the Mapbox Pedestrian traffic example
    map.on('load', function() {
      // Add the circle layer to the map
      map.addLayer({
        id: 'pedestrian_volume',
        type: 'circle',
        source: {
          type: 'vector',
          url: 'mapbox://mapbox.dvrdhs7l' // Your Mapbox tileset Map ID
        },
        'source-layer': 'Signalized_Intersection_Traff-4ypaqa' // name of tilesets
      });
    });
    </script>
</body>

</html>

Result

View demo one.

Add data-driven styles

Now that you’ve uploaded your data, you can start visualizing it on a map! In this step, you’ll plan out your visualization, then write the code to implement it.

Define the problem

Before getting started implementing your visualization, it’s important to take a step back to consider what you would like to convey to your readers. For this visualization, your readers should be able to learn:

  • Where the busiest intersections in Toronto are located.
  • The relative volume of pedestrians during an 8-hour period.

Luckily, the data you just uploaded includes total pedestrian counts at intersections for an 8-hour period.

Tell a story with your data

The circle-radius needs to progress from 2px to 10px as pedestrian volume at an intersection varies from its minimum value to its maximum value.

Circle-radius

Use an 'exponential' property function in the layer’s circle-radius property to make the size of each circle vary based on the value of the 8HrPedVol data field. With this property function, you can use two stops: specify a size for the smallest value of all points 8HrPedVol data field and another size for the largest. The exponential function will assign a circle-radius value between the two values.

Write the code

// Update the layer
map.on('load', function() {
  map.addLayer({
    id: 'pedestrian_volume',
    type: 'circle',
    source: {
      type: 'vector',
      url: 'mapbox://mapbox.dvrdhs7l' // Your Mapbox tileset Map ID
    },
    // name of tilesets
    'source-layer': 'Signalized_Intersection_Traff-4ypaqa',
    paint: {
      'circle-color': '#D49A66',
      // Add data-driven styles for circle radius
      'circle-radius': {
        property: '8HrPedVol',
        type: 'exponential',
        stops: [
          [124, 2],
          [34615, 10]
        ]
      },
      'circle-opacity': 0.8
    }
  });
});

Result

View demo two.

Add a legend

To help the user interpret the data on your map, add a legend showing the circle size and the corresponding value for quantiles.

Define quantiles

Break up your data into five equal groups. This code should go immediately before map.on('load', function(){...}):

// Store an array of quantiles
var max = 34615;
var fifth = 34615 / 5;
var quantiles = [];
for (i = 0; i < 5; i++) {
  var quantile = (fifth + i) * fifth;
  quantiles.push(quantile);
}

Create the HTML element

Create a div to contain your legend elements.

<div id='legend' class='legend'>
  <h4>Pedestrian Traffic</h4>
</div>

Create the CSS style

Next, define some CSS rules to make your legend look great:

.legend {
  background-color: #fff;
  border-radius: 3px;
  bottom: 30px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
  font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
  padding: 10px;
  position: absolute;
  right: 10px;
  z-index: 1;
}

.legend h4 {
  margin: 0 0 10px;
}

.legend p {
  margin-left: 30px;
  position: absolute;
  display: block;
  top: 0;
}

.legend div {
  position: relative;
}

.legend div span {
  border-radius: 50%;
  display: inline-block;
  margin-right: 5px;
  opacity: 0.8;
  background-color: #d49a66;
}

Build the legend

Next, use JavaScript to iterate through the quantiles array and generate the legend entries.

By default, exponential property functions have a base of 1. Exponential functions with a base of 1 are linear. You can derive the appropriate linear function from the two sets of values that you specified when adding data-driving styling to your circle layer ([124, 2] and [34615, 10]). Then, you can use that function to assign a radius to any number of pedestrians, in this case, the quantiles you defined earlier.

The relationship between the circle-radius and the number of pedestrians is radius = (rateOfChange * numberOfPedestrians) + radiusAtZero. Start by assigning the values you already know to JavaScript variables:

var minRadius = 2;
var maxRadius = 10;
var minPedestrians = 124;
var maxPedestrians = 34615;

Then, find the rateOfChange with this equation, which uses the variables you specified above:

var rateOfChange = (maxRadius - minRadius) / (maxPedestrians - minPedestrians);

Next, use the rateOfChange to solve for for radiusAtZero given the linear function:

var radiusAtZero = maxRadius - (rateOfChange * maxPedestrians);

Now that you have solved for all the unknown variables in the linear equation, you can use the equation to find the radius for any quantity of pedestrians. In this case, use the equation to find the radius for each quantile you defined earlier, and then create a legend entry for each with a circle of the appropriate size:

var legend = document.getElementById('legend');
function circleSize(quantile) {
  var radius = (rateOfChange * quantile) + radiusAtZero;
  var diameter = radius * 2;
  return diameter;
}
quantiles.forEach(function(quantile) {
  legend.insertAdjacentHTML('beforeend', '<div><span style="width:' + circleSize(quantile) + 'px;height:' + circleSize(quantile) + 'px;margin: 0 ' + [(20 - circleSize(quantile)) / 2] + 'px"></span><p>' + quantile + '</p></div>');
});

Result

Add user interactivity

Finally, add some interactivity to your map with tools for navigation (toolbar) and retrieving info from each intersection (adding popups).

Add a navigation toolbar

Add the Mapbox GL Navigation Control element.

// Add zoom and rotation controls to the map.
map.addControl(new mapboxgl.NavigationControl({ position: 'top-left' }));

Add a popup for each point

Use the Mapbox GL JS API function queryRenderedFeatures() to get the measurement date, vehicle traffic, and pedestrian traffic data from the map layer.

// When a click event occurs near a place, open a popup at the location of
// the feature, with HTML description from its properties
map.on('click', function(e) {
  var features = map.queryRenderedFeatures(e.point, { layers: ['pedestrian_volume'] });

  // if the features have no info, return nothing
  if (!features.length) {
    return;
  }

  var feature = features[0];

  // Populate the popup and set its coordinates
  // based on the feature found
  var popup = new mapboxgl.Popup()
  .setLngLat(feature.geometry.coordinates)
  .setHTML('<div id=\'popup\' class=\'popup\' style=\'z-index: 10;\'> <h5> Detail: </h5>' +
  '<ul class=\'list-group\'>' +
  '<li class=\'list-group-item\'> Date: ' + feature.properties['Count Date'] + ' </li>' +
  '<li class=\'list-group-item\'> Vehicle Count: ' + feature.properties['8HrVehVol'] + ' </li>' +
  '<li class=\'list-group-item\'> Pedestrian Count: ' + feature.properties['8HrPedVol'] + ' </li></ul></div>')
  .addTo(map);
});

// Use the same approach as above to indicate that the symbols are clickable
// by changing the cursor style to 'pointer'
map.on('mousemove', function(e) {
  var features = map.queryRenderedFeatures(e.point, { layers: ['pedestrian_volume'] });
  map.getCanvas().style.cursor = features.length ? 'pointer' : '';
});

Finally, add some CSS to style the popup HTML div.

.mapboxgl-popup {
  max-width: 400px;
  font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
}

Result

View demo four.

Next steps

Nice work! Ready to learn more about creating data-driven maps? Keep going with the guides below: