Building a map; one step at a time.

1. Background

This little tutorial assumes that you are familiar with D3 and its general working. If that is not the case, don’t worry! I’m going to outline in a very intuitive way what is going on; at any rate, I’ll be uploading in the page new resources for you to increase your D3 knowledge. Then, let’s start, shall we?

2. Attack plan

As I mentioned at the start of this series, cartography is a very extensive subject; and, therefore, it is necessary to take into account several D3 objects, as well as to make appropriate use of them. Do not despair! nevertheless; let us make and overview of the concepts and steps we’ll take in order to tackle this subject.

  1. Projections and the D3 projection object
  2. How to obtain and use cartographic information in D3
  3. The path object of D3
  4. Roll up your sleeves! Building the map

Very well, then; time to attend to each point.

3. Working with projections

3.1 From a sphere to a plane…

A recurrent problem that cartographers have had to cope up with, is the fact that Earth in not flat (I’m sorry to break your bubble if you are one of those poor lost souls who have put their faith in flat-Earth theory). This implies that we cannot directly go from a tridimensional sphere (or rather an ovoid, to be more precise) to a bi-dimensional plane. What can we do, then? Projections to the rescue!

What a projection attempts, is to transform a spherical polygonal geometry to a plane polygonal geometry. In this context, a polygon is a geographic region, for example, a country. This polygonal transformation is performed through complex mathematical calculations; fortunately for us, all this is done by D3.

To understand the projection process in a more graphic way, imagine a Jack-o’-lantern. Each shape carved on the pumpkin is a polygon; for example, if you carve two eyes, a nose, and a mouth, we would have four polygons. Once the Jack-o’-lantern is lit, you’ll see how the polygons carved on its surface are projected over the walls. This time, instead of a Jack-o’-lantern, imagine the planet Earth. In this analogy, each figure on the pumpkin could be a country, an island, a continent, etc.

Now, the projection of the polygons from our Jack-o’-lantern-Earth, is not the same on the walls as compared with the roof or the floor. In the same way, the projection would change if we place the Jack-o’-lantern-Earth inside a cylinder or over a cone. For these reasons, different projections render maps with different shapes; some projections are better than others are for certain tasks; for example, the conic projections are frequently used to create maps of specific countries. On the other hand, the cylindrical projections, as the Mercator one, are very useful to display maps of the entire world; however, they distort too much the size of the geographic regions as these go further away from the equator.

By the way, if you want to know more about projections, the USGS offers a thorough take on it.

3.2 And… How to do it in D3?

Lucky for us, D3 already offers a great variety of projections, which, practically, cover all the possible cases you could think of. It is even possible four you to create your own costume projections; although, I must warn you, this would be a fairly complicated endeavor.

Creating a projection object in D3 is pretty simple; it suffices to call the appropriate function. For example, in order to create a Mercator projection, you’d call the d3.geoMercator() function; on the other hand, if you wish to use a conical projections, the d3.geoConicConformal() function would be the one to use. These were just some examples; D3 has an ample array of options as for projections.

4. Okay, but, where does the cartographic data come from?

Up until now, we’ve seen how to make the projection of a sphere over a plane; however, we have not seen how to take concrete latitude and longitude data from actual geographic regions; and input that information to D3.

In the cartographic world, the geographic information is stored and shared through different formats, which depends on the criteria of each institution or the software being employed. In regards to D3, there exist two main formats: GeoJSON and TopoJSON. As their name indicate, they both follow the JSON notation. Since TopoJSON is a subset of GeoJSON, and, considering that, at any rate, D3 only works with GeoJSON directly; I’ll focus on the latter.

Now, where can you obtain information in the GeoJSON format? You see, there are many sources on the internet; for instance, in geojson-maps.ash.ms you can retrieve information from diverse regions of the world; nevertheless, if you need to get specific and detailed information for some particular region, the best sources are the registers created by the government institutions of each country. For instance, in Mexico’s case, we’d be referring to the INEGI; and, for the United States, we’d be talking about the USGS.

It is worth mentioning that, usually, the public information offered by government organizations is not in the GeoJSON format; therefore, we must perform a formats translation. I’m not going to go deeper on the details of how to achieve this translation, since there exist many free tools online that do this job; they sometimes even offer capabilities to simplify the cartographic information, which is very useful at times when there is much more data than we need for our purposes. One of my favorites tools for these kind of tasks is mapshaper.org

