Mapbox GL Maps: How-To Create Data-Driven Styles

Mapbox-GL is a high-performance open-source mapping library built by the Mapbox team on the WebGL canvas.  WebGL technology allows a web browser to use the client's GPU to render high-quality, smooth animations and, important for AtheteDataViz, high volumes of data.

In this tutorial, we're going to create this map.  The type of map is a "heatmap", consisting of a background layer, and a layer of GPS points, colored by the value of the "Speed" property for each point.  The higher the value of "speed" for each point, the more "red/yellow" the color.

 

What's the problem?

in the current Mapbox-GL Javascript library, data-driven styles are not readily exposed in the API.  "Data-Driven" simply means controlling a layer's color, blur, opacity, or other style based on the metadata of that data point - like speed or grade.  The Mapbox team is  working on a solution, but it is not yet available in the Javascript API.  

Anand Thakker made a great blog post on how to achieve data-driven styles in September 2015.  I want to extend his examples with how to apply this technique to geoJSON GPS point data loaded from a URL.

Example:  Color GPS points based on Speed

Step 1 - Set global variables

These variables will control the style of our layers.


var breaks = [3, 6, 9, 12, 16]; //break points denoting data sould be a new color
var colors = ['blue','cyan','lime','yellow','red']; //colors to use in the data-driven style
var layers = []; //bucket to hold our data layers.  We'll fill this in next.
var filters = []; //array to hold our filter layers.  We'll fill this in next.

Step 2 - Calculate the filters for each break.  

This will set a Mapbox filter parameter for each element in the "break" variable.  Our English logic here is:  "When the data value is between breaks[0] and breaks[1],  then use this filter object".


function calcHeatFilters(breaks, param) {
    //calculate filters to apply to sheet (first run only)
    filters = [];
    for (var p = 0; p < breaks.length; p++) {
      if (p<=0) {
        filters.push([ 'all',
          [ '<', param, breaks[p + 1] ]
        ])
      }
      else if (p < breaks.length - 1) {
        filters.push([ 'all',
          [ '>=', param, breaks[p] ],
          [ '<', param, breaks[p + 1] ]
        ])
      } else {
        filters.push([ 'all',
          [ '>=', param, breaks[p] ]
        ])
      }
  }
}

Step 3 - Define Layer object

Now we need to create the Mapbox GL JS layer object.  A layer is just a JSON array that defines the Mapbox GL JS "style".  The English logic of the code below is "For each item in the break array, add a layer to the layer array.  The added element to the layer array is a JSON string containing the definition of the filter and the style of the layer."

See the docs on Mapbox GL JS styles here


function calcHeatLayers(filters, colors) {
    //create layers with filters
    layers = [];
    for (var p = 0; p < breaks.length; p++) {
    layers.push({
        id: 'points-' + p,
        type: 'circle',
        source: 'geojson_src',
        paint: {"circle-color": colors[p],
                "circle-opacity" : 1,
                "circle-radius" : 10,
                "circle-blur" : 1
        },
        filter: filters[p]
      })
    }
}

OK!  Now we've got our layers.  Let's make our map.

Step 4 - Create the map and add the source layers

Here we load the map from mapbox.  Make sure you have a free Mapbox account created.  First, grab your Mapbox GL public key and place it in the <your Mapbox GL API key here> variable.   Second, either add your GPS coordinate data to the geoJSONvariable, which can either be a URL to the data or raw data in the Javascript file below.  

Lastly, take a look at what the code does below.  

In plain English, the code instructions are:  "First, run the functions to calculate the data-driven styles.  Next, create a map in the "map" HTML div.  Then once the map background style loads, fetch the data for the GeoJson layer.  Next add the geojson source to the map.  Finally, add the layers to the map as "circle" styles, and make a popup mouseover with the value of the speed when the user cursor hovers over the point."


calcHeatFilters(breaks, 's'); //calc the filters based on a data parm
    calcHeatLayers(filters, colors); //calc the layers

    if (!mapboxgl.supported()) {
        //stop and alert user map is not supported
        alert('Your browser does not support Mapbox GL.  Please try Chrome or Firefox.');
    } else {
        mapboxgl.accessToken = ;
        var map = new mapboxgl.Map({
            container: 'map', // container id
            style: 'mapbox://styles/mapbox/dark-v8', //stylesheet location
            center: [-89.948470, 40.783860], // starting position
            zoom: 12 // starting zoom 
        });
    }

    map.once('style.load', function() {
        geojson_src = new mapboxgl.GeoJSONSource({
            data: geojsonData,
            maxzoom: 16,
            buffer: 10,
            tolerance: 10
        });
        map.addSource('geojson', geojson_src);
        map.batch(function(batch) { //add each layer to the map in a batch
            for (var p = 0; p < layers.length; p++) {
                batch.addLayer(layers[p]);
            }
        });
    });

    //add popup for clarity about what we're displaying with this data driven style
    var popup = new mapboxgl.Popup({
        closeButton: false,
        closeOnClick: false
    });

    map.on('mousemove', function(e) {
        for (var p = 0; p < layers.length; p++) {
            map.featuresAt(e.point, {
                radius: 20, // Half the marker size (15px).
                includeGeometry: true,
                layer: 'points-' + p
            }, function(err, features) {
                // Change the cursor style as a UI indicator.
                var feature = features[0];
                // Initialize a popup and set its coordinates and add speed pointer
                if (!err && features.length) {
                    map.getCanvas().style.cursor = (!err && features.length) ? 'pointer' : '';
                    popup.setLngLat(feature.geometry.coordinates)
                        .setHTML('speed: ' + feature.properties.s)
                        .addTo(map);
                }
            });
        }
    });

Put it all together

With a bit of HTML and javascript imports,  we can display the map with data driven styles!  Full code sample below.  Tweak the "breaks", "colors", and "layers" functions to style the data code to adapt the style to fit your application needs.

Download the full code example on my GitHub