Overlaying geo data with Leaflet Version 1.3 and D3.js Version 4

In this post I describe how you can overlay Geo Data onto a leaflet map with D3.js.

Combining Leaflet and D3 and objectives

There are a number of tutorials online on how to overlay Geodata with D3.js. However, I couldn’t find any of these that worked for the most recent version of D3.js and Leaflet. They were all fundamentally broken. This post goes into the details of overlaying simple circles on a leaflet map.

Here I am using the latest (as of writing) versions of Leaflet and D3.js. These are V1.3 and V4.13.0 for leaflet and D3.js respectively.

Setting up Leaflet

First I am going to set up Leaflet and set some defaults up. Once this is done I will be able to set up D3 and start overlaying data.

First Leaflet needs to be imported and the CSS needs to be loaded. Here I have used cdnjs to load the library but this can be a local copy if preferred.

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.3.1/leaflet.css"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.3.1/leaflet.js"></script>

To draw a leaflet map you must first create an element to draw into. In the body of my HTML I create a div to hold the data and style its width and height.

<div id="map" style="width: 600px; height: 600px"></div>

Once I have created the map div I can start creating the map using JavaScript.

var map = L.map('map').setView([51.505548, -0.075316], 16);
mapLink = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);

Above I create a map centering on the coordinates 51.505548, -0.075316. In the second line I add a tile layer using the open street map tiling and add it to the map. These two lines then create a scrollable map on the page in the map div.

Now I have my leaflet scrollable map and can start to add geo data to it.

Setting up D3 and the SVG layer to draw to

Now I have my leaflet map I can start to add D3 and elements to the map.

In the head element I need to include the d3 source so I add the following line to the page.

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>

D3.js operates on a SVG element so I need to add a SVG to the Leaflet map. Using Leaflet I ask it to create a SVG element and add it to the map. This can be done with the following code.

var svgLayer = L.svg();
svgLayer.addTo(map);

By default Leaflet adds an initial group element inside this SVG so I can access both the SVG and the group using the following D3.js.

var svg = d3.select("#map").select("svg");
var g = svg.select('g');

Now I have access to the SVG and the group I can start to work with the data.

Loading data and Drawing to the SVG

For this demonstration, I have a json file with some coordinates that I am going to highlight on the map.

[
  {"coordinate":[51.506532, -0.074624]},
  {"coordinate":[51.504452, -0.076028]},
  {"coordinate":[51.505548, -0.075316]},
  {"coordinate":[51.508109, -0.076061]},
  {"coordinate":[51.506019, -0.073639]}
]

These could have names and more information to display on the map but for this example I am going o merely circle the locations.

To load this data I am going to use the D3.js loading functionality.

d3.json("london.json", function(pointsOfInterest) {
    //code here
});

Here this will load london.json asynchronously and once it has loaded it will call the callback. The contents of the file will be json decoded and stored in the variable pointsOfInterest. The following code lives in this callback.

Leaflet uses a custom object to hold latitude and longitude data, L.LatLng. So once my data has loaded I add this data to the pointsOfInterest object.

pointsOfInterest.forEach(function(d) {
    d.latLong = new L.LatLng(d.coordinate[0], d.coordinate[1]);
});

Once I have this in a form Leaflet will understand I start by creating the circles using D3.js.

var feature = g.selectAll("circle")
    .data(pointsOfInterest)
    .enter().append("circle")
    .style("stroke", "black")
    .style("opacity", .4)
    .style("fill", "blue")
    .attr("r", 20);

Here I take the data from my pointsOfInterest object and create a circle for each data item. At the moment this is just adding the elements to the group inside the SVG. This is not yet placing it on the correct place on the map.

Once I have the circles created I create a function that will place them in the correct location.

function drawAndUpdateCircles() {
    feature.attr("transform",
        function(d) {
            var layerPoint = map.latLngToLayerPoint(d.latLong);
            return "translate("+ layerPoint.x +","+ layerPoint.y +")";
        }
    )
}

This code adds the transform attribute to each of the circles. This is used to move the circle in the correct place on the map. However before the circle can be overlaid I need to work out where on the screen it needs to be placed.

I have stored where to store the circles using latitude and longitude values. This needs to be transformed into a pixel location on screen. Leaflet provides a simple way of doing this with map.latLngToLayerPoint(value). This returns an object with two properties, x and y. I then use these values to translate the location of the circles to the correct place on the map.

The final piece of code draws the shapes on the map and attaches the above function to the moveend event.

drawAndUpdateCircles();
map.on("moveend", drawAndUpdateCircles);

This means that every time that the map is moved or zoomed the location of the circles will be recalculated. Note this has changed compared to previous versions of Leaflet that required you to bind to the viewreset event.

Summary

Now I have created a Leaflet map and initialized the location. Once I had done this I initialised a SVG layer with Leaflet and targetted this with D3.js. Using D3.js I loaded json holding coordinate data and drew circles on the SVG. The final step was to create an update function which placed the circles on the correct location on the maps.

The full code for this is available on my website which includes a live example.

If you have any questions feel free to comment on the following blog post.

2 Comments

Add a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.