Shape maps

We’ve done maps based on points before, now it’s time to stretch out d3 projection skills a little bit further!

Data format: GeoJSON

Generally we use d3.csv to read in our data, because… our data is a CSV, of course. This time we’re going to be using JSON, and (surprise!) d3 supports that with d3.json.

Open example in new window

<script>
d3.json("/tutorials/assets/data/gz_2010_us_050_00_5m.json", function(error, data) {
  console.log(data);
});
</script>

Open example in new window

If you look at the console, you can see it has two keys:

  • type, which is set to "FeatureCollection"
  • features, which is an array with around 3000… things in it.

Each of those features has its own attributes, and some of those attributes have their own sub-attributes, and who knows, maybe it goes on forever and ever? Don’t sweat it, though, we’ve got this covered!

This is a specific kind of JSON called GeoJSON. It’s the exact same thing as JSON, just formatted in a specific way. For example, from http://geojson.org/:

{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": [125.6, 10.1]
  },
  "properties": {
    "name": "Dinagat Islands"
  }
}

Every single GeoJSON document will be formatted in the exact(-ish) same way, typically a FeatureCollection with a bunch of Features inside of it, and each of those features has geometry (the geography of the feature) and properties (the data attached to the feature). What we have now is

  • FeatureCollection of all of the counties in the USA
  • Each county is a Feature
  • Each county’s boundaries is inside of geometry, usually a MultiPolygon
  • Each country’s data set - its name, area, etc - is stored inside of properties

It might be easier if you take a little while to play around with creating your own GeoJSON! I highly highly recommend hopping on over http://geojson.io/, which allows you to build your own GeoJSON.

Back to our code

Pulling out the counties

First, let’s take out all of the counties into their own variable. Since we have a FeatureCollection, all of the counties are inside of the features key of our data.

var counties = data['features'];

Building our chart space

Nothing weird here, just making an svg and putting a g inside of it (we don’t have to use a g since we don’t have margins, but I think it’s good practice.)

var height = 500;
var width = 600;

// Append the svg inside of our div
var svg = d3.select("#map")
            .append("svg")
            .attr('height', svg_height)
            .attr('width', svg_width);

// Add in the g to offset it with the margin space
var map = svg.append("g").attr("transform", "translate(0,0)");

Creating our projection and adding our shapes

You can’t have a map without a projection! Since this is the USA, we’ll stick with d3.geo.albersUSA.

var projection = d3.geo.albersUsa()
                        .scale(800)
                        .translate([width / 2, height / 2]);

Last time we made a map we used circle to draw points, but how do we draw shapes? Remember when we tried to draw a line on a graph and accidentally drew these really weird ugly shapes instead? It’s the same thing!

In SVG, lines (with multiple points in them) and shapes are the same thing, shapes just happen to be filled in. This means we’ll use path to draw our shapes.

map.selectAll("path")
    .data(counties)
    .enter()
    .append("path");

…but last time we made a line with path, we had to set the d attribute to give it a path. We had a d3.geo.line thingie help us plot the x and y. You’ll probably remember with an example!

// map years on the x axis and sales on the y axis
// using our xscale and yscale
var line = d3.svg.line()
    .x(function(d) { 
      return xscale(d['year']); 
    })
    .y(function(d) { 
      return yscale(d['sales']); 
    });

// use line to help plot where the points will go
chart.selectAll("path").data(datapoints).attr('d', line)

But times have changed! We’re using a projection now, which means we don’t have an x-scale and y-scale anymore, we have some weird convoluted system instead. When we were plotting circles we used something like this:

circles.attr('cx', function(d) {
	var coords = [ d['lng'], d['lat'] ];
	var projected_coords = projection(coords);
	return projected_coords[0];
})
.attr('cy', function(d) {
	var coords = [ d['lng'], d['lat'] ];
	var projected_coords = projection(coords);
	return projected_coords[1];
});

BUT WORRY NO MORE! When we’re mapping with lines, d3 takes care of it all behind the scenes. It has a built-in projection helper for lines that is confusingly called path. Instead of muddling through like we did with circles, it’s so easy we might just die.

