Tableau with Next-Gen GL Maps, Today

Want to create a great map experience on your Tableau dashboard?  

 I combined Mapbox GL JS with Tableau to integrate the best of the mapping world with the best of the BI world.

Let's back up a minute

Why do we need better maps in Tableau?  Tableau is easy, and I thought Mapbox was already in Tableau?  

Consider the following.

How do I put data below map labels in Tableau?

Where'd my labels go?  Am I in NYC?  What street is this point incident on?  What borough am I in?

How do I control the user-experience and viz while zooming?

Huh.  When zoomed out the circles for my data are too big!  As I zoom, the circles get smaller!  When fully zoomed into street level, the circles are too small to be useful..

What about adding custom polygons and shapes?  3D buildings?  

How do I make geospatial queries, like distance, buffer, and intersect?

 How can I render some really big geo data, like shipping routes around the world, or a complex point cloud from a drone survey?  

All of the above are hard in Tableau today because maps are rasters.  Rasters are just images - a PNG file on your computer.  Images don't differentiate between "layers", and must be converted based on pixel information and position to extract vector data from them.

Mapbox GL solves this problem with vector tiles. The javascript API for Mapbox GL integrates seamlessly with Tableau via the Tableau Javascript API.  

Here's how to get started building with Mapbox GL and Tableau with vector maps, right now.  

What we're about to build

How to build it:

1. Prep your data

Get your data source in CSV format, fully cleaned and ready to visualize.  That means adding all the dimensions and measures that you'll need to your data first.

 For this example, I downloaded the NYC Vehicle Collision data from the NYC Open Data website, geocoded all missing long/lat points, removed null and irrelevant columns, and encoded strings as utf-8.

2. Create your Tableau Dashboard

For the NYC Vehicle Collision dashboard, I created a three charts - a time series bar chart by month, a categorical bar chart by collision factor, and a categorical bar chart by borough.  A parameter ties all of the charts together, allowing the user to select the metric to display - cycling injuries, cycling fatalities, pedestrian injuries, or pedestrian fatalities. 

Important note - Make each of your visuals a separate dashboard!  One dashboard per viz.  Also, set the dashboard size to 'automatic'.  This will allow the dashboard to be reactive on web and mobile, or whatever device you can cook up.

Don't worry about creating a map in Tableau - we'll handle that with Mapbox later on.  

When you're finished, publish your viz to Tableau Public (or your Enterprise Tableau Server, or Tableau Online) and grab your 'share' URL.  

The dashboard URL above is here.

3. Upload your CSV data source to Mapbox.

We want our map to be fast and look great.  Mapbox GL achieves this using vector tiles.  The Uploads API renders your CSV, ESRI Shapefile, or geojson data source as vector tiles.  Log in to Mapbox and upload your data.  When complete, grab the Map ID and your Mapbox Public Access Token.  

If you are unfamiliar with Mapbox, check out a tutorial on uploading data here.  It's easy!

Mapbox converted my CSV file to vector tiles!  

Grab your 'Map ID' string from the right sidebar.  Your Public Access token is on the main Mapbox Studio home page.

 

4. Create the web app

Create a new text file "index.html".  Put the following code into the file using your favorite text editor.

Load required libs

