We’ve done maps based on points before, now it’s time to stretch out d3 projection skills a little bit further!
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
.
<script>
d3.json("/tutorials/assets/data/gz_2010_us_050_00_5m.json", function(error, data) {
console.log(data);
});
</script>
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 USAFeature
geometry
, usually a MultiPolygon
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.
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'];
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)");
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);
I know it’s been a lot! We just haven’t had enough to draw an entire page yet. Let’s do that now!
<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>
AWESOME! And we didn’t even do much at all, really. That’s like ten lines of code after we create the SVG!
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.
<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>
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!
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:
<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>
Fun, right?
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.