Introduction

It has been a while since I did a technical post and wanted to do something a little bit different and a little more visual.  I've been looking for a little experiment I could do around data visualization and came up with the idea of plotting some of my Strava data onto maps.  The idea, to plot a 3D elevation view of my routes for a new way to view my runs.

The final result is going to be a proof of concept React component using Hooks. During my scoping exercise I narrowed down three different implementations:

After playing around with each option, Vis.js worked but felt rather vanilla in presentation, ThreeJS was going to be more effort than we want at this stage. Mapbox offer a fantastic API and great visual outcome, so this became the desired implementation.  

The following sections explain the technical implementation, if you want to skip straight to the results Click Here.

Exporting From Strava

Each activity on Strava lets you output the GPX file which is the raw data recorded by your GPS device.  Once exported the GPX needs to be converted into a geojson file that is supported by a number of web technologies. There are a number of online conversions and scripts available to do this.  I eventually found an excellent Python script that outputted the geojson in the format required for this experiment.

React + Mapbox - Technical Implementation

I wanted to avoid using a third party wrapper and use the direct mapbox.js. This obviously involves us telling the Mapbox API where to mount the component in the DOM.  The way we can achieve that is in React is by using Refs.  Since we are using Hooks we can make use of the useRef Hook to help us out.


const Map = () => {
	// Declare the useRef hook setting to null
	const mapContainer = useRef(null);
    
    return <div ref={el => (mapContainer.current = el)} />;
};

export default Map;

The above snippet gives us a reference to the DOM element.  Refs are generally discouraged in React but given we are using this to mount a third party library it is ok.  

Now we have our ref to the DOM element, we can install MapBoxGl using yarn and import it into our library in the usual manner and integrate it into our component. We want to set Mapbox when the component is mounted and have a container to store our map.  We can make use of useEffect and useState accordingly for this.  Mapbox has fantastic documentation on how to set up the map, but for reference I have included the initial setup of my component.

useEffect(() => {
    const initializeMap = ({ setMap, mapContainer }) => {
      MapboxGl.accessToken = <API TOKEN>

	let center = Turf.center(data);
    const map = new MapboxGl.Map({
    	container: mapContainer.current,
        style: "mapbox://styles/centrium/ck6nxu73n0yam1imvp641ulxl", 
        center: center.geometry.coordinates,
        zoom: 13,
        pitch: 60,
        bearing: -30,
        antialias: true
      });

We use TurfJs (turfjs.org/) to work out the center of the data that I load in so I can position the camera to point to the center when it is loaded up. Once loaded up I need to plot the data onto the map, there are several ways to do this but remember the goal is for 3D elevation.  This can be achieved using MapboxGL Fill-Extrusion layer. The extrusion height was obtained from reading the elevation property for each feature in the data.

       map.addLayer({
          id: "route-3d",
          type: "fill-extrusion",
          source: "route",
          paint: {
            "fill-extrusion-color": #ff0000,
            "fill-extrusion-height":["get", "elevation"],
            'fill-extrusion-opacity': 0.7
          }
        });

I say close because this is how it turned out:

First attempt

It looks ok, but notice some of the 3D elements vanish.  It turns out this is due to a performance improvement in the core MapboxGL library they implemented face culling which meant on the front face is displayed.  

After a few hours of reading, and looking at the Mapbox source to understand the problem I decided to fork my own version to disable the face culling.

The results

After a few final tweaks to the layers and color, I finally got something I quite liked the look off.  I changed the colours based on the value of the gradiant change. Green shows slight change in grade, whilst red shows a grade change of 7% or greater. The result was a proof of concept React function component that could take some Strava data and output beautiful 3D elevations plotted against maps. Each map can be zoomed and rotated for detailed analysis.

360 View of the Great North Run Route

The maps below are all generated from my favourite running achievements from the Edinburgh Marathon to a 5K around Central Park in New York.

As always I would love to hear any feedback.

Edinburgh Marathon 2015 (26.1miles)
Great North Run (2015) 13.1 Miles
Crammond 10k
New York 2016 5K