Embedding Mapbox Plots in Jupyter Notebooks

Hey there 2017

We've been working on this problem in a new Python Library, `mapboxgl`.  Check it out here!  https://github.com/mapbox/mapboxgl-jupyter

Now back to the old Jupyter + Mapboxgl Python blog post...

 

Problem

When analyzing data in the excellent Jupyter Notebook programming IDE, I often find myself wanting to make a map.  While some of the existing libraries make this possible - namely Python's Folium library, which uses Leaflet.js under the hood - I always come away wanting more flexibility.

Enter embedded Mapbox-gl.js visualizations right in the Jupyter notebook - allowing for interactive, fully realized map visuals.  

Here are the steps to make it happen.  The same technique can be applied to embed any javascript library, such as D3.js for more visualization and charting options.

Goal to Create

Below is our target visualization - an interactive segment exploration map of Strava segments.  The data is constantly fetched from the Strava API, and cached locally so that the user can build up a list of all of the segments in a given lat/long bounds.

Before we Begin...

Mapbox-GL.js interacts with data as Vector Tiles or as a geojson data layer.  Today we are going to use a geojson data layer.  Ensure your geojson data is valid prior to trying a viz!  Here are two useful Python Pandas functions to help output your data as a geojson FeatureCollection (points or lines).  Using the functions below, we can create a Python variable which contains a geojson string that we want to visualize.

I extended Geoff Boeing's work for the code below - see his related blog post here.


import pandas as pd
import json
#Converts a dataframe to a geojson Point output
def df_to_geojson_point(df, properties, lat='latitude', lon='longitude'):
    geojson = {'type':'FeatureCollection', 'features':[]}
    for _, row in df.iterrows():
        feature = {'type':'Feature',
                   'properties':{},
                   'geometry':{'type':'Point',
                               'coordinates':[]}}
        feature['geometry']['coordinates'] = [row[lon],row[lat]]
        for prop in properties:
            feature['properties'][prop] = row[prop]
        geojson['features'].append(feature)
    return geojson

#Converts a dataframe to a geojson LineString output
def df_to_geojson_line(df, properties, coords):
    geojson = {'type':'FeatureCollection', 'features':[]}
    for _, row in df.iterrows():
        feature = {'type':'Feature',
                   'properties':{},
                   'geometry':{'type':'LineString',
                               'coordinates': ''}}
        feature['geometry']['coordinates'] = row[coords]
        for prop in properties:
            feature['properties'][prop] = row[prop]
        geojson['features'].append(feature)
    return geojson

1) Setup the notebook for HTML/CSS/JS

The code allows the Jupyter notebook to pass information from the notebook server to the JavaScript code in your notebook cell.  Here we assign the JavaScript variable 'window.vizObj' to the Python variable 'geojson' (where we have our geojson string stored).  When we create out viz, we can access our data as a global variable through this window.vizObj variable.
 
The HTML() function allows us to run explicit HTML functions to define our web page.  In this instance, I take the opportunity to import a to define the map object DOM style - which will help format the final output map.


from IPython.display import Javascript
#Create a javascript variable with our geojson data to visualize in the browser
#The data object 'vizObj' will be a global varialbe in our window that 
#We can pass to another javascript function call
Javascript("""window.vizObj={};""".format(geojson))

#Create some HTML to style out map and define it's output size
from IPython.display import HTML
HTML('''
    
    
''')

2) Add required JavaScript libraries

In this notebook cell, we take advantage of Jupyter require.js to manage its notebook in the browser IDE.  We use the %%javascript magic function to tell Jupyter to interpret the cell as javascript - then we pull our required javascript libraries from a CDN.


%%javascript
require.config({
  paths: {
      mapboxgl: 'https://api.tiles.mapbox.com/mapbox-gl-js/v0.21.0/mapbox-gl',
      bootstrap: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min'
  }
});

3) Create your viz with Mapbox-GL.js Javascript!

Now we can interact with our data from our Jupyter notebook!  Start by requiring the libraries that you will need with a require function - then write your javascript code to make your visualization.  At the end, create the HTML element 'map' using the element.append() command - which tells Jupyter to append the HTML element to the cell output area. 


%%javascript
IPython.OutputArea.auto_scroll_threshold = 9999;
require(['mapboxgl', 'bootstrap'], function(mapboxgl, bootstrap){
    mapboxgl.accessToken = 'pk.eyJ1IjoicnNiYXVtYW5uIiwiYSI6IjdiOWEzZGIyMGNkOGY3NWQ4ZTBhN2Y5ZGU2Mzg2NDY2In0.jycgv7qwF8MMIWt4cT0RaQ';
    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: 10 // starting zoom 
    });
    
    
    function addSegLayer(mapid) {
        // Mapbox GL JS Api - import segment
        var segment_src = new mapboxgl.GeoJSONSource({
            data: window.vizObj,
            maxzoom: 18,
            buffer: 1,
            tolerance: 1
        });
        try {
            mapid.addSource('segment', segment_src);
            mapid.addLayer({
                id: 'segment',
                type: 'line',
                source: 'segment',
                paint: {
                    "line-opacity": 1,
                    "line-width": 5,
                    "line-color": 'red',
                }
            });
        } catch (err) {
            console.log(err);
        }
    };
    
    map.once('style.load', function(e) {
        addSegLayer(map);
        map.addControl(new mapboxgl.Navigation({
            position: 'top-left'
        }));
    });
    
});
element.append('<div id="map"></div>');

4) Interact with the result in your Jupyter Notebook

Right in your notebook you can now view the Mapbox plot from step 1.

Check out the notebook on my GitHub here.  

Questions?  Please leave a comment!  This is only the tip of the iceberg.