5. The path object: drawing the cartographic information on a canvas

The final ingredient to build our map is the path object, which we can create using the d3.geoPath(projection) function. In this case, the projection parameter refers precisely to the projection object we have already discussed about. Depending on the type of projection you want to employ, will be the projection object you will input to this function.

To use the path object, you must call it as a function (remember, in JavaScript, a function is also an object; which, in turn, can have properties and methods). This would be the way to call it: path(geoJsonObject); where the geoJsonObject parameter is precisely the object built under the GeoJSON standard.

What the path object does, is to generate a text string in which there are instructions that indicate how the map must be drawn. These instructions are inputted into the “d” attribute of the <path> element. Watch out! Do not confuse the path object with the <path> element; the former is a JavaScript object, while the latter is an HTML element.

As an example, to create and use a path object, we could employ the following code:

	var path = d3.geoPath(projection);
	var pathString = path(geoJsonObject); /*This text string is the one 
        we can use for the “d" attribute of the <path> element*/

6. Can we create the map yet?

After all the background I have given you, you may be eager to actually build a map using D3. Fine, then; I will not make you wait any longer. Actually, we are going to build three maps; increasing a little bit the difficulty each time. Let us begin!

6.1. A rough map

In the first place, I will present you the necessary code to create a very simple map.

<svg id="map-demo" height="250" width="400"></svg>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
    var geoJsonGeometry = {"type":"LineString",
            "coordinates":[
                [-109.952621, 22.873550], [-117.152752, 32.511857],
                [-106.389995, 31.729324], [-103.188676, 28.983335],
                [-101.830050, 29.821814], [-97.103269, 25.889812],
                [-97.366612, 21.168504], [-94.409246, 18.190767],
                [-90.240085, 21.096935], [-86.818797, 21.067803],
                [-92.307206, 14.541265], [-94.745877, 16.296351],
                [-96.587050, 15.666151], [-105.705476, 20.346444],
                [-114.719173, 31.663829], [-109.396981, 23.349035]]};
    var projection = d3.geoMercator();
    var path = d3.geoPath(projection);
    var pathString = path(geoJsonGeometry);
    d3.select("svg#map-demo").append("path").attr("d", pathString);   
</script>

If you place this code directly inside an HTML document, you will be able to visualize a rough map that outlines Mexico. In fact, the result should be very alike to this one:

As you may notice, the code is rather short. Now, I am going to explain what is going on.

In the first place, we are defining a <svg> element with the id “map-demo”; said element will be the canvas where the map shall be drawn.

Subsequently, we are importing the D3 library. As of the moment of this post’s publication, the latest D3 version is five.

The rest of the code is embedded inside a <script> element; therefore, from this point on, we will be executing JavaScript instructions.

At the first line of JavaScript, we are assigning an object with the GeoJSON format to the geoJsonGeometry variable. In the mentioned object, we have the entire geographic information required to delineate the map. The geoJsonGeometry object has two properties. The first property, called type, signals the type of object we are building; in this case, it is a LineString object. The second property, called coordinates, is an array of coordinates; each coordinate, in turn, is an array with two values: longitude and latitude (in that order). For this simple example, we are using 16 points that outline Mexico; since the number of points is fairly small, the resultant map is rather harsh; however, my objective is that you can appreciate what is happening.

Next, we are creating a projection object; in this case, the chosen projection type is Mercator; although, it could be any other one you would like.

Afterwards, we are defining the path object; for this, we are using the projection object that was created in the former step.

The next step is to create the pathString variable; this is, a text string with the instructions needed to draw a map. With this aim in mind, we call the path object as a function; feeding it with the previously defined GeoJSON object, which we assigned to the geoJsonGeometry variable.

The final JavaScript line, actually stands for several concatenated actions. The first one: to select the <svg> element identified as “map-demo”; next, to said element we add a <path> element; finally, we assign the pathString variable to the “d” attribute of the refered <path> element. Remember, this variable contains all the instructions needed to draw our map.

6.2. A simple map

