11 min read

In this article by Thomas Newton and Oscar Villarreal, authors of the book Learning D3.js Mapping, we will cover the following topics through a series of experiments:

  • Foundation – creating your basic map
  • Experiment 1 – adjusting the bounding box
  • Experiment 2 – creating choropleths
  • Experiment 3 – adding click events to our visualization

(For more resources related to this topic, see here.)

Foundation – creating your basic map

In this section, we will walk through the basics of creating a standard map. Let’s walk through the code to get a step-by-step explanation of how to create this map.

The width and height can be anything you want. Depending on where your map will be visualized (cellphones, tablets, or desktops), you might want to consider providing a different width and height:

var height = 600;
var width = 900;

The next variable defines a projection algorithm that allows you to go from a cartographic space (latitude and longitude) to a Cartesian space (x,y)—basically a mapping of latitude and longitude to coordinates. You can think of a projection as a way to map the three-dimensional globe to a flat plane. There are many kinds of projections, but geo.mercator is normally the default value you will use:

var projection = d3.geo.mercator();
var mexico = void 0;

If you were making a map of the USA, you could use a better projection called albersUsa. This is to better position Alaska and Hawaii. By creating a geo.mercator projection, Alaska would render proportionate to its size, rivaling that of the entire US. The albersUsa projection grabs Alaska, makes it smaller, and puts it at the bottom of the visualization. The following screenshot is of geo.mercator:

 Learning D3.js Mapping

This following screenshot is of geo.albersUsa:

 Learning D3.js Mapping

The D3 library currently contains nine built-in projection algorithms. An overview of each one can be viewed at https://github.com/mbostock/d3/wiki/Geo-Projections.

Next, we will assign the projection to our geo.path function. This is a special D3 function that will map the JSON-formatted geographic data into SVG paths. The data format that the geo.path function requires is named GeoJSON:

var path = d3.geo.path().projection(projection);
var svg = d3.select("#map")
   .append("svg")
   .attr("width", width)
   .attr("height", height);

Including the dataset

The necessary data has been provided for you within the data folder with the filename geo-data.json:

d3.json('geo-data.json', function(data) {
console.log('mexico', data);

We get the data from an AJAX call.

After the data has been collected, we want to draw only those parts of the data that we are interested in. In addition, we want to automatically scale the map to fit the defined height and width of our visualization.

If you look at the console, you’ll see that “mexico” has an objects property. Nested inside the objects property is MEX_adm1. This stands for the administrative areas of Mexico. It is important to understand the geographic data you are using, because other data sources might have different names for the administrative areas property:

 Learning D3.js Mapping

Notice that the MEX_adm1 property contains a geometries array with 32 elements. Each of these elements represents a state in Mexico. Use this data to draw the D3 visualization.

var states = topojson.feature(data, data.objects.MEX_adm1);

Here, we pass all of the administrative areas to the topojson.feature function in order to extract and create an array of GeoJSON objects. The preceding states variable now contains the features property. This features array is a list of 32 GeoJSON elements, each representing the geographic boundaries of a state in Mexico. We will set an initial scale and translation to 1 and 0,0 respectively:

// Setup the scale and translate
projection.scale(1).translate([0, 0]);

This algorithm is quite useful. The bounding box is a spherical box that returns a two-dimensional array of min/max coordinates, inclusive of the geographic data passed:

var b = path.bounds(states);

To quote the D3 documentation:

“The bounding box is represented by a two-dimensional array: [[left, bottom], [right, top]], where left is the minimum longitude, bottom is the minimum latitude, right is maximum longitude, and top is the maximum latitude.”

This is very helpful if you want to programmatically set the scale and translation of the map. In this case, we want the entire country to fit in our height and width, so we determine the bounding box of every state in the country of Mexico.

The scale is calculated by taking the longest geographic edge of our bounding box and dividing it by the number of pixels of this edge in the visualization:

var s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height);

This can be calculated by first computing the scale of the width, then the scale of the height, and, finally, taking the larger of the two. All of the logic is compressed into the single line given earlier. The three steps are explained in the following image:

 Learning D3.js Mapping

The value 95 adjusts the scale, because we are giving the map a bit of a breather on the edges in order to not have the paths intersect the edges of the SVG container item, basically reducing the scale by 5 percent.

Now, we have an accurate scale of our map, given our set width and height.

var t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];

When we scale in SVG, it scales all the attributes (even x and y). In order to return the map to the center of the screen, we will use the translate function.

The translate function receives an array with two parameters: the amount to translate in x, and the amount to translate in y. We will calculate x by finding the center (topRight – topLeft)/2 and multiplying it by the scale. The result is then subtracted from the width of the SVG element.

Our y translation is calculated similarly but using the bottomRight – bottomLeft values divided by 2, multiplied by the scale, then subtracted from the height.

Finally, we will reset the projection to use our new scale and translation:

projection.scale(s).translate(t);

Here, we will create a map variable that will group all of the following SVG elements into a <g> SVG tag. This will allow us to apply styles and better contain all of the proceeding paths’ elements:

