Geospatial information visualization - The pillars of information visualization - D3.js in Action (2015)

D3.js in Action (2015)

Part 2. The pillars of information visualization

Chapter 7. Geospatial information visualization

This chapter covers

· Creating points and polygons from GeoJSON and TopoJSON data

· Using Mercator, Mollweide, orthographic, and satellite projections

· Advanced TopoJSON neighbor and merging functionality

· Tiled mapping using d3.geo.tile

One of the most common categories of data you’ll encounter is geospatial data. This can come in the form of administrative regions like states or counties, points that represent cities or the location of a person when making a tweet, or satellite imagery of the surface of the earth.

In the past, if you wanted to make a web map you needed a specialized library like Google Maps, Leaflet, or OpenLayers. But D3 provides enough core functionality to make any kind of map you’ve seen on the web (some examples of maps created in this chapter using D3 can be seen infigure 7.1). Because you’re already working with D3, you can make that map far more sophisticated and distinctive than the out-of-the-box maps you typically see. The major reason to continue to use a dedicated library like Google Maps API is because of the added functionality that comes from being in that ecosystem, such as Street View of Google tiles or integrated support for Fusion Tables. But if you’re not going to use the ecosystem, then it may be a smarter move to build the map with D3. You won’t have to invest in learning a different syntax and abstraction layer, and you’ll have the greater flexibility D3 mapping affords.

Figure 7.1. Mapping with D3 takes many forms and offers many options, including traditional tile-based maps (section 7.5), cutting-edge TopoJSON operations (section 7.4), globes (section 7.3.1), spatial calculations (section 7.1.4), and data-driven maps (section 7.1) using novel projections (section 7.1.3).

Because mapmaking and geographic information systems and science (known as GIS and GIScience, respectively) have been in practice for so long, well-developed methods exist for representing this kind of data. D3 has built-in robust functionality to load and display geospatial data. A related library that you’ll get to know in this chapter, TopoJSON, provides more functionality for geospatial information visualization.

In this chapter, we’ll start by making maps that combine points, lines, and polygons using data from CSV and GeoJSON formatted sources. You’ll learn how to style those maps and provide interactive zooming by revisiting d3.zoom() and exploring it in more detail. After that, we’ll look at the TopoJSON data format and its built-in functionality that uses topology, and why it provides significantly smaller data files. Finally, you’ll learn how to make maps using tiles to show terrain and satellite imagery.

7.1. Basic mapmaking

Before you explore the boundaries of mapping possibilities, you need to make a simple map. In D3, the simplest map you can make is a vector map using SVG <path> and <circle> elements to represent countries and cities. We can bring back cities.csv, which we used in chapter 2, and finally take advantage of its coordinates, but we need to look a bit further to find the data necessary to represent those countries. After we have that data, we can render it as areas, lines, or points on a map. Then we can add interactivity, such as highlighting a region when you move your mouse over it, or computing and showing its center.

Before we get started, though, let’s take a look at the CSS for this chapter.

Listing 7.1. ch7.css

path.countries {

stroke-width: 1;

stroke: black;

opacity: .5;

fill: red;

}

circle.cities {

stroke-width: 1;

stroke: black;

fill: white;

}

circle.centroid {

fill: red;

pointer-events: none;

}

rect.bbox {

fill: none;

stroke-dasharray: 5 5;

stroke: black;

stroke-width: 2;

pointer-events: none;

}

path.graticule {

fill: none;

stroke-width: 1;

stroke: black;

}

path.graticule.outline {

stroke: black;

}

7.1.1. Finding data

Making a map requires data, and you have an enormous amount of data available. Geographic data can come in several forms. If you’re familiar with GIS, then you’ll be familiar with one of the most common forms for complex geodata, the shapefile, which is a format developed by Esri and is most commonly found in desktop GIS applications. But the most human-readable form of geodata is latitude and longitude (or xy coordinates like we list in our file) when dealing with points like cities, oftentimes in a CSV. We’ll use cities.csv, shown in the following listing. This is the same CSV we measured in chapter 2 that had the locations of eight cities from around the world.

Listing 7.2. cities.csv

"label","population","country","x","y"

"San Francisco", 750000,"USA",-122,37

"Fresno", 500000,"USA",-119,36

"Lahore",12500000,"Pakistan",74,31

"Karachi",13000000,"Pakistan",67,24

"Rome",2500000,"Italy",12,41

"Naples",1000000,"Italy",14,40

"Rio",12300000,"Brazil",-43,-22

"Sao Paolo",12300000,"Brazil",-46,-23

