This is a step-by-step tutorial adapted from a tutorial made for Maptime Amsterdam's fourth Meetup, on February 18th, 2015.
We've adapted it slightly to match the versions of libraries we are using in DS 4200 at Northeastern in the Spring of 2020.
We've also shortened the tutorial. If you'd like to see the original tutorial, you can view the original github page it was posted on.
When you are finished, please publish to github pages, and post a link to your github pages HERE: https://neu-ds-4200-s20.github.io/in-class-programming-geo-jmclarney/
Then, submit a link to your repository on Canvas under the in-class assignment page.
First, you should have created a copy of this repository using github classroom. As with other assignments, open your terminal, clone it, and then navigate inside the directory that was created.
As we talked about in class, generally, to visualize geographic data, you'll have to go through a few steps.
- Download publicly available geography data
- Use command line tools to convert it to TopoJSON data that d3 can use
In this tutorial, we've already done that for you. However, if you are interested, this section describes how we've done that.
We'll use geo-spatial files containing U.S. state boundaries from the National Historical Geographic Information System:
The National Historical Geographic Information System (NHGIS) provides, free of charge, aggregate census data and GIS-compatible boundary files for the United States between 1790 and 2013.
You don't have to download and convert NHGIS data, this tutorial comes with all the data you need. You can just skip this section!
To download and convert the data needed for this tutorial yourself, follow these steps:
- Get Shapefiles with state data, per year
- You can use the NHGIS Data Finder to select the data you need,
- Or simply download
animated-borders-d3js.zip
from Maptime Amsterdam's Dropbox
- Move/copy the zipped Shapefiles to the data directory (the files should be named
nhgis0001_shapefile_tl2000_us_state_XXXX.zip
) - Convert the Shapefiles to TopoJSON!
To do this, you need to install shp2json and TopoJSON, and you need Node.js.
brew install node
npm install -g shp2json
npm install -g topojson
Afterwards, you can convert a Shapefile to TopoJSON by running shp2json and piping its output to TopoJSON:
shp2json <shapefile> | topojson -p STATENAM -s 1e-6
Or, run shp2topojson.sh
, a convenient script that comes with this tutorial:
cd scripts
./shp2topojson.sh
Done! But you should have a look at the TopoJSON files in the data
directory, either with your text editor, or on GitHub (GitHub lets you even view GeoJSON files!).
Run a python server and confirm that you have an empty website showing up.
python -m http.server
In this step, we'll add the D3 and TopoJSON JavaScript libraries, an SVG element, and code to load and display a TopoJSON file.
First, inside the <head>
element, include two JavaScript libraries:
<script src="http://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
Also inside the <head>
element, add some CSS:
<style>
* {
margin: 0;
padding: 0;
}
svg {
position: absolute;
width: 100%;
height: 100%;
}
</style>
This CSS defines the web page's style, and without it, our map will never be properly displayed.
Then, right before the <h1>
tag we've added in the previous step, add an SVG element:
<svg>
<g> id="states"></g>
</svg>
Now, for some proper JavaScript! Past the following code inside the renderMap.js
file that is being included in our index.html
file:
var svgStates = d3.select("svg #states"); // (1)
var projection = d3.geoAlbersUsa(); // (2)
var path = d3.geoPath()
.projection(projection); // (3)
d3.json("data/1790.json", function(error, topologies) { // (4)
var state = topojson.feature(topologies[0], topologies[0].objects.stdin); // (5)
svgStates.selectAll("path") // (6)
.data(state.features)
.enter()
.append("path")
.attr("d", path);
});
Explanation of individual JavaScript lines:
- Use D3 to select the SVG group with id
#states
. - D3 has some map projections built-in. We'll use Albers USA projection, but feel free to experiment with some of the others!
- See http://bost.ocks.org/mike/map/ for more information!
- Load
states.json
(JSON array containing 13 decades of U.S. states) topojson.feature
will convert a TopoJSON object to a GeoJSON object. We'll start with just the first element in the TopoJSON array (topologies[0]
= year 1790). I've used the TopoJSON command line utility to convert NHGIS's Shapefiles to TopoJSON, this is why the objects in the JSON file are inside an object calledstdin
...- See http://bost.ocks.org/mike/map/ for more information!
OK. Maybe you should just open the results in your browser!
After this step, your map should look something like this:
- Screenshot:
Replace the following line:
var projection = d3.geoAlbersUsa(); // (2)
With:
var width = window.innerWidth, // (1)
height = window.innerHeight;
var projection = d3.geoAlbersUsa()
.translate([width / 2, height / 2]); // (2)
- Gets the width and height of the browser window
- Translates the projection, and sets the center halfway the browser's width and height
The map should now be centered nicely in your browser's window!
After this step, your map should look like this:
- Screenshot:
The map will be much easier to read if we would add the U.S. coastline. Natural Earth provides a great selection of public domain geo-spatial files, including just the file we need.
We don't want the coastline to be too visible, we'll use SVG's stroke-dasharray
attribute do draw a dashed line. Add the following lines to the page's CSS section:
#boundary path {
stroke-dasharray: 3 5;
}
path {
fill: none;
stroke: black;
}
And inside the SVG tag, just before <g id="states"></g>
, add:
<g id="boundary"></g>
To draw the coastline shape in the SVG group we just created, we need D3 to select it. Replace the variable initialization code (the code at the top of renderMap.js
that defines svgStates
) with the following lines:
var svgStates = d3.select("svg #states"),
svgBoundary = d3.select("svg #boundary"),
states = {},
startYear = 1790,
currentYear = startYear;
Then, just before we load states.json
, add the following JavaScript:
d3.json("data/usa.json", function(error, boundary) {
svgBoundary.selectAll("path")
.data(boundary.features)
.enter()
.append("path")
.attr("d", path)
});
I'm sure that part does not need explaining anymore! Let's have a look at the map!
After this step, your map should look like this:
- Screenshot:
First our map was black, and later it turned white with a gray outline. Now, it's time for some colors. D3 has great color scales built in, but it's not too easy to make sure two neighbouring states do not get the same colors. Algorithms which makes sure this won't happen do exist, but it's much easier to manually assign each state with its own color (Mike Bostock does this as well in his Let’s Make a Map tutorial).
I've created a separate file, colors.js
, which contains a color for each state. I've used colors from the D3 scales, but you should also have a look at ColorBrewer.
Include the colors file in your index.html
ABOVE WHERE YOU INCLUDE THE renderMap.js
file:
<script src="static/colors.js"></script>
<script src="js/renderMap.js"></script>
Finally, add four lines of JavaScript after .attr("d", path)
in the renderMap.js
file, where you are rendering the states (and make sure to remove the semicolon on the previous line). The code should look like this:
svgStates.selectAll("path")
.data(state.features)
.enter()
.append("path")
.attr("d", path)
.style("fill", function(d, i) {
console.log("d is ", d)
var name = d.properties.STATENAM.replace(" Territory", "");
return colors[name];
});
- Sets the
fill
SVG style attribute of each separate state - Gets the
STATENAM
property from the GeoJSON object, and removes the string" Territory"
from the name (that way, we don't have to worry about state names like Iowa Territory but we can use Iowa instead!) - Looks up the state name in the colors list
After this step, your map should look like this:
- Screenshot:
It would be great if we could see the names of the states! Luckily, this is easy with the SVG title element!
Add the following code to renderMap.js
where you are rendering states, after the style()
function:
.append("svg:title")
.text(function(d) { return d.properties.STATENAM; });
Afterwards, the code should look like this, with the svg:title
part in the end:
svgStates.selectAll("path")
.data(state.features)
.enter()
.append("path")
.attr("d", path)
.style("fill", function(d, i) {
console.log("d is ", d)
var name = d.properties.STATENAM.replace(" Territory", "");
return colors[name];
})
.append("svg:title")
.text(function(d) { return d.properties.STATENAM; });
After this step, your map should look like this:
- Screenshot:
You're done! Great job! Remember to publish to github pages and to submit a link to your repo to Canvas.