By Jonathon Morgan


D3 is quickly become the de facto library for browser-based data visualizations. However while it's widely used for line graphs and bar charts, its mapping features are still fairly underutilized -- particularly in relation to more established tools like CartoDB, and of course Google Maps. Those tools have their place, but when you need fine-grained control over the presentation and interactivity of your geospatial data, D3 can be a powerful alternative.

Today we'll walk through how to create a popular visualization; the choropleth map. These are used to show the relative concentration of data points within a given region. For example this might be the number of people within a particular age range in every county in a state, or the number of reported cases of the flu in each state in a country. The information we'll be mapping is a little more exotic. I recently collaborated with Eliot Higgins, an arms transfer analyst focused on the ongoing conflict in Syria, to retrieve data from 1,700 Facebook pages and YouTube accounts associated with militant groups and humanitarian organizations working in Syria. We ingested that data into CrisisNET, which then made it possible for us to generate a "heat map" showing which parts of Syria are experiencing the most intense fighting.

In order to do this we'll need to:

  1. Work with projections to transform latitude, longitude pairs to x, y browser coordinates
  2. Render city boundaries as SVG paths using D3 drawing tools
  3. Shade each city relative to its reported level of violence

Let's get started.

Getting the Data

Before we can do anything we'll need some data. A geospatial "feature" (like a city, state, etc), is defined as a polygon, which is represented as a list of latitude/longitude pairs. For example:

  [ 36.712428478000049, 35.83274311200006 ], 
  [ 36.704171874000053, 35.830347390000043 ],

Each pair is a corner of the polygon, so if you plotted them on a map and connected the dots, you would get the outline of the feature. Awesome!

Geospatial data comes in a variety of formats, like shapefiles, and KML. However the emerging standard, particularly for use in web applications, is GeoJSON. Not surprisingly, this is the format supported by D3 and the one we'll be using.

Depending on the region you're trying to map, GeoJSON polygons defining features in that region may be easy to find -- like these GeoJSON files for all counties in the United States. On the other hand, particularly if you're interested in the developing world, you'll probably need to be more creative. To map cities in Syria, I tracked down a shapefile from an NGO called Humanitarian Response, and then converted that shapefile to GeoJSON using a tool called ogr2ogr. Fortunately for you, I've made the GeoJSON file available, so just download that and you'll be ready to go.

Let's Talk Projections

With our polygons in hand, we can start mapping.

Remember that latitude and longitude coordinates denote positions on the surface of the Earth, which is not flat (it is an ellipsoid). Your computer screen is a plane (which means it's flat), so we need some way to translate the position of a point on a curved surface to its corresponding point on a flat surface. The algorithms for doing this are called "projections." If, like me, you've forgotten most of your high school geometry, you'll be pleased to learn that D3 comes included with a number of popular projections, so we won't need to write one. Our only job is to choose the correct projection for our visualization.

The Albers and Azimuthal Equal Area projections are recommended for choropleth maps, but I found both rendered my cities in a way that didn't connect all the points in the polygons from our shapefile, so some of the city outlines didn't form an enclosed shape. This made it impossible to shade each city without the color overflowing into other parts of the map. Although this is probably due more to my lack of familiarity with the specifics the Albers and Azimutha projections, I found that the Conic Conformal projection worked out of the box, so that's the one I chose.

Drawing the Map

Now that you understand the background, we can start coding. First, attach an element to the DOM that will serve as our canvas.

<div id="map"></div>  

Next create an SVG element and append it to the map DOM node we just created. We'll be drawing on this SVG element in just a second.

// Size of the canvas on which the map will be rendered
var width = 1000,  
    height = 1100,
    // SVG element as a JavaScript object that we can manipulate later
    svg ="#map").append("svg")
      .attr("width", width)
      .attr("height", height);

Despite the rather lengthy explanation, defining the projection in our application is actually fairly straightforward.

// Normally you'd look this up. This point is in the middle of Syria
var center = [38.996815, 34.802075];

// Instantiate the projection object
var projection = d3.geo.conicConformal()  
    // Size of the map itself, you may want to play around with this in 
    // relation to your canvas size
    // Center the map in the middle of the canvas
    .translate([width / 2, height / 2])

With a projection ready to go, we're ready to instantiate a path. This is the path across your browser window D3 will take as it draws the edges of all our city polygons.

// Assign the projection to a path
var path = d3.geo.path().projection(projection);  

Finally, let's give some geospatial data to our path object. This data will be projected to x, y pairs, representing pixel locations on our SVG element. When D3 connects these dots, we'll see the outlines of all the cities in Syria.

Let's use d3's json method to retrieve the GeoJSON file I referenced earlier.

d3.json("cities.json", function(err, data) {  
  $.each(data.features, function(i, feature) {
      .attr("class", "border");

That's it!

Most of the heavy lifting is taken care of by D3, but in case you're curious about what's happening, here's a little more detail. Our GeoJSON file contains an array of features, each of which is a polygon (which is represented as an array of longitude, latitude coordinate pairs). We pass the polygon to our path using the datum method, and the polygon is then converted by our projection to a linestring of pixel positions which is used by the browser to render a path DOM node inside our svg element. Phew.

With a working map of the country, we can now change its appearence and add interactivity just like any other DOM node. Next week we'll use the CrisisNET API to count reports of violent incidents for each city in Syria, and shade each city on the map with CSS based on those report counts.

In the meantime you can checkout the full, working map on our Syria project page.