One thing you’ll notice is that the latitudes and longitudes are imprecise. San Francisco, for instance, isn’t at 37,-122 but rather 37.783, -122.417. When you plot these cities, they’re going to look pretty off as you zoom in. Obviously, you’ll want to use more accurate coordinates for your maps, but for this example, which mostly uses maps that are zoomed way out, this should be fine.

If you only have city names or addresses and need to get latitude and longitude, you can take advantage of geocoding services that provide latitude and longitude from addresses. These exist as APIs and are available on the web for small batches. You can see an example of these services maintained by Texas A&M at http://geoservices.tamu.edu/Services/Geocode/.

When dealing with more complex geodata like shapes or lines, you’ll necessarily deal with more complex data formats. You’ll want to use GeoJSON, which has become the standard for web-mapping data.

GeoJSON

GeoJSON (geojson.org) is, like it sounds, a way of encoding geodata in JSON format. Each feature in a featureCollection is a JSON object that stores the border of the feature in a coordinates array as well as metadata about the feature in a properties hash object. For instance, if you wanted to draw a square that went around the island of Manhattan, then it would have corners at [-74.0479, 40.6829], [-74.0479, 40.8820], [-73.9067, 40.8820], and [-73.9067, 40.6829], as shown in figure 7.2. You can easily export shapefiles into GeoJSON using QGIS (a desktop GIS application;qgis.org), PostGIS (a spatial database run on Postgres; postgis.net), GDAL (a library for manipulation of geospatial data; gdal.org), and other tools and libraries.

Figure 7.2. A polygon drawn at the coordinates [-74.0479, 40.8820], [-73.9067, 40.8820], [-73.9067, 40.6829], and [-74.0479, 40.6829].

A rectangle drawn over a geographic feature like this is known as a bounding box. It’s often represented with only two coordinate pairs: the upper-left and bottom-right corners. But any polygon data, such as the irregular border of a state or coastline, can be represented by an array of coordinates like this. In the following listing, we have a fully compliant GeoJSON "FeatureCollection" with only one feature, the simplified borders of the small nation of Luxembourg.

Listing 7.3. GeoJSON example of Luxembourg

{

"type": "FeatureCollection",

"features": [

{

"type": "Feature",

"id": "LUX",

"properties": {

"name": "Luxembourg"

},

"geometry": {

"type": "Polygon",

"coordinates": [

[

[

6.043073,

50.128052

],

[

6.242751,

49.902226

],

[

6.18632,

49.463803

],

[

5.897759,

49.442667

],

[

5.674052,

49.529484

],

[

5.782417,

50.090328

],

[

6.043073,

50.128052

]

]

]

}

}

]

}

We’re not going to create our own GeoJSON in this chapter, and unless you get into serious GIS, you may never create your own GeoJSON. Instead, you can get by with downloading existing geodata, and either use it without editing it or edit it in a GIS application and export it. In our examples in this chapter, we’ll use world.geojson (available at emeeks.github.io/d3ia/world.geojson), a file that consists of the countries of the world in the same simplified, low-resolution representation that you see in listing 7.4.

Projection

Entire books have been written on creating web maps, and an entire book could be written on using D3.js for crafting maps. Because this is only one chapter, I’ll gloss over many deep issues. One of these is projection. In GIS, projection refers to the process of rendering points on a globe, like the earth, onto a flat plane, like your computer monitor. You can project geographic data in many different ways for representation on your screen, and in this chapter we’ll look at a few different methods.

To start, we’ll use one of the most common geographic projections, the Mercator projection, which is used in most web maps. It became the de facto standard because it’s the projection used by Google Maps. To use the Mercator projection, you have to include an extension of D3,d3.geo.projection.js, which you’ll want for some of the more interesting work you’ll do later in the chapter. By defining a projection, you can take advantage of d3.geo.path, which draws geoData onscreen based on your selected projection. After we’ve defined a projection and have geo.path() ready, the entire code in the following listing is all that we need to draw the map shown in figure 7.3.

Figure 7.3. A map of the world using the default settings for D3’s Mercator projection. You can see most of the Western Hemisphere and some of Europe and Africa, but the rest of the world is rendered out of sight.

Listing 7.4. Initial mapping function

Why do you only see part of the world in figure 7.3? Because the default settings of the Mercator projection show only part of the world in your SVG canvas. Each projection has a .translate() and .scale() that follow the syntax of the transform convention in SVG, but have different effects with different projections.

scale

You have to do some tricks to set the right scale for certain projects. For instance, with our Mercator projection if we divide the width of the available space by 2 and divide the quotient by Math.pi, then the result will be the proper scale to display the entire world in the available space. Figuring out the right scale for your map and your projection is typically done through experimenting with different values, but it’s easier when you include zooming, as you’ll see in section 7.2.2.