First drop in the HTML page header, where we load the javascript and css libraries we need to make our web app work.  These libraries are Mapbox GL JS, Tableau JS, and JQuery (which is required for Tableau's JS API to function properly)

<head>
    <meta charset='utf-8' />
    <title>Mapbox GL in a Tableau Dashboard</title>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
        <script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.28.0/mapbox-gl.js'></script>
    <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.28.0/mapbox-gl.css' rel='stylesheet' />
    <link href='https://www.mapbox.com/base/latest/base.css' rel='stylesheet' />
    <script src='https://code.jquery.com/jquery-1.10.2.js'></script>
    <script src="https://public.tableau.com/javascripts/api/tableau-2.min.js"></script>
</head>

Now we create the body below the HTML <head>.  Here we insert the div elements for each of our Tableau Dashboard Viz's and our Mapbox Map:

    <div id='title' class="middle">
        <h2> <a href='https://data.cityofnewyork.us/Public-Safety/NYPD-Motor-Vehicle-Collisions/h9gi-nx95' target='_blank' class="color: white;">NYC Vehicle Collisions</a> </h2>
    </div>
    <div class='container'>
        <div class='section group' id='mycontainer'>
            <div class='col span_2_of_3'>
                <div id='map'></div>
            </div>
            <div class='col span_2_of_3'>
                <div id='tableauViz1'></div>
            </div>
            <div class='col span_2_of_3'>
                <div id='tableauViz2'></div>
            </div>
            <div class='col span_2_of_3'>
                <div id='tableauViz3'></div>
            </div>
        </div>
    </div>

Load Dashboard Elements

Next we load our Tableau dashboards.  Insert the code below in a <script> tag in the body of the page.  Here we load the Tableau viz elements into each HTML div.  The main thing you need to update are the `oneURL`, `twoURL`, and `threeURL` variables to reflect your Tableau dashboard share URL.

var vizOptions = {
        width: '100%',
        height: '400px',
        hideTabs: true,
        hideToolbar: true,
        onFirstInteractive: function() {}
    };
var oneUrl = 'https://public.tableau.com/views/NYCSafetyDashboard/a2?:showVizHome=no&:display_spinner=yes&:jsdebug=n&:embed=y&:display_overlay=no&:display_static_image=no&:animate_transition=yes ';
var twoUrl = 'https://public.tableau.com/views/NYCSafetyDashboard/b2?:showVizHome=no&:display_spinner=yes&:jsdebug=n&:embed=y&:display_overlay=no&:display_static_image=no&:animate_transition=yes ';
var threeUrl = 'https://public.tableau.com/views/NYCSafetyDashboard/c2?:showVizHome=no&:display_spinner=yes&:jsdebug=n&:embed=y&:display_overlay=no&:display_static_image=no&:animate_transition=yes ';

onePh = document.getElementById('tableauViz1')
twoPh = document.getElementById('tableauViz2')
threePh = document.getElementById('tableauViz3')

var viz1 = new tableauSoftware.Viz(onePh, oneUrl, vizOptions);
var viz2 = new tableauSoftware.Viz(twoPh, twoUrl, vizOptions);
var viz3 = new tableauSoftware.Viz(threePh, threeUrl, vizOptions);

Make your Mapbox Map

Next we make a map with Mapbox GL map!  Make sure to update the code below with your Mapbox public access token, and edit your `map.addSource()` URL's to the Map ID of your tileset (the data you uploaded to your Mapbox account).

After the map.addSource() function, I add layers from the sources.  Think of layers and being the style of data you want to draw on the map from your data.  In this case, I create a 'circle' layer and define how it should be styled with Mapbox GL Style Spec - which is just some (powerful) JSON configuration code.  

mapboxgl.accessToken = 'your-mapbox-public-access-token';
    var bounds = [
        [-75.04728500751165, 39.68392799015035],
        [-72.91058699000139, 41.87764500765852]
    ];
    var centerPoint = [-73.98, 40.7488]

    var map = new mapboxgl.Map({
        container: 'map',
        style: 'mapbox://styles/mapbox/dark-v9',
        center: centerPoint,
        zoom: 12,
        minZoom: 10,
        maxZoom: 22,
        maxBounds: bounds
    });

    map.once('load', function() {
        map.addSource('veh-incidents', {
            type: 'vector',
            url: 'mapbox://rsbaumann.4juqkxoc?optimize=true'
        });
      
        map.addLayer({
            'id': 'veh-incd',
            'type': 'circle',
            'source': 'veh-incidents',
            'source-layer': 'nyc_pedcyc_collisions_161004-d5u2nq',
            'paint': {
                'circle-color': {
                    'property': 'CYC_INJ',
                    'type': 'exponential',
                    'colorSpace': 'lab',
                    'stops': [
                        [1, 'orange'],
                        [2, 'red']
                    ]
                },
                'circle-radius': {
                    'property': 'CYC_INJ',
                    'base': 3,
                    'type': 'exponential',
                    'stops': [
                        [{
                            zoom: 10,
                            value: 1
                        }, 2],
                        [{
                            zoom: 12,
                            value: 1
                        }, 4],
                        [{
                            zoom: 14,
                            value: 1
                        }, 10],
                        [{
                            zoom: 16,
                            value: 1
                        }, 20],
                        [{
                            zoom: 10,
                            value: 3
                        }, 3],
                        [{
                            zoom: 12,
                            value: 3
                        }, 6],
                        [{
                            zoom: 14,
                            value: 3
                        }, 15],
                        [{
                            zoom: 16,
                            value: 3
                        }, 30],
                    ]
                },
                'circle-opacity': 0.8,
                'circle-blur': 0.5
            }
        }, 'veh-incd-base');

Awesome!  At this point you can open your HTML file in your browser, and you should see your map and all your Tableau Dashboards on one canvas - did it work?

Sync Filters between Tableau and Mapbox

Next we want to integrate Tableau filters and parameters with Mapbox..  Use the code below to calculate what a Mapbox filter to remove or add should be, based on the filters in your Tableau Dashboard.  

function mutateFilter(filter, old_operator, old_key, new_filter, recalc) {
        // Given a Mapbox GL Style Spec filter, remove an existing filter matching old_key and old_operator
        // If recalc==true, push the new_filter to the array
        
        for (i = 1; i < filter.length; i++) {
            if ((filter[i][0] === old_operator) && (filter[i][1] === old_key)) {
                filter.splice(i, 1); //remove existinng parameter filter
            }
        }
        if (recalc==true) {
            filter.push(new_filter);
        }
        return filter
    }

Now add event listeners to the 'markerEvent' in your Tableau dashboards.  This event will fire when the user 'clicks' on a bar or point on the Tableau Dashboard.  After the event is fired, run a callback function to set the appropriate filters in each map and Tableau viz.

viz1.addEventListener(tableau.TableauEventName.MARKS_SELECTION, onBoroughSelection);

function onBoroughSelection(marksEvent) {
    return marksEvent.getMarksAsync().then(reportBorough);
}

function reportBorough(marks) {
    //sync filters on map and dashboard when interacting with boroughs dash 
    if (marks.length > 0) {
      //add the new filter to the list
        pair = marks[0].getPairs()[0];
        fieldName = pair.fieldName.toUpperCase().replace(/\s/g, '_');
        fieldValue = pair.value;
        newFilterOne = ['==', fieldName, fieldValue]
        incd_filter = mutateFilter(incd_filter, '==', fieldName, newFilterOne, true)
        //apply the clicked filter to the other Tableau workbooks
        viz2.getWorkbook().getActiveSheet().getWorksheets()[0].applyFilterAsync(fieldName, fieldValue, tableau.FilterUpdateType.REPLACE);
        viz3.getWorkbook().getActiveSheet().getWorksheets()[0].applyFilterAsync(fieldName, fieldValue, tableau.FilterUpdateType.REPLACE);
    } else {
      //Reset the filter to empty if no values
        viz2.getWorkbook().getActiveSheet().getWorksheets()[0].clearFilterAsync(fieldName);
        viz3.getWorkbook().getActiveSheet().getWorksheets()[0].clearFilterAsync(fieldName);
        incd_filter = mutateFilter(incd_filter, '==', 'BOROUGH', [], false)
    }
    refreshMap(map)
}

The gist of the code above in the reportBorough() function is to use the Tableau JS API to get the 'value' of the data field the user interacted with.  For example, in this dashboard, we are getting the 'name' of the NYC Borough the user clicked on in the bar chart.  Then we can pass that value to mutateFilter() to calculate what the equivalent Mapbox filter so the filtered visuals remain .

Sync Parameters between Tableau and Mapbox

Now we apply the same filter technique to the parameter selection dropdown box:

function onParamChange(paramEvent) {
        return paramEvent.getParameterAsync().then(reportSelectedParams);
    }

function updateParam() {
        //Get the property value from the HTML dropdown
        var propValue = document.getElementById('metric').value
        //run a function to set the Mapbox filer
        filter = ['all', ['>=', propValue, 1]]
        map.setFilter('incd', filter)
        //Update the Tableau viz
        viz1.getWorkbook().changeParameterValueAsync('Metric', propValue);  
    };

Boom.

 Mapbox GL JS and all the capabilities, now on your Tableau Dashboard.

As a plus, this technique (pioneered by Alan Walker)  yields a mobile-ready design.  Pull it up on your phone or tablet, laptop or TV, and enjoy making some awesome data viz.  Credit for the dashboard design is all for Anya A'Hearn

Source code on Github.

Build something cool with Tableau and maps?

Share it here!