// We're using the albers projection
var projection = d3.geo.albersUsa()
                        .scale(800)
                        .translate([width / 2, height / 2]);
                        
// Use the projection to create a path helper for our lines
var path = d3.geo.path()
    .projection(projection);

// Use the path helper with the d attribute to draw the path
map.selectAll("path")
    .data(counties)
    .enter()
    .append("path")
    .attr('d', path);

Putting it together

I know it’s been a lot! We just haven’t had enough to draw an entire page yet. Let’s do that now!

Open example in new window

<div id="map"></div>
<script>
d3.json("/tutorials/assets/data/gz_2010_us_050_00_5m.json", function(error, data) {
	/* Take a look at our data */
  console.log(data);

  /* Hmmmm, it has a .type and it has .features. What's in .features? */
  console.log(data['features']);

  /* It's a long list of stuff! Let's look inside one of those features */
  console.log(data['features'][0]);
  
  /* Let's break out our features into another variable... */
  var counties = data['features'];

  /* 
    Okay, so it has two bits:
    1) geometry - the type (multipolygon, line, point) and the coordinates
    2) properties - the bits of data that come along with it
  */
  
  /* Let's build something at http://geojson.io/ */
  
  // Build our chart space
  var height = 500;
  var width = 600;

  // Append the svg inside of our div
  var svg = d3.select("#map")
              .append("svg")
              .attr('height', height)
              .attr('width', width);

  // Add in the g to offset it with the margin space
  var map = svg.append("g");

  // create our projection
  var projection = d3.geo.albersUsa()
                          .scale(800)
                          .translate([width / 2, height / 2]);

  var path = d3.geo.path()
      .projection(projection);

  map.selectAll("path")
      .data(counties)
      .enter()
      .append("path")
      .attr('d', path);
});
</script>

Open example in new window

AWESOME! And we didn’t even do much at all, really. That’s like ten lines of code after we create the SVG!

Coloring the map

Now we all know we could use .style('fill', '#ff0000') to make the map red. But how do you change the color based on some value? We could always use console.log inside of a .style call, like so:

map.selectAll("path")
    .data(counties)
    .enter()
    .append("path")
    .style('fill', function(d) {
      // look at our data
      console.log(d);
      // but always return red
      return '#ff0000';
    })
    .attr('d', path);

It gives us a type, some properties and geometry.

{ 
  "type": "Feature", 
  "properties": { 
    "GEO_ID": "0500000US02261", 
    "STATE": "02", 
    "COUNTY": "261", 
    "NAME": "Valdez-Cordova", 
    "LSAD": "CA", 
    "CENSUSAREA": 34239.880000 
  }, 
  "geometry": { 
    "type": "MultiPolygon", 
    "coordinates": [ ] 
  }
}

Where we could usually do something like d['NAME'], when you work with GeoJSON you always always always need to look inside of properties. Always always always (usually). You will forget this, but try not to. console.log will help you realize what you’ve done, but it’s always best to be prepared. Let’s look a everyone’s Census land area.

map.selectAll("path")
    .data(counties)
    .enter()
    .append("path")
    .style('fill', function(d) {
      // look at our data
      console.log(d['properties']['CENSUSAREA']);
      // but always return red
      return '#ff0000';
    })
    .attr('d', path);

Hmmm. That’s… a lot of numbers? If we’re going to build a scale, we’ll want to lean on our old friend d3.max.

// find the largest area of a county
var max_area = d3.max( counties, function(d) { return d['properties']['CENSUSAREA'] });

Remember, it’s always inside of ['properties']! Then we’ll make an appropriate scale using this data.

// color them between beige and red
var color_scale = d3.scale.linear().domain([0, max_area]).range(['beige', 'red']);

And plop that into the .style call

map.selectAll("path")
    .data(counties)
    .enter()
    .append("path")
    .style('fill', function(d) {
      return color_scale(d['properties']['CENSUSAREA']);
    })
    .attr('d', path);

