Getting Started with D3.js Maps
By: Angelica Lo Duca
Tags: Data Visualisation, JavaScript, Dataviz, Data Science, D3js

Data Visualisation

Getting Started with D3.js Maps

Image by Author

Many Javascript libraries exist to build and animate maps, such as Leaflet.js and Highcharts. In this article I exploit the very famous Data Driven Documents (D3) library (version 5), which is more than a simple graph library.

D3 is a Javascript library which permits to manipulate documents, based on data.

In this tutorial I will build a choropleth map which shows the population of each country of the world. I have modified the original code, by adapting it to D3 v5 and enriching it with interactivity and annotations.

The full code can be downloaded from my Github Repository.

Setup

Firstly, I build a basic HTML page, which also includes the D3 javascript:

D3 draws a graph or a map within a SVG object (or a canvas). Thus, I must create a SVG object within my HTML document and then include the Javascript file ( map.js ), which will contain the code to build the map.

<body>

<svg id="map"></svg>

<script src="map.js"></script>

</body>

map.js

I define the size and the margins of the SVG image which will contain the map:

var margin = {top: 20, right: 10, bottom: 40, left: 100},

width = 600 - margin.left - margin.right,

height = 400 - margin.top - margin.bottom;

and I retrieve the SVG object from the HTML, I set its height and the width through the attr() function applied to the SVG object. Then I append a g (Group) object to the SVG object and I apply a transformation, which translate the g element from the left and from the top, in order to respect margins.

// The svg

var svg = d3.select("svg")

.attr("width", width + margin.left + margin.right)

.attr("height", height + margin.top + margin.bottom)

.append("g")

.attr("transform",

"translate(" + margin.left + "," + margin.top + ")");

Now I build a projection. A D3 projection is a transformer which converts geographical coordinates (in the form of longitude, latitude) to pixels, in order to be represented within a SVG image. Many projections exist in D3. In this example, I exploit the geoMercator() projection. I set the scale of the map and the center, then I translate it to the mid of the SVG object.

var projection = d3.geoMercator()

.scale(70)

.center([0,20])

.translate([width / 2 - margin.left, height / 2]);

I define the color scale as a d3.scaleThreshold() where the domain ranges from 100 M to 500 M and the range from dark pink to light pink.

var domain = [100000000, 500000000]

var labels = ["< 100 M", "100 M - 500 M", "> 500 M"]

var range = ["#F8CAEE","#BF76AF","#852170"]

var colorScale = d3.scaleThreshold()

.domain(domain)

.range(range);

1. Data Download

D3 does not support embedded maps, thus the map shape must be downloaded from a different sources as a GeoJson object. In this specific example, I download data from two sources: the first source contains the map as a GeoJSON and the second one the population as a CSV. The population file looks like the following snapshot:

Image by Author

For each country of the world, its code and population are given. The code is the same as that provided in the GeoJson map.

I exploit the Javascript concept of Promises, to wait until all the data are available.

var promises = []

var data = d3.map();

promises.push(d3.csv(" promises.push(d3.json(" https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson "))promises.push(d3.csv(" https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world_population.csv ", function(d) { data.set(d.code, +d.pop); })) myDataPromises = Promise.all(promises).then(function(my_data) {



var topo = my_data[0]

// do some stuff });

Note that I store the content of the CSV file into an external variable, called data . The map is available as first element of the my_data array passed as input of the anonymous function within the promise.

2. Draw the map

Now I’m ready to draw the map. The raw map can be drawn from the GeoJson, which contains the lines to be built as SVG paths. Thus, for each feature of the GeoJSON I build a line and I apply it the projection as a d attribute. I color each country through the fill attribute with the color scale applied to the population associated to that country. I also set the mouseover and mouseleave events, which will be used for map interactivity.

svg.append("g")

.selectAll("path")



.data(topo.features)

.enter()

.append("path")

.attr("class", "topo")

// draw each country

.attr("d", d3.geoPath()

.projection(projection)

)

// set the color of each country

.attr("fill", function (d) {

d.total = data.get(d.id) || 0;

return colorScale(d.total);

})

.style("opacity", .7)

.on("mouseover", mouseOver )

.on("mouseleave", mouseLeave )

3. Map interactivity

I define two functions mouseOver() and mouseLeave() which will act as event listeners. The first function is invoked when the user goes with the mouse over a country. As a result, the borders of the country are highlighted, as well as the population is shown as a tooltip.

In order to manage the tooltip, I define an additional div in the original HTML file:

<body>

<div id="tooltip"></div>

<svg id="map"></svg>

<script src="map.js"></script>

</body>

At the beginning of the map.js file, I also build the tooltip, with opacity = 0 , which means that the tooltip is initially hidden.

The mouseOver() function sets the opacity attribute to 1 and highlights the borders.

let mouseOver = function(d) {



d3.select(this)

.transition()

.duration(200)

.style("opacity", 1)

.style("stroke", "black")

In addition, it sets the tooltip opacity to 1. In this way, the tooltip is shown to the user. The tooltip is positioned near the mouse: the mouse coordinates are taken from d3.event.PageX and d3.event.PageY . The text of the tooltip is set to the value of the population.

d.total = data.get(d.id) || 0;



tooltip

.style("opacity", 0.8)

.html(d.id + ": " + d3.format(",.2r")(d.total))

.style("left", (d3.event.pageX) + "px")

.style("top", (d3.event.pageY - 28) + "px");

The mouseLeave() function performs the inverse operations: it sets the country borders to transparent and the tooltip opacity to 0.

d3.selectAll(".topo")

.transition()

.duration(200)

.style("stroke", "transparent")



tooltip

.style("opacity", 0)

4. Legend

Near the map I draw a legend. Firstly, I set the legend coordinates and I append a g element to the basic SVG element. I translate the g element of the defined coordinates:

var legend_x = width - margin.left

var legend_y = height - 30



svg.append("g")

.attr("class", "legend")

.attr("transform", "translate(" + legend_x + "," + legend_y+")");

Now I create a d3.legendColor() object, with the previous defined colorScale and I draw it through the call() function.

var legend = d3.legendColor()

.labels(labels)

.title("Population")

.scale(colorScale)





svg.select(".legend")

.call(legend);

5. Annotations

Finally, I enrich the map with some annotations. I exploit the d3-annotation library, which must be included in the header of my HTML file:

At the end of map.js I can draw the annotations. I must specify the note to be added, the font color ( color ) and the coordinates ( x and y ).

const annotations = [

{

note: {

label: "despite its great territorial extension Australia has only 20 million inhabitants.",

title: "Australia Population",

wrap: 150, // try something smaller to see text split in several lines

padding: 10 // More = text lower



},

color: ["#852170"],

x: projection([150.916672,-31.083332])[0],

y: projection([150.916672,-31.083332])[1],

dy: -30,

dx: 10

}

]

Now I can draw the annotation, by appending it to the SVG element:

// Add annotation to the chart

const makeAnnotations = d3.annotation()

.annotations(annotations) svg.append("g")

.style("opacity", 1)

.attr("id", "annotation")

.call(makeAnnotations) })

Summary

In this tutorial, I have illustrated how to build a D3 choropleth map enriched with interactions and annotations.

D3.js does not support maps natively. However, maps can be downloaded as GeoJSON, which can be added to SVG objects.

If you wanted to be updated on my research and other activities, you can follow me on Twitter, Youtube and and Github.

Related Articles