Different families of projections have different scale defaults. The d3.geo.albers-Usa projection defaults to 1070, while d3.geo.mercator defaults to 150. As with most D3 functions like this, you can see the default by calling the function without passing it a value:

By adjusting the translate and scale as in listing 7.5, we can adjust the projection to show different parts of the geodata we’re working with—in our case, the world. The result in figure 7.4 shows that we now see the entire world rendered.

Figure 7.4. The Mercator-projected world from our data now fitting our SVG area. Notice the enormous distortion in size of regions near the poles, such as Greenland and Antarctica.

Listing 7.5. Simple map with scale and translate settings

7.1.2. Drawing points on a map

Projection isn’t used only to display areas; it’s also used to place individual points. Typically, you think of cities or people as represented not by their spatial footprint (though you do this with particularly large cities) but with a single point on a map, which is sized based on some variable such as population. A D3 projection can be used not only in a geo.path() but also as a function on its own. When you pass it an array with a pair of latitude and longitude coordinates, it returns the screen coordinates necessary to place that point. For instance, if we want to know where to place a point representing San Francisco (roughly speaking, -122 latitude, 37 longitude), then we could simply pass those values to our projection:

We can use this to add cities to our map along with loading the data from cities.csv, as in the following listing and which you see in figure 7.5.

Figure 7.5. Our map with our eight world cities added to it. At this distance, you can’t tell how inaccurate these points are, but if you zoom in, you see that both of our Italian cities are actually in the Mediterranean.

Listing 7.6. Loading point and polygon geodata

One thing to note from listing 7.6 is that coordinates are often given in the real world in the order of “latitude, longitude.” Because latitude corresponds to the y-axis and longitude corresponds to the x-axis, you have to flip them to provide the x, y coordinates necessary for GeoJSON and D3.

7.1.3. Projections and areas

Depending on what projection you use, the graphical size of your geographic objects will appear different. This is because it’s impossible to perfectly display spherical coordinates on a flat surface. Different projections are designed to visually display the geographic area of land or ocean regions, or the measurable distance, or particular shapes. Because we included d3.geo.projection.js, we have access to quite a few more projections to play with, one of which is the Mollweide projection. In the code in listing 7.7, you can see the settings necessary to properly display a Mollweide projection of our geodata. We’ll use the calculated area of the countries (the graphical area, not their actual physical area) to color each country. The results are quite distinct from the same code running on our Mercator projection, as shown in figure 7.6. The world as displayed with Mollweide curves the edges, rather than stretching them into a rectangle like Mercator does.

Figure 7.6. Mercator (left) dramatically distorts the size of Antarctica so much that no other shape looks as large. In comparison, the Mollweide projection maintains the actual physical area of the countries and continents in your geodata, at the cost of distorting their shape and angle. Notice that geo.path.area measures the graphical area and not the actual physical area of the features.

Listing 7.7. Mollweide projected world

Picking the right projection is never easy, and depends on the goals of the map you’re making. If you’re working with traditional tile mapping, then you’ll probably stick with Mercator. If you’re working on the world scale, it’s usually best to use an equal-area projection like Mollweide that doesn’t distort the visual area of geographic features. But because D3 has so many different projections available, you should experiment to see which best suits the particular map you’re creating.

Infoviz term: choropleth map

As you encounter more mapmaking, you’ll hear the term choropleth map used to refer to a map that encodes data using the color of a region. You can use the existing geographic features, in this case countries, to display statistical data, such as the GDP of a country, its population, or its most widely used language. You can do this in D3 either by getting geodata where the properties field has that information or by linking a table of data to your geodata where they both have the same unique ID values in common.

Keep in mind that choropleth maps, although useful, are subject to what’s known as the areal unit problem, which is what happens when you draw boundaries or select existing features in such a way that they disproportionately represent your statistics. This is the case with gerrymandering, when political districts are drawn in such a way as to create majorities for one political party or another.

7.1.4. Interactivity

Much of the geospatial data-related code in D3 comes with built-in functionality that you’ll typically need when working with geodata. In addition to determining the area like we did to color our features, D3 has other useful functions. Two that are commonly used in mapping are the ability to quickly calculate the center of a geographic area (known as a centroid) and its bounding box, like you see in figure 7.7. In the following listing, you can see how to add mouseover events to the paths we created and draw a circle at the center of each geographic area, as well as a bounding box around it.