var map = svg.append(‘g’).attr(‘class’, ‘boundary’);

Finally, we are back to the classic D3 enter, update, and exit pattern. We have our data, the list of Mexico states, and we will join this data to the path SVG element:

   mexico = map.selectAll('path').data(states.features);
 
   //Enter
   mexico.enter()
       .append('path')
       .attr('d', path);

The enter section and the corresponding path functions are executed on every data element in the array. As a refresher, each element in the array represents a state in Mexico. The path function has been set up to correctly draw the outline of each state as well as scale and translate it to fit in our SVG container.

Congratulations! You have created your first map!

Experiment 1 – adjusting the bounding box

Now that we have our foundation, let’s start with our first experiment. For this experiment, we will manually zoom in to a state of Mexico using what we learned in the previous section.

For this experiment, we will modify one line of code:

var b = path.bounds(states.features[5]);

Here, we are telling the calculation to create a boundary based on the sixth element of the features array instead of every state in the country of Mexico. The boundaries data will now run through the rest of the scaling and translation algorithms to adjust the map to the one shown in the following screenshot:

 Learning D3.js Mapping

We have basically reduced the min/max of the boundary box to include the geographic coordinates for one state in Mexico (see the next screenshot), and D3 has scaled and translated this information for us automatically:

 Learning D3.js Mapping

This can be very useful in situations where you might not have the data that you need in isolation from the surrounding areas. Hence, you can always zoom in to your geography of interest and isolate it from the rest.

Experiment 2 – creating choropleths

One of the most common uses of D3.js maps is to make choropleths. This visualization gives you the ability to discern between regions, giving them a different color. Normally, this color is associated with some other value, for instance, levels of influenza or a company’s sales. Choropleths are very easy to make in D3.js. In this experiment, we will create a quick choropleth based on the index value of the state in the array of all the states.

We will only need to modify two lines of code in the update section of our D3 code. Right after the enter section, add the following two lines:

//Update
var color = d3.scale.linear().domain([0,33]).range(['red',   'yellow']);
mexico.attr('fill', function(d,i) {return color(i)});

The color variable uses another valuable D3 function named scale. Scales are extremely powerful when creating visualizations in D3; much more detail on scales can be found at https://github.com/mbostock/d3/wiki/Scales.

For now, let’s describe what this scale defines. Here, we created a new function called color. This color function looks for any number between 0 and 33 in an input domain. D3 linearly maps these input values to a color between red and yellow in the output range. D3 has included the capability to automatically map colors in a linear range to a gradient. This means that executing the new function, color, with 0 will return the color red, color(15) will return an orange color, and color(33) will return yellow.

Now, in the update section, we will set the fill property of the path to the new color function. This will provide a linear scale of colors and use the index value i to determine what color should be returned.

If the color was determined by a different value of the datum, for instance, d.sales, then you would have a choropleth where the colors actually represent sales. The preceding code should render something as follows:

Learning D3.js Mapping

Experiment 3 – adding click events to our visualization

We’ve seen how to make a map and set different colors to the different regions of this map. Next, we will add a little bit of interactivity. This will illustrate a simple reference to bind click events to maps.

First, we need a quick reference to each state in the country. To accomplish this, we will create a new function called geoID right below the mexico variable:

var height = 600;
var width = 900;
var projection = d3.geo.mercator();
var mexico = void 0;
 
var geoID = function(d) {
   return "c" + d.properties.ID_1;
};

This function takes in a state data element and generates a new selectable ID based on the ID_1 property found in the data. The ID_1 property contains a unique numeric value for every state in the array. If we insert this as an id attribute into the DOM, then we would create a quick and easy way to select each state in the country.

The following is the geoID function, creating another function called click:

var click = function(d) {
   mexico.attr('fill-opacity', 0.2); // Another update!
   d3.select('#' + geoID(d)).attr('fill-opacity', 1);
};

This method makes it easy to separate what the click is doing. The click method receives the datum and changes the fill opacity value of all the states to 0.2. This is done so that when you click on one state and then on the other, the previous state does not maintain the clicked style. Notice that the function call is iterating through all the elements of the DOM, using the D3 update pattern. After making all the states transparent, we will set a fill-opacity of 1 for the given clicked item. This removes all the transparent styling from the selected state. Notice that we are reusing the geoID function that we created earlier to quickly find the state element in the DOM.

Next, let’s update the enter method to bind our new click method to every new DOM element that enter appends:

//Enter
mexico.enter()
     .append('path')
     .attr('d', path)
     .attr('id', geoID)
     .on("click", click);

We also added an attribute called id; this inserts the results of the geoID function into the id attribute. Again, this makes it very easy to find the clicked state.

The code should produce a map as follows. Check it out and make sure that you click on any of the states. You will see its color turn a little brighter than the surrounding states.

Learning D3.js Mapping

Summary

You learned how to build many different kinds of maps that cover different kinds of needs. Choropleths and data visualizations on maps are some of the most common geographic-based data representations that you will come across.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here