Making a Map in D3.js v.5

A pretty specific title, huh? The versioning is key in this map-making how-to. D3.js version 5 has gotten serious with the Promise class which resulted in some subtle syntax changes that proven big enough to cause confusion among the D3.js old dogs and the newcomers. This post guides you through creating a simple map in this specific version of the library. If you’d rather dive deeper into the art of making maps in D3 try the classic guides produced by Mike Bostock.

Our objective is to create a map centered on Spain and its neighbouring countries, and add some reasonably big cities onto it. The visualisation will be done in D3.js and the data transformation will be a joint effort between OGR2OGR and R. Prior to launching our D3 playground we need to acquire a base map and a list of locations we want to plot. Datasets obtained from external sources are rarely “plot-ready” and require to be transformed to the right format before use. I’ve collected these under the “preparation tasks” section in this guide. Once we have the data groomed and ready, and a good idea of what we want to visualise, the script is rather straightforward.

Preparation Tasks

#1 As a step 1. I recommend drawing the visualisation you have in mind on a piece of paper. This will help you make the design decisions going forward. 

#2 Download the map that will serve as your base image.
I used a map of world administrative boundaries in 1:10M scale obtained from Eurostat. You can check whether the map matches your expectations by uploading it to MapShaper.org.
Hovering over a country with the ‘i‘ sign activated will give you information associated with each shape, including the ID you can reference that country by (useful later).

#3 Extract the countries you need for your visualisation to minimise the GeoJSON size. In my scenario I needed to visualise Portugal, Spain, Andorra, France, Switzerland, Italy, Austria, Germany, Hungary, Lichtenstein, Belgium, Luxembourg, Slovenia, Croatia, Slovakia, Bosnia and Herzegovina, Czech Republic, and on the African side Algeria, Morocco, Tunisia, and Libya.
I used OGR2OGR to extract these countries from the Eurostat dataset – it was a single line of code:

ogr2ogr -where "FID in ('PT','ES','AD','FR','CH','IT','AT','DE','HU','LI','BE','LU','SI','HR','SK','BA','CZ', 'DZ','MA','TN','LY')" countries.geojson CNTR_RG_10M_2016_4326.geojson

You can look up the FID value in the geojson file or by using the information view in MapShaper.
For installation tips and more on OGR2OGR check out my older post Extracting countries from GeoJSON with OGR2OGR. Alternatively you can use Python, R, or even do it by hand.

#4 Tip: perform any dataset transformations or corrections beforehand – it’s often easier to do these outside the D3 realm. I’d advise to use D3 strictly to do data visualisation tasks (plotting, movement, actions).
If you need to reproject your data, you might find my post Changing dataset projection with OGR2OGR useful.

#5 Get your locations ready.
Put your locations in a csv file and make sure you have these 3 base columns: name, latitude, and longitude. 
If you’re planning to populate the map with more than a handful of locations, and your points are missing their geocoordinates, you can look them up using one of the geocoding tools or APIs on the web, for example Map Developers. In case you want to generate a set of locations from scratch, you can use Geonames. Geonames is a geographical database with more than 10M locations, freely distributed under a Creative Commons licence. You’d extract from it the locations that make sense for your visualisation – e.g. by keeping only the highly populated cities. For a guide on how to extract a reasonable set of locations from Geonames take a look at my introduction to Point-in-Polygon in R. I’ll use that dataset in this analysis.

The map

#1 Create an HTML layout: load the D3 v5 library, point to your CSS file, scale SVG to fit your website. All the usual.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My map</title>
<script type="text/javascript" src="https://d3js.org/d3.v5.min.js"></script>
<link rel="stylesheet" type="text/css" href="map.css">
<style></style>
</head>
<body>
    <div id="container" class="svg-container"></div>
    <script type="text/javascript">
    var w = 1400;
    var h = 700;
    var svg = d3.select("div#container").append("svg").attr("preserveAspectRatio", "xMinYMin meet")
    .attr("viewBox", "0 0 " + w + " " + h).style("background","#c9e8fd")
    .classed("svg-content", true);

#2 Define your map: choose your projection, specify the center, scale, and zoom. I find D3 in Depth’s Geographic Projection Explorer incredibly useful in preparing the map definition. Check out their documentation on D3’s approach to rendering geographic information, too.

var projection = d3.geoMercator().translate([w/2, h/2]).scale(2200).center([0,40]);
var path = d3.geoPath().projection(projection);