Figure 7.7. Your interactivity provides a bounding box around each country and a red circle representing its graphical center. Here you see the bounding box and centroid of China. The D3 implementation of a centroid is weighted, so that it’s the center of most area, and not just the center of the bounding box.

Listing 7.8. Rendering bounding boxes with geodata

You’ve learned the core geo functions that allow you to make maps with D3: geo .projection and geo.path. By using these functions, you can create maps with a distinct look and feel, and provide your users with the ability to interact with them as shapes and as geographic features. D3 provides more functionality, and we’ll dive into it now.

7.2. Better mapping

To make your maps more readable, you can use built-in features from d3.geo: the graticule generator and the zoom behavior. One provides grid lines that make it easier to read a map, and the other allows you to pan and zoom around your map. Both of these follow the same format and functionality of other behaviors and generators in D3, but are particularly useful for maps.

7.2.1. Graticule

A graticule is a grid line on a map. Just as D3 has generators for lines, areas, and arcs, it has a generator for graticules to make your maps more beautiful. The graticule generator creates gridlines (you can specify where and how many, or use the default) and also creates an outline that can provide a useful border. Listing 7.9 shows how to draw a graticule beneath the countries we’ve already drawn. Instead of .data we use .datum, which is a convenience function that allows us to bind a single datapoint to a selection so it doesn’t need to be in an array. In other words,.datum(yourDatapoint) is the same as .data([yourDatapoint]).

Listing 7.9. Adding a graticule

var graticule = d3.geo.graticule();

d3.select("svg").append("path")

.datum(graticule)

.attr("class", "graticule line")

.attr("d", geoPath)

.style("fill", "none")

.style("stroke", "lightgray")

.style("stroke-width", "1px");

d3.select("svg").append("path")

.datum(graticule.outline)

.attr("class", "graticule outline")

.attr("d", geoPath)

.style("fill", "none")

.style("stroke", "black")

.style("stroke-width", "1px");

But how are we drawing so many graticule lines in figure 7.8 from a single datapoint? The geo.graticule function creates a feature known as a multilinestring. A multiline-string, as you may have figured out, is an array of arrays of coordinates, each corresponding to separate individual components of a feature. Multilinestrings and their counterparts, multipolygons, have always been a part of GIS because countries like the United States or Indonesia are made up of disconnected features such as states and regions, and that information needed to be stored in the data. As a result, when d3.geo.path gets a multipolygon or multilinestring, it draws a <path> element made up of multiple, disconnected pieces.

Figure 7.8. Our map with a graticule (in light gray) and a graticule outline (the black border around the edge of the map)

7.2.2. Zoom

You dealt with zoom a little bit in chapter 5, when you saw how the zoom behavior can easily allow you to pan a chart around the screen. Now it’s time you start zooming with zoom. When we first looked at the zoom behavior, we used it to adjust the transform attribute of a <g> element that held our chart. This time, we’ll use the scale and translate values of the zoom behavior to update the settings of our projection, which will give us the ability to zoom and pan our map.

Create a zoom behavior and call it from the <svg> element. Whenever you have a drag event on anything in the <svg>, a mousewheel event, or a double-click, then it triggers zoom. When we worked with zoom before, we only dealt with the dragging, which updates thezoom.translate() value and which you can use to update the translate value of whatever element you want to update. This time, we’ll also use the zoom.scale() value, which gives us an increasing (when you double-click or roll your mousewheel forward) or decreasing (when you roll your mousewheel backward) value. To use zoom with a projection, we’ll want to overwrite the initial zoom.scale() value with the scale value of the projection, and do the same with the zoom translate value. After that, any time we have an event that triggers zoom, we’ll use the new values to update our projection, as shown in the following listing and in figure 7.9.

Figure 7.9. Our map with zooming enabled. Panning occurs with the drag behavior and zooming with mousewheel and/or double-clicking. Notice that the bounding box and centroid functions still work, because they’re based on our constantly updating projection.

Listing 7.10. Zoom and pan with maps

The zoom behavior updates its .translate() array in reference to your dragging behavior, and increases or decreases the .scale() value in reference to your mousewheel and double-click behavior. Because it’s designed to work with SVG transform and D3 geographic projections,d3.behavior.zoom is all you need for pan-and-zoom functionality.

Infoviz term: semantic zoom

When you think about zooming in on things, you naturally think about increasing their size. But from working with mapping, you know that you don’t just increase the size or resolution as you zoom in; you also change the kind of data that you present to the reader. This is known as semantic zoom in contrast to graphical zoom. It’s most clear when you look at a zoomed-out map and see only country boundaries and a few major cities, but as you zoom in you see roads, smaller cities, parks, and so on.

