Graduated circle map tutorial with Mapbox GL JS data-driven styles

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

Tutorial goals

  1. Create a map
  2. Style data using Mapbox GL JS data-driven styles
  3. Create a color legend
  4. Add user click interactivity

Steps

Make a graduated circle map in four steps:

  1. Load data with Mapbox GL JS
  2. Add data-driven styles
  3. Add a color legend
  4. Add user interactivity

Step 1 : Load data with Mapbox GL JS

To get started, download the pedestrian traffic data from the Open Government License, City of Toronto and open it using any text editor.

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 guide on adding custom data to Mapbox Studio.

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

  • Your map ID and point layer name. A Map ID identifies the tileset (data) you have created on Mapbox. The layer name in the tileset identifies the features within the tileset that can be added to a map.

  • Your access token. The access token is used to associate a map with your account and track map views or monthly active users.

In the code below, insert your {mapbox-public-token}, {your point layer name}, and {your map id} inside the script tags. Cut and paste this code into a new text file, and save it as filename index.html.

Initialize the map

<!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.33.1/mapbox-gl.js'></script>
    <link href='https://api.mapbox.com/mapbox-gl-js/v0.33.1/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 = '{mapbox-public-token}'; //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 () {
         map.addLayer({
            'id': 'pedestrian_volume',
            'type': 'circle',
            //Load the vector tile source from our Mapbox Pedestrian traffic example
            'source': {
                type: 'vector',
                url: 'mapbox://mapbox.dvrdhs7l' //Your Mapbox tileset Map ID
            },
            'source-layer': 'Signalized_Intersection_Traff-4ypaqa', //name of tileset
            'paint' : {
                'circle-color' : 'red',
                'circle-radius' : 4,
                'circle-opacity' : 0.8
              }
        });
    });
        </script>
</body>

</html>

Result

View demo one.

Step 2 : Add data-driven styles

Next, make the color and radius circles on the map change based on the number of pedestrians at an intersection using data-driven styles.

Define the problem

  1. What are the available attributes to display?
    • Total pedestrian counts at point intersections for an 8-hour period
  2. What will be communicated?
    • Where the busiest intersections in Toronto are located
    • The relative volume of pedestrians during the 8-hour period

Tell a story

The circle-color needs to ramp from a light color (#D49A66) to a darker color (#44505A) as pedestrian volume at an intersection varies from min to max. To determine appropriate numerical stops to use in the data-driven style, use the jenks optimization method to calculate optimal bin sizes. Loading in the pedestrian volume data for Toronto, the jenks method yields [1614, 4132, 8056, 13919, 20464, 34615] as bin stops for 8HrPedVol.

The 8HrPedVol data is continuous, and it should define exact color stops to generate a legend. To do this, choose the interval property function in the circle-color data-driven style. The interval property-function defines a stops range parameter ([start, stop]) for each color in the data. For example, if 8HrPedVol is between 0 and 1614, the color will be #D49A66.

Finally choose a color gradient to use with the data-driven style. To choose a color gradient, check out Tristen’s color picker website.

Write the code

// Store an array of circle-color ramps for data-driven styles
var colorList = [
    [1614, '#D49A66'],
    [4132, '#A29354'],
    [8056, '#738856'],
    [13919, '#50795F'],
    [20464, '#416562'],
    [34615, '#44505A']
];

// 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
        },
        'source-layer': 'Signalized_Intersection_Traff-4ypaqa', //name of tileset
        'paint': {
        //Add data-driven styles for circle-color
        'circle-color': {
            property: '8HrPedVol',
            type: 'interval',
            stops: colorList
        },
        //Add data-driven styles for circle radius
        'circle-radius': 10,
        'circle-opacity': 0.8
        }
    });
});

Result

View demo two.

Step 3 : Add a legend

To help the user understand the absolute number of pedestrians at an intersection, add a color legend.

Create a legend

  1. Create an HTML element with some CSS styling to the map
  2. Populate the HTML element with colors and labels linked to the data-driven style

Create the CSS style

First create the legend CSS:

.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 div span {
  border-radius: 50%;
  display: inline-block;
  height: 10px;
  margin-right: 5px;
  width: 10px;
}

Create the HTML element

Next create the HTML legend element, identifying the pedestrian count and the color.

<!-- add html elements to the legend !-->
<div id='legend' class='legend'>
    <h4>Pedestrian Traffic</h4>
    <div><span style='background-color: #44505A'></span>34615</div>
    <div><span style='background-color: #416562'></span>20464</div>
    <div><span style='background-color: #50795F'></span>13919</div>
    <div><span style='background-color: #738856'></span>8056</div>
    <div><span style='background-color: #A29354'></span>4132</div>
    <div><span style='background-color: #D49A66'></span>1614</div>
  </div>

Result

View demo three.

Step 4 : Add user interactivity

Now provide the user with the ability to interact with the 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:

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