D3 for Maps
In many ways, a map is just another data-driven graphic. So, of course D3 can also be used to create
maps. We will use a geosjon file to load a map of the same Overijssel municipalities we
have used above. The data was created using the export to geojson option in QGIS, such
possibilities are available in most GIS software, or from web services such as the one at www.mapshaper.org

Visualising Geographic Data
The map is made using the code in listing 13 below. If you study the code to see how the map was
made, you can see we are using techniques very similar to the BarChart examples.
You will see only a few D3 functions and methods that we did not use before:
First of all a
projection object, in lines 27-31. D3 has a lot of functionality to use
geographic data, i.e. data that is expressed as coordinate pairs in longitude and latitude.
Here we use the d3.geoMercator() function that is a so-called factory
to turn lon-lat coordinate pairs into cartesian screen coordinates, using the Mercator
projection mathematics. To specify the parameters, here we use the .center, .scale and
.translate methods of this projection object to zoom into the Overijssel area. There
are a multitude of available projections, you can look for examples and explanation at the D3
website.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <script src="lib/d3.js"></script> <!--CSS styling for map elements:--> <style> .mapSVG { background-color: rgb(250, 250, 250); } .municipality { fill: rgb(255, 240, 214); stroke: rgb(255, 159, 227); } </style> </head> <body> <svg class="mapSVG"></svg> <script type="text/javascript"> const mapWidth = 400; const mapHeight = 350; let mapSVG = d3.select("svg") .attr("width", mapWidth) .attr("height", mapHeight) ; // Define Mercator proj at data center (lon-lat): const myProj = d3.geoMercator() .center([6.0, 52.5]) .scale(10000) .translate([mapWidth / 2, mapHeight / 2]) ; //Define svg path generator using the projection: const svgpath = d3.geoPath().projection(myProj); // asynchronously load geojson using a Promise: d3.json("data/overijssel.json").then( // this function is run when the Promise is fulfilled: function(geojson) { mapSVG.selectAll("path") .data(geojson.features).enter().append("path") // for each d create an svgpath that uses the geoPath generator: .attr("d", svgpath) // give it a class, so it's styled by the CSS: .attr("class", "municipality") ; } //end of callback function .then() ); //end of d3.json </script> </body> </html> |
The projection factory is used in turn in another very useful factory method called
d3.geoPath().
This code, seen in line 33, takes a collection coordinates and transforms
them into an SVG drawing path that creates an SVG object in the HTML DOM (which then is drawn on
the webpage). The geoPath() factory is finally employed in line 41, in the
statement .attr("d", svgpath) to load, project and draw the coordinates of each
municipality from the dataset. Note that the second argument of the .attr() is the svgpath
object, which in fact is a function (remember: in javascript everything is an object)...!
The dataset is loaded in line 35, using the function d3.json(). This works
exactly as the d3.csv() we used earlier, but now for JSON or geoJSON data. The latter
is a special version of the general JavaScript Object Notation (JSON) format. The format is
standardised (see geojson.org), but the standard
allows for several ways to structure the data, therefore it is sometimes tricky to find the proper
objects to address. In the case of our Overijssel data this looks as follows:
|
1 2 3 4 5 6 7 8 |
{"type":"FeatureCollection", "features": [ { "type":"Feature", "properties": {"code":"GM0141","name":"Almelo","population":72730, dotdotdot }, "geometry": {"type":"Polygon","coordinates":[[[6.6986,52.3937],[6.6978,52.3912], dotdotdot |
FeatureCollection, that has a sub-object with the name features.
The value of features is a JSON array, and each element of the array is a Feature
object which in turn will have properties, which store the data attributes of the features. In GeoJSON,
each feature also has a geometry object, which can be of type Polygon, Line, or Point.
The array of features is similar to the array of data objects we used in the CSV solution, therefore
to bind to it we use the line 39: .data(geojson.features).enter().append("path"). The
d3.geoPath() function is smart enough to figure out how to extract the needed geometry
objects from this array and apply them to create an SVG path...!
To get to the other elements of the data, you can address sub-objects of the features
elements, for example you cans use d.properties.population to retrieve the number of
inhabitants.
If you use the code above and test it in your browser, you will notice that the map does not really fit nicely in the SVG element (the grey rectangle). Experiment with the projection settings to make the map fit better (as in the figure above).
Create a Thematic Map
Just like in the bar chart example, you can of course use the attribute properties in the data files to create data-driven visualisations using the SVG drawing possibilities. For example, you can create an extra layer on top of the municipalities to draw circles of different sizes, depending on a data property. Thus, you would create a so-called proportional point symbol map, as shown in the Figure below.

You have already used almost all the elements and code parts necessary for you to create such a proportional point map yourself.
There is one exception: To place the points, you need an appropriate point geometry, preferably automatically
placed inside the municipality polygons. You can, of course, use a GIS software to create such a geometry and save that as
GeoJSON data. But this is not necessary, because the D3 library has several useful functions that operate on geometry paths
(see d3-geo documentation).
The one to use here is path.centroid(), which "returns the projected planar centroid (typically
in pixels) for the specified GeoJSON object." This is returned in an array of two coordinates
[x,y]. Thus, to retrieve the X-coordinate of the centroid of a municipality, you would use
svgpath.centroid(d)[0]...
Create a proportional point symbol map of the number of inhabitants per munipality of Overijssel. Think carefully about the several code parts needed to achieve that, and look at earlier pages to check how you achieved that. To refine your visualisation, experiment with the CSS settings and with the way you calculate the circle radii from the data values. If you are not succesful in creating a fully working map, the solution will be shown below in due time...