You should try to use semantic zoom whenever you’re letting your user zoom in and out of any data visualization, not just a chart. It allows you to present strategic or global information when zoomed out, and high-resolution data when zoomed in.

The default zoom behavior assumes a user knows that the mousewheel and double-clicking are associated with zooming. But sometimes you want zoom buttons, because you can’t assume the user knows that interaction or because you want to constrain or control the zooming process in a more complicated manner. The code in the following listing creates a zoom function and adds the necessary buttons, as seen in figure 7.10.

Figure 7.10. Zoom buttons and the effect of pressing Zoom Out five times. Because the zoom buttons modify the zoom behavior’s translate and scale, any mouse interaction afterward reflects the updated settings.

Listing 7.11. Manual zoom controls for maps

With this kind of styling and interactivity in place, you can make a map for most any application. Zooming and panning is important for maps because users expect to be able to zoom in and out, and they also expect the details of the map to change when they do so. In that way, geospatial is one of the most powerful forms of information visualization because users have a high level of literacy when it comes to reading and interacting with maps. But users also expect a map to have certain features and functionality, and when those are missing they think it’s broken. Make sure that when you create your map, it either includes this functionality or you have a good reason to leave it out.

7.3. Advanced mapping

We’ve covered the aspects of creating maps that you’ll likely end up using with all your maps. You could explore many variations. You may want to scale your <circle> elements based on population, or use <g> elements so that you can also provide labels like we did earlier. But if you’re making a map, it will probably have polygons and points and take advantage of bounding boxes or centroids, and will likely be tied to a zoom behavior. The exciting thing about D3 is that it lets you explore more complex ways of representing geography, with a little more effort.

7.3.1. Creating and rotating globes

We’ll do only one thing in 3D in this entire book, and that’s create a globe. We don’t need to load three.js or learn WebGL. Instead, we’ll take advantage of a trick of one of the geographic projections available in D3: the orthographic projection, which renders geographic data as it would appear from a distant point viewing the entire globe. We need to update our projection to refer to the orthographic projection and have a slightly different scale.

Listing 7.12. Creating a simple globe

projection = d3.geo.orthographic()

.scale(200)

.translate([width / 2, height / 2])

.center([0,0]);

With this new projection, you can see what looks like a globe in figure 7.11.

Figure 7.11. An orthographic projection makes our map look like a globe. Notice that even though the paths for countries are drawn over each other, they’re still drawn above the graticules. Also notice that although zooming in and out works, panning doesn’t spin the globe but simply moves it around the canvas. The coloration of our countries is once again based on the graphical size of the country.

To make it rotate, we need to use d3.mouse, which returns the current position of the mouse on the SVG canvas. Pair this with event listeners to turn on and off a mousemove listener on the canvas. This simulates dragging the globe, which we’ll use only to rotate it along the x-axis. Because we’re introducing new behavior and it’s been a while since we looked at the full code, the following listing has the entire code for creating the globe.

Listing 7.13. A draggable globe in D3

