Introduction to Digital Cartography: GeoJSON and D3.js

There’s something so satisfying about organized data, particularly in the form of a great visualization. Coming from an Environmental Studies background, I get particularly excited about spatial and geographic data and can easily spend hours on Google Maps for entertainment. Throw a good data set into the mix (plus maybe a tour of the Grand Canyon) and there goes my entire weekend.

I’ve always loved those fascinating, shaded maps — known as choropleths — that you see online showing election results (the New York Times always has great examples) and originally set out to learn what it takes to make one in JavaScript. These choropleth maps — used for visualizing any statistic that can be aggregated within a geographic boundary — are just one of many visualizations you can create using D3.js (Data-Driven Documents).

I quickly realized that I needed to explore exactly how to make the base map itself before I could start adding data to it. In this post, I’ll discuss some of the resources and challenges involved with digital cartography and how to get started by building a world map.


Data is everything in digital cartography and comes in a few additional formats beyond the ubiquitous JSON. We’ll review two of these — GeoJSON and TopoJSON — and their advantages and use cases.

GeoJSON

JSON data takes the form of a standard JavaScript object, nested to group data (“values”) by common parents (“keys”). This key-value format is second nature to any JavaScript developer and the basis of object-oriented programming. GeoJSON follows the same structure, but requires that the object contain the keys type, geometry, and properties.

There are two values for type — Feature and FeatureCollection. Below is an example of a basic Feature object.

{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-
74.0060, 40.7128]
},
"properties": {
"name": "New York City"
}
}

FeatureCollection objects are essentially wrappers for organizing multiple Features into one group.

{
"type": "FeatureCollection",
"features": [ ** array of Feature objects **]
}

The geometry key is where things get interesting. GeoJSON is used for geographic data that we can plot on a map, but in order to do so we need to declare a geometric shape that best suits the geography data we’re trying to store. There are six geometry types available, which I find are best understood with the below graphics.

Basic geometries by Wikipedia
Advanced or “multipart” geometries by Wikipedia

If we were to write a GeoJSON object describing Colorado, a polygon geometry would likely be enough to draw a single, contained outline. Hawaii, on the other hand, would require a multi-polygon geometry to include its many islands in the same shape. These six geometry types make GeoJSON objects quite useful for simple geographic features such as outlining a single country, but become complicated the moment we need to add a neighboring country or map an entire continent, which is where TopoJSON comes into play.

TopoJSON

When we need to hold data for more advanced or detailed geographic features, we use an extension of GeoJSON called TopoJSON that contains additional coding for topology.

Geospatial topology studies the rules and relationships between the points, lines, and polygons representing the features of a geographic region.

To encode North America with GeoJSON, we would need to use the simple geometries we learned above to draw Canada, the United States, and Mexico individually and put the shapes next to each other. But we shouldn’t do this because it results in inefficiency and redundancy by overlapping lines at the borders.

Instead, TopoJSON takes into account these shared lines (called arcs) and builds a complete feature from this information that can be stored in a single file. It’s essentially a form of memoization only instead of storing the results of a function in a cache for repeated use, we’re storing a geographic border in the form of an arc.

What we gain in efficiency we lose in simplicity when writing TopoJSON files. The example below is the code necessary to construct two adjacent rectangles, and is surprisingly confusing to a budding digital cartographer.

{
"type":"Topology",
"transform":{
"scale": [1,1],
"translate": [0,0]
},
"objects":{
"two-squares":{
"type": "GeometryCollection",
"geometries":[
{"type": "Polygon", "arcs":[[0,1]],"properties": {"name": "Left_Polygon" }},
{"type": "Polygon", "arcs":[[2,-1]],"properties": {"name": "Right_Polygon" }}
]
},
"one-line": {
"type":"GeometryCollection",
"geometries":[
{"type": "LineString", "arcs": [3],"properties":{"name":"Under_LineString"}}
]
},
"two-places":{
"type":"GeometryCollection",
"geometries":[
{"type":"Point","coordinates":[0,0],"properties":{"name":"Origine_Point"}},
{"type":"Point","coordinates":[0,-1],"properties":{"name":"Under_Point"}}
]
}
},
"arcs": [
[[1,2],[0,-2]],
[[1,0],[-1,0],[0,2],[1,0]],
[[1,2],[1,0],[0,-2],[-1,0]],
[[0,-1],[2,0]]
]
}

