Visualizing Climate Change with Mapbox GL

NOAA recently updated their ocean temperature records to account for inaccuracies. This proved to be one more nail in the coffin of climate change denial.

That said, we’ve had surface records for a long time that show rising temperatures. When someone dumped the HadCRUT3 Global Temperature Record on Reddit, I decided display it using Mapbox GL.

Click here to check out the final visualization.

A bit about Mapbox: Mapbox maintains an awesome mapping service built on Open Street Map and Leaflet technology. It’s mostly open source and extremely powerful (seriously, check out all the APIs they have).

Looking at the visualization, you can clearly see the temperature rising over time. As my friend put it: Great! You can really see the temperature rising on this map. Oh wait. That’s not so great.

“Great! You can really see the temperature rising. Oh wait. That’s not so great.”

Quick Overview

To build this visualization, I had to do three things:

  1. Convert Raw Data to GeoJSON Feature Collections
  2. Upload Data to Mapbox and Creating a new Map Style
  3. Load my Map Style using MapboxGL.js and embed it in a webpage

Code is here—you can either clone it or use the web interface to see the files I’m referencing.

Step 1: Parsing and Conversion

The raw temperature data can be downloaded from the UK Met Office. By itself, it’s a number of text files in a structured format. I wrote a simple python script to parse these files and convert them to GeoJSON Feature Collections that could be uploaded to Mapbox Studio.

Protip: use Pandas to parse this kind of data. Did not know about it at the time.

GeoJSON is a simple format for describing objects on a map. It describes points, polygons, etc along with their geographic coordinates. For the stations, I created point features with a set of properties numbered from 0 to 160, corresponding to observed temperature from 1850 to 2010. If a temperature observation is missing, the value is set to -99, so that the visualization code knows to disregard it.

This results in some data bloat (because we have many missing data points), but it avoided issues with telling Mapbox GL to render an undefined value.

The parsing script generates 13 GeoJSON files, corresponding to temperature data for a given month of the year. Additionally, one contains header information that is used to draw the tooltips in the visualization. I segmented the data like this because embedding all 1920 possible temperature observations in each point feature made the file too big for Mapbox. Since we’re concerned with seeing data trends, segmenting the data by month makes sense as this shows change over time rather than seasonal variations.


Step 2: Uploading to Mapbox

I then uploaded all GeoJSON files to Mapbox Studio and added them to a new style. Each file went on a different layer, which were placed below country labels / boundaries to prevent the visualization from obscuring the underlying map.

All layers were initially set to be invisible so they could be activated programatically.

You can see the layers here:

Climate Data is loaded in 12 layers, each invisible.

I then exported this style and used it as the basis of the visualization. Saving it as a style meant that I didn’t have to manually add all the layers each time the map was loaded, reducing the amount of code I had to write and possibly boosting performance.


Step 3: Rendering Map with JavaScript

The main visualization code is in climate-sim.js . If you want to see the HTML and/or CSS, they can be viewed in the Github Repo. My CSS skills were very basic when I made the visualization; also, the HTML file is a vanilla page with a <div id="map”> element for Mapbox GL to target, as well as some JQuery controls. Not going to go over these here.

Here’s a walkthrough of climate-sim.js

Constants

I start out by defining some constants.

Global State Variables

Next I declare some globals to hold the simulation state. (If this were a large project, it’d be a good idea to embed these in a state object of some sort.)

I also define a global singleton for the popup tooltip:

Because the simulation is simple, using a single popup object and moving it around is more efficient than creating multiple ones and keeping track of them.

Helper Functions

Next I declare a few functions to convert colors between formats, calculate a color based on a gradient, and initialize various control objects. (These are minified in the climate-sim.js file).

Temperature/Header Display Styles

This is where the code starts to get interesting. I opted to offer two distinct display methods: heatmap and solid. These are stored in a global object called styles that contains two generator functions with the same names. heatmap achieves its effect by drawing every station with a large radius and a large blur, so that adjacent stations blend together. Solid instead draws a small radius circle with no blur.

