Plotting on maps with d3 and Mapbox

Intro

Mapbox offers beautiful, vector-tiled maps. The provided Mapbox GL JS library has some nice plotting capabilities, but you will quickly encounter its limits if you need to do advanced plotting.

In those cases, d3.js comes to the rescue with its almost unlimited customization capability.

In this tutorial, I will show how to get the two working together. We will create the above visualization, a map showing all earthquakes in 2018 with a magnitude 5 or greater.

You can find all code used in this tutorial in this Github repository.

A simple map

To get started, head on over to Mapbox and register to get an access token.

We will start with their simple map example as basic a template. In the body of the Html, you create a nice, empty map using your mapbox token.

<div id="map"></div>
<script>
  mapboxgl.accessToken = "YOUR_TOKEN";
  var map = new mapboxgl.Map({
    container: "map",
    style: "mapbox://styles/mapbox/streets-v9",
    center: [-74.5, 40.0],
    zoom: 9
  });
</script>

Adding the d3 svg

Next, we add the usual d3 svg element on which we will draw our graphics. We append the svg to the mapbox canvas:

var container = map.getCanvasContainer();
var svg = d3
  .select(container)
  .append("svg")
  .attr("width", "100%")
  .attr("height", "500")
  .style("position", "absolute")
  .style("z-index", 2);

You might notice that I’m setting a z-index. This is due to one of those small hidden traps I encountered: By default, the mapbox tiles will be placed on top of the d3 svg canvas, hiding all the beautiful graphics (and making you waste a lot of time trying to figure out what’s wrong with no error message in the console).

To avoid this, you specify the z-index css attribute for the svg, then do the same for the map with a lower value (note that the z-index is ignored unless you also set the position attribute):

#map {
  position: relative;
  z-index: 0;
  width: 100%;
  height: 500px;
}

Also pay attention that the size of your svg is the same as the map.

Getting mapbox and d3 to talk

The key to plotting with d3 and mapbox is to get their coordinate systems to talk to each other. Spatial data is usually in latitude/longitude coordinates, which mapbox uses internally, but d3 has to know which pixel on the canvas corresponds to which spatial coordinate.

This is done using the following projection function:

function project(d) {
  return map.project(new mapboxgl.LngLat(d[0], d[1]));
}

That’s it! This is the rosetta stone that connects d3 and mapbox.

Dots on a map

Let’s get some d3 objects on the map!

d3

First, we add some data, add circles and bind the data.

var data = [
  [-74.5, 40.05],
  [-74.45, 40.0],
  [-74.55, 40.0],
];

var dots = svg
  .selectAll("circle")
  .data(data)
  .enter()
  .append("circle")
  .attr("r", 20)
  .style("fill", "#ff0000");

We haven’t set the position yet - time for our projection method to shine.

In the following render method, we pass the data of the circles the projection method, get the canvas coordinates corresponding to the spatial coordinates, and set them as the position of the circles:

function render() {
  dots
    .attr("cx", function (d) {
      return project(d).x;
    })
    .attr("cy", function (d) {
      return project(d).y;
    });
}

I’ve encapsulated this in a render method, because every time our map moves (through dragging or zooming) we have to redraw the position of our circles, which we do like so:

map.on("viewreset", render);
map.on("move", render);
map.on("moveend", render);
render(); // Call once to render

That’s it! This is the basic pattern to connect d3 and mapboxgl, which you can use to draw all d3 objects.

Earthquakes!

To illustrate the above template, I’ve downloaded data on all earthquakes in 2018 with a magnitude of 5 or greater from the USGS Earthquake Harads Program API. You can find the data here.

The map is on the top of the page.

You can find the full code for the earthquakes map and all other code in this Github repository.

That’s it for now! I might add more d3/mapboxgl tutorials on paths and animations, so please leave a comment if you found this tutorial useful.