Luckily, most common TopoJSON files already exist and are readily available for developers to use. Here is an excellent GitHub resource by Mike Bostock, the creator of D3.js, for learning more about TopoJSON including pre-built United States and Worldwide atlas files. Understanding TopoJSON files brings us another step closer to drawing a map in our browser, but let’s first have a brief word on map projections.

Map Projections

If we were planning to map a relatively small geographic area such as Manhattan or New England, we probably wouldn’t bother as much with our map’s projection. But if we want to map the entire world, it’s definitely something we’ll have to take into strong consideration.

Map projections enable us to draw a spherical object (Earth) onto a flat surface (in this case, our browser window). All projection types have a trade-off between some feature’s accuracy and another’s distortion and it’s usually up to the cartographer to choose the best one for the project. I won’t go into detail here about the different projection types, but I recommend reviewing these d3-geo images for examples rendered using the D3.js library.

Creating a World Map with D3.js

D3.js is a JavaScript library for creating data visualizations that works by attaching data to HTML elements in a way that can then be manipulated using CSS. In the case of cartography, that means drawing the map as a scalable vector graphic (SVG) or a canvas element.

Right now we have a TopoJSON file that we want to turn into a map we can view in our browser. Let’s start by creating an HTML file and adding script tags to the head to connect the D3.js and TopoJSON libraries. We’ll also add some simple CSS styling to use later.

<head>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<style>
path {
fill: lightgray;
stroke: #000;
}
    .graticule {
fill: none;
stroke: #ccc;
stroke-width: .5px;
}
    .foreground {
fill: none;
stroke: #333;
stroke-width: 1.5px;
}
</style>
</head>

Add an SVG element and script tag to the body for our D3.js manipulations.

<body>
<svg width="960" height="600"></svg>
<script>
// D3 JavaScript will go in here
</script>
</body>

Inside the above script tag, declare and initialize a few variables.

const svg = d3.select("svg")
const myProjection = d3.geoNaturalEarth1()
const path = d3.geoPath().projection(myProjection)
const graticule = d3.geoGraticule()

Let’s walk through what these are doing:

  1. Select our SVG element so that we’ll be able to attach our map to it later and have it render in the browser.
  2. Initialize your chosen map projection.
  3. Initialize a new geoPath using our desired projection. (SVG elements use a path attribute to hold instructions for drawing shapes — we want ours to be geographic.)
  4. Add a graticule to display lines of latitude and longitude (optional).

Below our variable declarations, we’ll write a drawMap function that will be used later as a callback.

function drawMap(err, world) {
if (err) throw err
  svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
  svg.append("path")
.datum(graticule.outline)
.attr("class", "foreground")
.attr("d", path);
  svg.append("g")
.selectAll("path")
.data(topojson.feature(world, world.objects.countries).features)
.enter().append("path")
.attr("d", path);
}

Let’s walk through the code:

  1. Establish an error handler for our callback function.
  2. Add a <path>* element inside the <svg> element and attach the graticule variable we declared earlier as its data. Then assign it the .graticule class style and add the d* attribute set to our path variable.
  3. Add another <path> element inside the <svg> element to draw an outline around our world map by attaching the graticule outline as data. Then assign it both the .foreground class and d attribute as the same path.
  4. Add a <g>* element inside the <svg> element to hold our countries. Use topojson.feature() to convert the countries object on the world argument from TopoJSON to a GeoJSON FeatureCollection and isolate its features property using dot notation. Remember from earlier that this will be an array of individual GeoJSON features — .enter() is essentially forEach() in D3.js terms and combined with .append(“path”) plus the final line will create a new <path> element for each feature (country) created by the .data method and include our path variable on each one’s d attribute.

*For more information about <svg> elements and their children, read through MDN’s documentation and examples.

We have two things left to do before we can view our map — bring in our TopoJSON world file and call the drawMap function — and the d3.json() method will do exactly that. It takes two arguments: a URL to our desired JSON file and a callback to execute once the file is loaded. We’ll use the basic world map found here for our JSON file.

d3.json("https://unpkg.com/world-atlas@1.1.4/world/110m.json", drawMap)

And that’s it! Run your HTML file on a server (I recommend http-server) and you should see the below map in your browser. Congratulations!

The complete code from this post can be found here.