#3 Point to the datasets:

var worldmap = d3.json("countries.geojson");
var cities = d3.csv("cities.csv");

#4 Finally we can draw the map. I’m going to load both data sets in my promise syntax, then reference each by its index number. We’ll iterate through the data sets with the clever Promise.all method. Check out my guide to promises in D3.js v.5 if you’re unfamiliar with this new syntax.

Promise.all([worldmap, cities]).then(function(values){    
// draw map
    svg.selectAll("path")
        .data(values[0].features)
        .enter()
        .append("path")
        .attr("class","continent")
        .attr("d", path),
// draw points
    svg.selectAll("circle")
        .data(values[1])
        .enter()
        .append("circle")
        .attr("class","circles")
        .attr("cx", function(d) {return projection([d.Longitude, d.Lattitude])[0];})
        .attr("cy", function(d) {return projection([d.Longitude, d.Lattitude])[1];})
        .attr("r", "1px"),
// add labels
    svg.selectAll("text")
        .data(values[1])
        .enter()
        .append("text")
        .text(function(d) {
            return d.City;
            })
        .attr("x", function(d) {return projection([d.Longitude, d.Lattitude])[0] + 5;})
        .attr("y", function(d) {return projection([d.Longitude, d.Lattitude])[1] + 15;})
        .attr("class","labels");
});

Let’s review what’s happening in the code block by block. Promise.all allows for resolving values from multiple promises at once – in our case the countries geojson and the cities csv file. The Draw Map section plots the base map. In the data call we reference the first value returned by the promise – so the map.csv. The Draw Points section puts our cities on the map – this time by referencing the second data set. The cx and cy coordinates ask for latitude and longitude information and act on one or another (index 0 or 1). Finally, the Add Labels section prints the city names and places them so that they don’t overlap the points. 

#5 Adjust the CSS. This is how my map.css looks like:

.continent {
    fill: #f0e4dd;
    stroke: #e0cabc;
    stroke-width: 0.5;
}

.circles {
    fill: #3c373d;
}

.labels {
    font-family: sans-serif;
    font-size: 11px;
    fill: #3c373d;
}

The full code looks like this:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My map</title>
<script type="text/javascript" src="https://d3js.org/d3.v5.min.js"></script>
<link rel="stylesheet" type="text/css" href="map.css">
<style></style>
</head>
<body>
    <div id="container" class="svg-container"></div>
    <script type="text/javascript">
    var w = 1400;
    var h = 700;
    var svg = d3.select("div#container").append("svg").attr("preserveAspectRatio", "xMinYMin meet").style("background-color","#c9e8fd")
    .attr("viewBox", "0 0 " + w + " " + h)
    .classed("svg-content", true);
    var projection = d3.geoMercator().translate([w/2, h/2]).scale(2200).center([0,40]);
    var path = d3.geoPath().projection(projection);
       
  // load data  
var worldmap = d3.json("countries.geojson");
var cities = d3.csv("cities.csv");
   
Promise.all([worldmap, cities]).then(function(values){    
 // draw map
    svg.selectAll("path")
        .data(values[0].features)
        .enter()
        .append("path")
        .attr("class","continent")
        .attr("d", path),
 // draw points
    svg.selectAll("circle")
        .data(values[1])
        .enter()
        .append("circle")
        .attr("class","circles")
        .attr("cx", function(d) {return projection([d.Longitude, d.Lattitude])[0];})
        .attr("cy", function(d) {return projection([d.Longitude, d.Lattitude])[1];})
        .attr("r", "1px"),
 // add labels
    svg.selectAll("text")
        .data(values[1])
        .enter()
        .append("text")
        .text(function(d) {
                    return d.City;
               })
        .attr("x", function(d) {return projection([d.Longitude, d.Lattitude])[0] + 5;})
        .attr("y", function(d) {return projection([d.Longitude, d.Lattitude])[1] + 15;})
        .attr("class","labels");
       
    });
   
</script>
</body>
</html>

Download my files (including the map.html, map.css, countries.geojson, and cities.csv) if you want to try it out on your environment.

Here’s the final visualisation – unfortunately pasted as an image as I haven’t figured out yet how to embed D3js scripts in the new Gutenberg WordPress editor…

Thanks for looking! Let me know in the comments whether you’d improve something or in case you’d spotted an error. Remember to follow my adventures in the data wonderland on Twitter:

Leave a Reply