A plugin by Jason Davies known as d3.geo.zoom (https://www.jasondavies.com/maps/rotate/) abstracts this functionality.

But this map still has the problem of a graphical artifact from the graticule outline, which must be removed when drawing globes. Another problem is seeing through the globe to the other side. This might be a fine idea, if it didn’t also muddle the SVG drawing code so that the shapes are drawn poorly when they get near the border (notice how poorly Antarctica looks in figure 7.12). Also, our cities are drawn above the paths, even when they’re ostensibly on the other side of the world (for example, Karachi).

Figure 7.12. A globe with a transparent surface. You can see Australia through the globe because the projection doesn’t by default clip this. Cities are drawn at the correct coordinates but are uniformly drawn above the features because the <circle> elements are drawn on top of the <path> elements in the DOM.

The path drawing can be handled with the clipAngle property of the projection, which clips any paths drawn with that projection if they fall outside of a particular angle from its center. This can be useful to show only small parts of your dataset for performance or display purposes. Here’s how it looks in our new projection code:

projection = d3.geo.orthographic()

.scale(200)

.translate([width / 2, height / 2])

.clipAngle(90);

This won’t work for the circles we’re using for our cities, because clipAngle only applies to data that’s created by d3.geo.path(). For the circles, we have to ensure that they’re only displayed if they fall within that clip angle. Taking this into account, we can pass a test in the zoomed function to determine whether a city should be displayed based on its coordinates.

Listing 7.14. Hiding cities on the other side of a rotated globe

You may think you’re done, but there’s one related issue to address now. You draw all the countries when the globe is first initialized, but many of them are clipped, and so your geo.path.area() function, which determines the area as the shape is drawn, has even worse issues than the Mercator projection had. For instance, in figure 7.13, Australia is colored as if it had an area similar to Madagascar. Fortunately, D3 also includes d3.geo.area(), which determines the spherical area of a shape corresponding to its geographic area, as in figure 7.14.

Figure 7.13. Our rotating and properly clipped globe

Figure 7.14. Our globe with countries colored by their geographic area, rather than their graphical area

We could rewrite the draw code to use d3.geo.area, but instead let’s recolor our existing globe. But how do we get the data? Until now, we’ve assumed that the data array was exposed somewhere our functions could get to, but what if it’s outside our current scope? In this case, we can useselectAll.data() and get an array of data associated with whatever we select (which includes undefined elements if we select HTML elements that aren’t bound with data). You’ll see this in action more in the next chapter.

var featureData = d3.selectAll("path.countries").data();

var realFeatureSize =

d3.extent(featureData, function(d) {return d3.geo.area(d)});

var newFeatureColor =

d3.scale.quantize().domain(realFeatureSize).range(Reds[7]);

d3.selectAll("path.countries")

.style("fill", function(d) {return newFeatureColor(d3.geo.area(d))});

The spherical area of a shape as measured by d3.geo.area() is given in steradians, and so it’s only a roughly proportionate area. If you want the actual square kilometers of a country or other shape, you’ll still need to calculate that in a GIS package like QGIS, or get that information from another source.

This globe still has some issues. Because you don’t update the projection.center(), and you base the rotation off the current position of the mouse, it resets any time you drag the globe. You also don’t clip the cities when you first draw them. Further, you can make a D3 globe drag in any of the three directions you can rotate a normal globe. But if you’re looking for that level of functionality, then you’re better off exploring the many and robust examples available online (such as those of Jason Davies at http://jasondavies.com/maps/voronoi/capitals/). Instead, we’ll look at another exotic way of representing geodata, the satellite projection.

7.3.2. Satellite projection

Isometric views of the world are powerful tools for storytelling. Imagine you had to create a map related to how the Middle East has a changing view of Europe. By crafting a satellite view looking out over the Mediterranean from the Middle East as shown in figure 7.15, you invite your map reader to see a distant Europe from a geographical perspective in the Middle East.

Figure 7.15. A satellite projection of data from the Middle East facing Europe

This is a projection just like the orthographic, Mercator, and Mollweide projections we previously used, but, as you see in the following listing, it has specific settings for scale and rotate. It also uses new settings, tilt and distance, to determine the angle of the satellite projection.

Listing 7.15. Satellite projection settings

Tilt is the angle of the perspective on the data, while distance is the percentage of the radius of the earth (so 1.119 is 11.9% of the radius of the earth above the earth). How do you come up with such exact settings? You have two options. The first is to understand how to describe a tilted projection like this mathematically. If you have a degree in math or geography, you can look into literature for calculating this. If, like me, you don’t have that kind of background, then I would suggest building a tool, using the code we explored in this chapter, to adjust the rotation, tilt, distance, and scale settings interactively. That’s how I did it, and you can play with my satellite projection tool here: http://bl.ocks.org/emeeks/10173187.

Recall my advice for understanding how the Sankey layout works. Use information visualization to visualize how the functions work so that you can better understand them and find the right settings. Otherwise, you’re going to need to take a course in GIS or wait for someone to write D3.js Mapping in Action.

Now we’ll shift gears away from visualization and back to geodata structure to explore a library that was developed by Mike Bostock and is intimately tied to D3 mapping: TopoJSON.

7.4. TopoJSON data and functionality

TopoJSON (https://github.com/mbostock/topojson) is, fundamentally, three different things. First of all, it’s a data standard for geographic data, and an extension of GeoJSON. Secondly, it’s a library that runs in node.js to create TopoJSON-formatted files from GeoJSON files. Thirdly, it’s a library that runs in JavaScript that processes TopoJSON-formatted files to create the data objects necessary to render them with libraries like D3. You won’t deal with the second form at all, and you’ll only examine the first in a cursory manner as you learn about rendering TopoJSON data, merging it, and using it to find a feature’s neighbors.

7.4.1. TopoJSON the file format

The difference between GeoJSON files and TopoJSON files is that while GeoJSON records for each feature an array of longitude and latitude points that describe a point, line, or polygon, TopoJSON stores for each feature an array of arcs. An arc is any distinct segment of a line shared by one or more features in your dataset. The shared border between the United States and Mexico is a single arc that’s referred to in the arcs array of the feature for the United States and the arcs array of the feature for Mexico.

Because most datasets have shared segments, TopoJSON often produces significantly smaller datasets. This is part of its appeal. Another part is that if you know what segments are shared, then you can do interesting things with the data, like easily calculating the neighboring features or the shared border, or merging features.

TopoJSON stores the arcs as a reference to a particular arc in a master list of arcs that defines the coordinates of that arc. You need the Topojson.js library included in any website you’re using to create maps with TopoJSON, because it changes TopoJSON into a format that D3 can read and create graphics from.

7.4.2. Rendering TopoJSON

Because TopoJSON stores its data in a format different from the GeoJSON structure that’s expected by d3.geo.path(), we need to include Topojson.js and use it to process TopoJSON data to produce GeoJSON features. This is rather straightforward and can be done in a call to our new datafile, as shown in the following listing. Figure 7.16 shows the properly formatted features in your console.

Figure 7.16. TopoJSON data formatted using Topojson.feature(). The data is an array of objects, and it represents geometry as an array of coordinates like the features that come out of a GeoJSON file.

Listing 7.16. Loading TopoJSON

Now that it’s in the format we want, we can send it to our existing code and draw this array of features like we did with the features we loaded from world.geojson. We replace our earlier countries with the worldFeatures variable declared in listing 7.16. That’s all that most people do with TopoJSON, and they’re happy for it because TopoJSON data is significantly smaller than GeoJSON data. But because we know the topology of the features in a TopoJSON data file, we do interesting geographic tricks with it.

7.4.3. Merging

The TopoJSON library provides you with the capacity to create new features by merging existing features. You can create a new feature for “North America” by merging the countries in North America, or create “The United States in 1912” by merging the states that were part of the United States in 1912. Listing 7.17 shows the code to draw a map using our new TopoJSON data file and merge all the countries that have a center west of 0° longitude. The results, in figure 7.17, show that merging combines not only contiguous features but also separate features into a multipolygon.

Figure 7.17. The results of merging based on the centroid of a feature. The feature in gray is a single merged feature made up of many separate polygons.

Listing 7.17. Rendering and merging TopoJSON

We can adjust the mergeAt test slightly to look at the x coordinate or to see features that have greater values of mergeAt. As shown in figure 7.18, this creates a single feature in each of four cases: less than or greater than 0° latitude and less than or greater than 0° longitude. Notice in each case that it’s a single feature but not a single polygon.

Figure 7.18. By adjusting the merge settings, we can create something like northern, southern, eastern, and western hemispheres as merged features. Notice that because this is based on a centroid, we can see at the bottom left a piece of Eastern Russia as part of our merged feature, along with Antarctica.

A quick note for those who may want to continue working in topologies: Topojson.merge has a sister function, mergeArcs, that allows you to merge shapes but keep them in TopoJSON format. Why would you want to maintain arcs? Because then you could continue to use TopoJSON functionality like merging, creating meshes, or finding neighbors of your newly merged features.

7.4.4. Neighbors

Because we know when features share arcs, we also know what features neighbor each other. The function Topojson.neighbors builds an array of all the features that share a border. We can use this array to easily identify neighboring countries in our dataset using the code in the following listing. The results of the interaction provided by this code are shown in figure 7.19.

Figure 7.19. Hover behavior displaying the neighbors of France using TopoJSON’s neighbor function. Because Guyana is an overseas department of France, France is considered to be neighbors with Brazil and Suriname. This is because France is represented as a multipolygon in the data, and any neighbors with any of its shapes are returned as neighbors.

Listing 7.18. Calculating neighbors and interactive highlighting

TopoJSON is a powerful new technology that provides tremendous opportunity for web map development. Understanding how it models data and the functionality that it provides are key to creating maps that impress users. As you explore traditional web tile mapping, you’ll see that you can combine more traditional web mapping techniques with the advanced functionality provided by TopoJSON and D3’s geo functions to make incredibly sophisticated web maps.

7.5. Tile mapping with d3.geo.tile

So far you’ve made choropleth maps, some of which are simple and others, like the satellite projection or the globe, rather exotic. But none of your maps have terrain, or satellite imagery. That kind of data—raster or image data—isn’t nearly as lightweight as vector data. Think about the size of a picture you take with the camera on your phone, and imagine how large an image must be if you want to give your user the ability to zoom in to any street in the world.

To get around the problem of these massive images, web mapping uses tiles to display satellite and terrain data. A high-resolution satellite image of a city, for instance, would be cut into 256- by 256-px tiles at as many zoom levels as are appropriate and stored on a server in directories indicating the zoom and position of those tiles. It sounds like it might be a lot of work to make tiles, but fortunately, you don’t have to, because companies like Mapbox (mapbox.com) provide you with tiles and the tools, like TileMill, to customize them. (Both free and commercial versions are available, depending on how many visitors your site receives.)

If you open up tile.js and take a look at it, you’ll see that it’s a small file. That’s because geotiles are simple. Each tile is a raster image (typically a PNG) that represents one square of the earth somewhere, as you see in figure 7.20. Its filename indicates the geographic location and at what zoom level the image shows. The d3.geo.tile() function (the library to access this function is available at https://github.com/d3/d3-plugins/tree/master/geo/tile) parses that filename and directory structure for us so that we can use these tiles in our map. First, though, we have to calibrate the scale and translate of our projection as well as our zoom behavior.

Figure 7.20. Your first tiled map, using pregenerated tiles from Mapbox

Listing 7.19. A tile map

We’ll want to add our points and polygons to this map. The code to do that isn’t very different from the code you saw in listing 7.19 and the code we’ve been working with throughout the chapter. We’ll use the same data, but add a function on the display styling of the countries to make half of them disappear. You can see the results in figure 7.21.

Figure 7.21. A tile map overlaid with the point and polygon data we worked with throughout this chapter

Listing 7.20. A tile map with vector data overlaid

7.6. Further reading for web mapping

As I said in the beginning of this chapter, the things you can do with D3’s mapping capabilities would fill an entire book. Following are a few other capabilities we didn’t cover in this chapter.

7.6.1. Transform zoom

The method we used for our zoom behavior in this chapter is known as projection zoom and recalculates mathematically the shape of features based on a change in scale and translation. But if you’re using a projection that’s flat like Mercator, then you can achieve faster performance by tying the change in scale and translate of the zoom behavior to your features’ SVG transform. One issue you’ll run into is that font size and stroke width are affected by SVG transform, and so you’ll need to adjust those settings on the fly.

7.6.2. Canvas drawing

The .context function d3.geo.path allows you to easily draw your vector data to a <canvas> element, which can dramatically improve speed in certain cases. It also allows you to use .toDataURL() to dynamically create a PNG for users to save or share on social media.

7.6.3. Raster reprojection

Jason Davies and Mike Bostock have both provided examples of reprojecting, not just vector data, but the tile data used in tile maps (see bl.ocks.org/mbostock/ and www.jasondavies.com/maps/raster/satellite/). You can use this to show a satellite-projected terrain map, or a terrain map with the Mollweide projection we used earlier.

7.6.4. Hexbins

The d3.hexbin plugin allows you to easily create hexbin overlays for your maps like that seen in figure 7.22. This can be effective when you have quantitative data in point form and you want to aggregate it by area.

Figure 7.22. An example of hexbinning by Mike Bostock showing the locations of WalMart stores in the United States (available at http://bl.ocks.org/mbostock/4330486).

7.6.5. Voronoi diagrams

As with hexbins, if you only have point data and want to create area data from it, you can use the d3.geom.voronoi function to derive polygons from points like the kind seen in figure 7.23.

Figure 7.23. An example of a Voronoi diagram used to split the United States into polygons based on the closest state capital (available at http://www.jasondavies.com/maps/voronoi/us-capitals/).

7.6.6. Cartograms

Distorting the area or length of a geographic object to show other information creates a cartogram. For example, you could distort the streets of your city based on the time it takes to drive along them, or make the size of countries on a world map bulge or shrink based on population. Although no simple functions exist to create cartograms, examples of how to create them in D3 include one created by Jason Davies (http://www.jasondavies.com/maps/dorling-world/), one created by Mike Bostock (http://bl.ocks.org/mbostock/4055908), and the cost cartogram I built (orbis.stanford.edu).

7.7. Summary

In this chapter, we’ve covered the incredible breadth of geospatial information visualization capabilities present in D3. Maps are a core aspect of information visualization, and the creation of rich interactive websites and D3’s geo functions allow you to make maps that are much richer than the pushpin web maps that you typically see on the web. To make those maps, we walked through a massive amount of functions and concepts, including

· Understanding the GeoJSON spatial data format

· Creating simple maps

· Creating map components like graticules

· Computing geospatial attributes like centroids and bounding boxes

· Giving the user rich interactive panning and zooming

· Using different projections

· Creating globes

· Rendering TopoJSON and using it to merge features and find neighbors

· Creating tile maps with TopoJSON overlays.

In the next chapter, you’ll start using D3 selections and data-binding to create galleries and tables using traditional DOM elements.