heatmap and solid are functions rather than style objects because I need to choose which property contains temperature data dynamically. Thus, I can call styles['heatmap'](150) to get a heatmap temperature style corresponding to the 150th year of the visualization.

Here is the heatmap style:

All temperatures are displayed as circles with radii from 60 at base-zoom to 600 at max-zoom. This ensures they will be quite large and overlap. To create the heatmap effect, you’ll notice circle-opacity is set to 0.125. This ensures that they will add and blend with each other (it is set to 0.0 at -99 so that missing data points do not draw). Lastly, I set circle-blur to 1 so that they display in a very diffuse manner.

Here’s what it looks like:

Here is the solid style:

No blur, small radius, 100% opacity—ideal for showing the precise location of temperature stations.

Here’s the effect:

Lastly, headerStyle which renders the header layer.

This layer draws invisible circles that will register when the mouse hovers over them, but not interfere with the drawing of the map. Making them invisible by using the visibility property will cause them not to trigger mouseover events. Thus I use circle-opacityinstead. I use the circle-radius property from the solid style to keep the sizes in sync with each other and ensure that mousing over a circle in solid display will trigger a popup to appear. (Popups are hidden in heatmap display because it’d be very difficult to figure out which station is under the mouse).

Map Update Functions

Next, a few functions that update and redraw the map when our state changes. Every time the map changes I call updateMap which chooses a new temperature data point, makes a new style to draw it, and then applies the new style to the map.

The actual styles are applied to the map by calling applyStyles :

This only updates the properties passed in. When drawing a map layer initially, I have to set all styles, but to change the temperature drawn I only need to update circle-color and circle-opacity properties, as blur and size do not change within a given display style. This speeds up redraws.

layers is an array, so that I can easily change the style for all layers.

Next, we updateAnomaly which changes the color of the water on the map if the user has checked the anomaly checkbox.

I retrieve the current temperature anomaly and then decide what color to draw by using helper functions convertColor and colorFromGradient.

Next, updatePopup :

This updates the popup’s HTML contents when the map is animating, so the currently displayed temperature is kept in sync.

Finally, updateHTML :

This keeps the UI components in sync.

Drawing the Map

First, we make sure the document is ready:

$(document).ready(function(){ ... }

Then create the map:

Wait for it to load:

Display our data and load some values:

I store the initial water color so it can be restored if the user toggles anomaly display. This decouples the code from the chosen map style. I then apply the current style to all layers, and set the current layers to visible so they can interact with the mouse.

Next I create the slider using JQuery:

Important thing to note here: I do not update the map as the slider is moving — only when it stops. This makes it faster.

Next I initialize the months selection menu and set an event handler on change (using my initSelect helper function):

Important point to note here is that to change months, I simply hide the current layer and show the layer corresponding to the month I want. This creates a break in continuity to change layers, but it ensures that animations within a layer (showing temperature trends) are smooth.

Next, I initialize the styles selector similarly:

When a user selects a new style, I use the applyStyles helper to reapply it to all layers.

Next major order of business is setting up event handling for the popups:

I’ve left the comments in here to explain what is going on. Main things to note: (1) we don’t display the popup if we’re in heatmap mode, (2) we display the popup by calling popup.setLngLat(result['coordinates']) if we've passed all the checks. As discussed before, we don’t create a new popup object as this would be wasteful—we just reuse the existing one.

Also note that since the header layer and the temperature layer are different, we must query two layers as follows:

and then load the data out of them using a little kludge:

This adds complexity, but keeps the map size smaller. Adding header data to every temperature layer would be wasteful.

Finally, I set up event handlers for the other controls:

That’s all there is to it! With a few wrapper functions to update the proper map layers, we’ve displayed 150 years of climate data in our browser. We just translate a given year to a temperature index, and then tell Mapbox GL to render the map color based on that temperature index.

Feel free to clone the repo and play around with it yourself if you’re so inclined :)