D3 overlays and Mapbox GL
Last updated
Last updated
Website
Ordnance SurveyData
OS Data HubIn this tutorial, we'll learn how to use D3.js to add an overlay of geographic features to an interactive Mapbox GL map. Our goal is to create a smooth-panning and zooming user experience, and to gain access to the amazing capabilities of D3.
We'll use the OS Vector Tile and Features APIs, as well as D3.js, Mapbox GL JS and Turf.js.
"Choose the right tool for the job" - a common sense saying that applies just as well to web development as it does to home improvement.
In this tutorial, we'll learn how to use D3.js to add an overlay of geographic features to an interactive Mapbox GL map. Our goal is to create a smooth-panning and zooming user experience, and to gain access to the amazing capabilities of D3.
Our index.html
file for this project is very, very simple: we load libraries and place a single <div id="map">
element within the <body>
tag. All other elements will be added programmatically in js/tutorial.js
.
With Mapbox GL JS, first we'll create a basemap with vector tiles served from the OS Vector Tile API. Since our focus is on the D3 overlay, we won't go into this code in depth - you can find it on our Examples page.
One adjustment though: we want our basemap in greyscale, so we can distinguish the overlaid features more easily. The ability to customise style is a major advantage of vector tiles. So, when we instantiate a new mapboxgl.Map
object, we include a custom style:
With the basemap created, we are ready to start working on our overlay.
Our goal is to add a D3 overlay to our Mapbox basemap, placing SVG elements representing geographic features at their appropriate positions. This is a subtle trick - the user shouldn't be able to recognise the difference between the layers. But - programmatically - the SVG elements will have access to some of the functionality D3 makes available, including mouse events, animations and so on.
The first step is to append an <svg>
element to our #map
division. This SVG element will hold all of the features we overlay.
We start by getting the dimensions of the #map
element's bounding client rectangle and appending the <svg>
element. It is important to assign the result of this operation to a variable - svg
- as we will be interacting with it shortly. We'll also create new selections for the two layers we are going to overlay, a polygon representing the borough of Camden and points representing rail and tube stations.
D3 is a powerful library for creating interactive geographic visualisation. This tutorial won't go into how it works in depth - but to draw the features in the SVG element we just created we will need a few tools from the D3 toolkit: d3.geoTransform
and d3.geoPath
.
We'll use d3.geoTransform
to transform longitude and latitude decimal coordinates into the x and y pixel values on the screen. We do this by defining a function, projectPoint
, which accepts lon
(longitude) and lat
(latitude) as input parameters and returns x
and y
values. Note that the output values will depend on the position of the map in the viewport - which makes sense, as we want to transform the geographic coordinate pairs into the correct pixel coordinates based on the map's current placement.
d3.geoPath
is a geographic path generator. Geometries for SVG <path>
elements are defined by the string assigned to the d
attribute; d3.geoPath
generates those path data strings from geographic data like points, linestrings and polygons.
update()
update
is the key to smooth interaction with our SVG overlay. The update()
function updates the <path>
elements so they appear to pan and zoom smoothly with the underlying Mapbox basemap. We will call this once the map is set up, adding the d
attribute to each path
- here we'll just make sure the function is invoked when "viewreset"
, "move"
and "moveend"
map events are fired.
We fetch data dynamically from the OS Features API to visualise in the SVG overlay we added. To do this, we need to create a valid Features API query, fetch the GeoJSON data, then visualise it appropriately.
We're only interested in fetching point data from the Zoomstack_RailwayStations
feature type that are within the boundaries of of the Borough of Camden.
We downloaded the border of the borough from Camden's Open Data website, then simplified it using the Visvalingam weighted area method on mapshaper.org. This polygon will be used in our XML spatial filter - for now we place the file (camden-simplified.json
) in the data
directory.
Once the basemap is loaded, we call an asynchronous function that loads this GeoJSON, draws the polygon as an SVG <path>
. Let's see this code before looking at fetching results from the OS Features API.
Now we construct a filter from the Camden borough geometry, and send a request to the OS Features API. The request will return a GeoJSON FeatureCollection where each Feature in the features array represents a single geometry (in this case, Point
) that matches query parameters.
Note how we call turf.flip()
on the GeoJSON feature representing the Camden polygon. This is because the OGC Web Feature Service requires coordinate pairs to be ordered as [latitude, longitude], while GeoJSON orders coordinates as [longitude, latitude]. This is a consistent bugbear of people working with spatial data - so keep your eyes open!
We don't know how many features will match our query. For this reason, we use a while
loop: while there are still results, fetch the next set of matching features. In this way we "page" through results, building a GeoJSON FeatureCollection of all the features returned by the queries as we go.
<path>
sOne of our last steps: joining the station features to our stations
selection, and specifying the attributes of each element that is created by D3. We'll set each element's class
to station
, then set fill
to a value dependent on the station type in the feature's properties
object. We'll also set event listeners to update the position and content of the div.tooltip
element we appended to the body earlier so the tooltip appears next to the station when we hover on it.
And, finally, we call update()
to update each <path>
's d
attribute based on the position and zoom level of the basemap.
And there you have it! We created a custom-styled basemap with Mapbox GL JS and the OS Vector Tile API, added an SVG overlay with D3.js, loaded spatial data from the OS Features API and visualised a polygon and a number of points on the overlay. We connected some event listeners and a dynamic tooltip.
What's more, we fused the amazing world of D3.js with the beautiful interactive vector tile maps built with the OS Vector Tile and Mapbox GL JS.
Want to see more? Check out our Data Hub APIs on the OS Data Hub.
And let us know if you build on or adapt this tutorial by tweeting at @OrdnanceSurvey and tagging #OSDeveloper.