Okay, the former example taught us the general mechanism to draw a map; however, it is time to create a map with a truly professional appearance. For this, we need cartographic information of a better quality. I’ve prepared a GeoJSON file with the geographic information of the North American continent. If you wish, you may download this file; it would be a good exercise for you to see the structure of a real life GeoJSON object.

For this example, the code would look like this:

<svg id="map-demo-1" height="250" width="400"></svg>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
    var projection = d3.geoMercator();
    var path = d3.geoPath(projection);
    d3.json("http://harkthedata.com/2018/11/03/north_america.geojson").then(geoData => {
        var pathString = path(geoData);
        d3.select("svg#map-demo-1").append("path").attr("d", pathString);
    });
</script>

And the result of running this code would look as follows:

As you may notice, the code looks pretty similar to the one we saw before; but, in this case, we have retrieved the data from an external file. In fact, doing so is much more appropriate; the GeoJSON structures may get to be considerably large and it would not be practical to declare a JavaScript variable with all the geographic information.

In case you are not sufficiently familiarized with D3, allow me to outline what is occurring:

The d3.json(file) function, looks for a document with a JSON structure in the location signaled by the parameter file (remember, GeoJSON is not but an extension of JSON); if the file is found, the structure read from it will be imputed to the function fed to the method “then”. It is exactly this function where we are indicating the steps to follow in order to draw the map. As you may realize, these are the same steps that we had already followed in the previous example.

6.3. A choropleth map

To finalize, I want to show you a little bit more of the tremendous capability D3 has for map creation. Up until this point, we have only used a single <path> element to draw the visual representation of all our cartographic information; however, in fact, embedded in the GeoJSON file we have used, there is the information of 18 countries. Wouldn’t it be nice to be able to visualize each one of them? Well, for this purpose, we are going to make use of the Update Pattern; if you are not familiarized with it, suffice to say that it is a D3 methodology through which it is possible to manipulate several HTML elements simultaneously, with no need of using loops.

The code that will work the magic is the one that follows:

<svg id="map-demo-2" height="250" width="400"></svg>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
    var projection = d3.geoMercator();
    var path = d3.geoPath(projection);
    var colorScale = d3.scaleSequential(d3.interpolateRainbow);
    d3.json("http://harkthedata.com/2018/11/03/north_america.geojson").then(geoData => {
        d3.select("svg#map-demo-2").selectAll("path").data(geoData.features)
	        .enter().append("path") /*For each country, a <path> element is added. 
		At the end, 18 of this elements will have been added*/
                    .attr("d", path) /*The path object will be called as a function for 
	            each GeoJSON object, this is, for each country. The resultant text string shall 
		    be assigned to the “d”  attribute of each <path>*/
                    .attr("fill", (d, i) => colorScale(i / 17)); /*For each <path> 
		    element, i.e., each country, the "fill" parameter is assigned, wich 
		    allows us to set the color of every one of them. The “i” parameter is 
		    the index of each country inside the array*/
    });
</script>

The result of running this code should look as the next:

Surely, many sections of the code may look familiar to you now. Among the changes, you may notice the definition of the variable colorScale; this object can be called as a function. If we feed this function with a value between 0 and 1, it returns a color; or, to be more accurate, a text string with the hexadecimal representation of a color.

Unlike the previous two examples, what we are doing in this case is to create a <path> element for each country. The JSON object contained in the north_america.geojson file contains a parameter called “features”; this parameter is an array, which, in turn, contains 18 GeoJSON objects: one per country and every one with the necessary information to draw the shape of each country.

In the block of code previously presented, I have included comments in several lines for you to know what these lines are doing; only remember: the path object and the <path> element are two different things.

7. Conclusions

What do you think? As you can now appreciate, D3 offers a superb array of tools!

I hope you agree with me: the code needed to create a map in D3 is really short and straightforward; however, with the purpose of getting the most out of this library, we may need to dive deeper into some concepts related to the structure of the geographical data; as well as the way D3 operates. I think there are two subjects worth studying further in order to create much more effective geo-visualizations:

  1. Details of the GeoJSON objects structure
  2. Mechanics of the D3 Update Pattern working

I’ve already created links where you’ll be able to further study these subjects. Don’t be shy; go, visit them!