And we should be good to go! Let’s put it all together and see if it works.

Open example in new window

<div id="map"></div>
<script>
d3.json("/tutorials/assets/data/gz_2010_us_050_00_5m.json", function(error, data) {
  console.log(data);
  console.log(data['features']);
  console.log(data['features'][0]);
  
  /* Let's break out our features into another variable... */
  var counties = data['features'];

  // Build our chart space
  var height = 500;
  var width = 600;

  // Append the svg inside of our div
  var svg = d3.select("#map")
              .append("svg")
              .attr('height', height)
              .attr('width', width);

  // Add in the g to offset it with the margin space
  var map = svg.append("g");

  // create our projection
  var projection = d3.geo.albersUsa()
                          .scale(800)
                          .translate([width / 2, height / 2]);

  var path = d3.geo.path()
      .projection(projection);

  // find the largest area of a county
  var max_area = d3.max( counties, function(d) { return d['properties']['CENSUSAREA'] });

  // color them between beige and red
  var color_scale = d3.scale.linear().domain([0, max_area]).range(['beige', 'red']);

  map.selectAll("path")
      .data(counties)
      .enter()
      .append("path")
      .style('fill', function(d) {
        return color_scale(d['properties']['CENSUSAREA']);
      })
      .attr('d', path);
});
</script>

Open example in new window

Adjusting your scale

Ah, that’s… pretty useless. You can see that Alaska has some big counties and that the projection is making them nice and small, but it seems kind of useless.

The right thing to do in this case is (probably) to make a histogram and figure out what the best ways to cut that up. But hey, instead of doing that, let’s just try a bunch of different color scales!

Middle values

What if instead of saying “these are big”, instead we wanted to say “these are medium and these are big and these are small”? We might have blue be 0, beige be the median, and red be the largest.

Question: But how?

Answer: Your friend d3.max has a friend named d3.median that you can use with d3.scale.linear().

var max_area = d3.max( counties, function(d) { return d['properties']['CENSUSAREA'] });
var median_area = d3.median_area( counties, function(d) { return d['properties']['CENSUSAREA'] });

And when you’re making a color scale, you can pass multiple ranges. You can say zero to the median is blue to beige, and the median to max is beige to red. You do this like so:

// blue to beige to red
var color_scale = d3.scale.linear().domain([0, median_area, max_area]).range(['red', 'beige', 'red']);

Let’s see how it looks:

Open example in new window

<div id="map"></div>
<script>
d3.json("/tutorials/assets/data/gz_2010_us_050_00_5m.json", function(error, data) {
  console.log(data);
  console.log(data['features']);
  console.log(data['features'][0]);
  
  /* Let's break out our features into another variable... */
  var counties = data['features'];

  // Build our chart space
  var height = 500;
  var width = 600;

  // Append the svg inside of our div
  var svg = d3.select("#map")
              .append("svg")
              .attr('height', height)
              .attr('width', width);

  // Add in the g to offset it with the margin space
  var map = svg.append("g");

  // create our projection
  var projection = d3.geo.albersUsa()
                          .scale(800)
                          .translate([width / 2, height / 2]);

  var path = d3.geo.path()
      .projection(projection);

  // find the largest area of a county
  var max_area = d3.max( counties, function(d) { return d['properties']['CENSUSAREA'] });
  var median_area = d3.median( counties, function(d) { return d['properties']['CENSUSAREA'] });

  // color them between beige and red
  var color_scale = d3.scale.linear().domain([0, median_area, max_area]).range(['blue', 'beige', 'red']);

  map.selectAll("path")
      .data(counties)
      .enter()
      .append("path")
      .style('fill', function(d) {
        return color_scale(d['properties']['CENSUSAREA']);
      })
      .attr('d', path);
});
</script>

Open example in new window

Fun, right?

Next steps

Now that you’ve seen that mixing up the color scale a little can tell a whole different story, check out the same map colored many many different ways at color scale examples.

Want to hear when I release new things?
My infrequent and sporadic newsletter can help with that.