TopoJSON, GeoJSON, and Projections: Developing Interactive Map Visuals for Web Apps
If you didn’t like maps before, you probably still won’t — but hear me out
A few weeks ago, I took up a small proof-of-concept project as a follow-up to learning the D3 library.
I had just finished a stint in data visualization, and it occurred to me it’d be exciting to be able to carry these visuals into an even more relatable real-world format. Two weeks prior, I had visited the Museum of Modern Art in New York and saw a display in their 21st century–tech exhibit, where the artist had animated wind patterns over the United States across the years.
It struck me how elegant the implementation looked and found myself imagining how they did it. The more I thought about it, the more difficult it seemed to me. I left the museum that day impressed, with a cherished memory of a piece of code elegant enough to be exhibited as art.
Somebody out there was able to take gross collections of wind data, process them down to their position and directionality, and place them over a map of the US in an elegant, easy to follow visual. It was clean, it was relatable, and it was nice to look at. People liked it so much they even put it in a museum.
This experience, combined with my exploration of the D3 library, inspired me to look into mapping data in geographic frameworks. It’s a concept we see every day: GPS routing in Google Maps, weather patterns mapped on the news, and local restaurants/stores dropped as pins in your area. Mapping data to geography is powerful for both its ease of interpretation and relatability. We’re exposed to maps so often nowadays we recognize their context at a glance — and relate them to ourselves almost immediately.
So I set up a project for myself, simple at first but easy to expand on: I wanted to write a web app that rendered a map of the globe that responded to my actions and supported plotting of data based on latitude and longitude. Over the few days I worked on it, I was able to learn about a variety of tools and resources for coding with maps, but I want to highlight two in particular that I think are the most important for getting started: GeoJSON/TopoJSON file formats and map-projection libraries.
GeoJSON vs. TopoJSON: What Are They?
When trying to render a map, the first and most important resource you need is, unsurprisingly, the map. There’s an almost absurd number of APIs and formats out there that store data for map drawing, but two of the most prevalent (and the ones I used) are TopoJSON and GeoJSON files.
So what are they? Well, TopoJSON files are a derivative of GeoJSON, so let’s start with the parent.
GeoJSON
GeoJSON files are a format for storing map shapes in JavaScript Object Notation (JSON). They’re often formatted in a Feature Collection
for interpretation by coding libraries and contain arrays of attributes to draw (each attribute is called a Feature
).
GeoJSON features have two keys: geometry
and properties
.
The properties
key stores general information often used for the identification/labeling of the feature.
The geometry
key is the more critical part for rendering. It stores the type of geometry the Feature
is and a set of coordinates, the format of which varies depending on the geometry type. A Feature
’s type ranges through: Point
, LineString
, Polygon
, MultiPoint
, MultiLineString
, and MultiPolygon
.
From these types, any arrangement of visual properties can be generated. Rendering libraries use this type in combination with the collection of coordinates to draw the map features correctly with respect to each other.
GeoJSON files are notable for their relative ease of readability and the convenience of being able to extract/add/transfer features to and from a collection as easily as a simple copy-paste. Every shape drawn on a map from a GeoJSON has a corresponding feature and can be manipulated as such.
The downside of GeoJSON, however, comes from the same principle — that every shape in a GeoJSON is its own feature. As a result, a lot of borders between adjacent features end up getting stored in memory twice/thrice/four times. GeoJSON files, especially for detailed layouts like the United States map below, are notoriously large, and they aren’t always best for the ease of transfer over a network.
That’s where GeoJSON’s cousin, the TopoJSON file, comes in.
TopoJSON
TopoJSON files are similar to GeoJSON in their storage as JavaScript objects.
However, in terms of readability, TopoJSON files are very, very mean. Below I’ve copied a rough depiction of a TopoJSON object logged to the browser console. The general gist is this: Instead of storing each feature as its own shape, TopoJSON files instead store every line/edge on a map as an arc
.
They also store a collection of objects to be drawn as a Geometry Collection
that references back to the arc array. For readability, it’s a nightmare of arrays in arrays of arrays and nested geometry objects referencing indexes in the nested arrays. I wouldn’t bother trying to make sense of it unless you’re very interested in the format, but the syntax does have some major benefits.
Because TopoJSON files construct the shapes from shared edges instead of tracking each individually, they’re often an order of magnitude smaller in file size than a GeoJSON file referring to the same map. For example, the TopoJSON file storing the geometry for the above United States map is about one-fifth the size of its corresponding GeoJSON. For portability and transferring, TopoJSON files come out on top, especially as your maps get more complicated.
Many rendering libraries require GeoJSON-esque file formats to draw, but plenty of tools exist for translating between the two. For example, in JavaScript, the topojson-client
library has a Feature
function that extracts GeoJSON features from TopoJSON objects, allowing for storage in the smaller file size and the handling benefits of the GeoJSON format.
So now that we have our source for the map, how do we draw it? Or, more accurately, how do we want to draw it?
Map-Projection Libraries
Map-geography files store features, coordinates, and geometries in terms of the geographic-coordinate system — i.e., they orient everything based on latitude, longitude, and its spatial position on a globe.
Unfortunately for us, computer screens are not spherical — they’re flat. So to fix this problem, we end up exploring the world of map projections and the code libraries that support them.
What is a map projection?
At its simplest, a map projection is a way of flattening coordinates for drawing on a map. It’s easy enough to follow but potentially very difficult to implement from scratch.
Map projections typically flatten, compact, and distort map features to fit them to a surface that can be unrolled onto a flat page.
For example, the Mercator Projection, one of the most common projection types used in schools, is a cylindrical projection. Cylindrical projections are some of the most common since they tend to maintain map accuracy around the most heavily populated areas of the globe.
However, there are hundreds of other options with various benefits. For example, below is an animation of the Dymaxion Projection, one of my personal favorites. It’s notable for extremely low degrees of map distortion (for those interested) and almost completely avoids land breaks in its unrolling. It’s great for mapping human-migratory patterns.
But back to the point: How do we take our map files and generate flat visual projections for rendering on screen? Our answer is libraries-code
libraries.
People out there who are much better than me at math have already put together the functions and operations required to translate longitude/latitude to 2D x and y positions, and they’ve included those functions in code libraries. Almost every major language will have libraries built for this purpose, open source and otherwise. Since we are working with a JSON format, ideally we’d use one in JavaScript. Fortunately, I found out D3 has a sublibrary, d3-geo, dedicated to just this.
With this library, we’re able to translate map data to any 2D projection supported by D3. We finally have all the tools and knowledge needed to start handling, rendering, and manipulating our maps. That being said, let’s get to building our globe.
Building a Globe
To generate my globe, I built the application in a React framework, partially as a test and partially to use the responsiveness of React state as a way to interact with my map dynamically.
To start, I rendered a SVG in a component I named Canvas
to contain all the downstream paths I’d need to draw for the globe features. Then, I rendered a child component called Globe
that handled the generation of the map features.
Inside the SVG, we can begin the steps for drawing. First, I imported my TopoJSON file into the component, then used topojson-client
to extract the features into the GeoJSON format.
This will store our features in a new array we’ve named data
. These features are what we can plug into D3 to generate paths for drawing.
To do this, we first use d3-geo
to generate a projection function. To test, we’ll start with a Mercator Projection. Once we configure our projection function, we pass it into another d3-geo
function, geoPath
. This will give us back another function that accepts a GeoJSON feature and returns a path attribute. If this is difficult to follow, please feel free to reference the code below.
Now, path
will store a function for generating our SVG paths. Using this, we can pass path each of our GeoJSON features, and it’ll return us the d attribute of a path.
To implement this, I made a Continent
component that excepted one feature as a datum
prop. Using this prop and my path
generator, I could render paths for each of my features in the collection. For example:
Notice because each continent path
is its own component, I’m able to add event listeners to each continent specifically and make them respond uniquely. Combining all these functions, I can draw all of my features to my SVG and make them do fancy things. Like so:
Now we’ve got our paths rendering, but there’s still some work to do. First of all, it’s not quite a globe. Second, it’s not rotating. Third and last, we need to make sure we can render visuals to the globe appropriately based on lat/long. The first is actually quite an easy fix, and the second follows along smoothly.
d3-geo
has an orthographic projection type, so rendering as a globe is as simple as changing our projection function.
However, this will only show us the front part of the globe, covering anything that should appear behind it. We can compensate for this by adding a rotation configuration to our projection that turns the map on three axes. By customizing these values, we can change the part of the globe that’s facing us. Furthermore, if we make this rotation value dependent on state, we can leave it up to the user to turn the globe as they please.
We’re nearly there. With rotation dependent on state, adding autorotate is as simple as setting a timer/interval to increment the state periodically. Lastly, we need to make sure we can render data points on the map corresponding to appropriate lat/long.
Because of the way we implemented this map, this turns out to be surprisingly easy. As long as we’re able to generate an appropriate GeoJSON object, we can actually just pass it into the same path generator we used to make the continents. The projection and path-generation functions will handle all the difficult parts for us, and GeoJSON formats are much easier to custom create and manipulate versus other formats.
As it turns out, d3-geo
comes to our rescue again and provides us with a method to generate GeoJSON circles with only the lat, long, and radius. Using the geoCircle
function, we can custom create our circles, like so:
With this, we’ve generated two circles and their shadows for visual effect: circle1
at 0, 0
and circle2
10 degrees longitude to the east.
We can draw our circles with a higher scale value in our projection to make them appear raised off the surface of the globe. Then we can draw the shadows on the same scale as the globe to make them appear cast by our flying saucers.
In the end, you’ll have something like this:
It’ll render our globe with aesthetically pleasing, accurate visuals.
Conclusions
Map generation and data plotting have become small passions of mine over the past month, and I’m working on a few projects using these techniques to consolidate understanding and test limits. As I first began to explore mapping and geographic rendering, I didn’t have any idea exactly how deep it could go. It’s an incredibly deep rabbit hole to dive into, but playing around with projection types and dynamic map rendering has been some of the most fun I’ve ever had while coding.
Understanding map-data formats and being able to traverse them are incredibly powerful skills, and I strongly encourage anyone with an interest in this material to explore its libraries and resources. D3’s website has dozens of examples of similar implementations.
I’ll link the Git repo for my topojson-test
project below, and I hope I’ve inspired some of you to take up similar projects as a challenge for yourself. Thank you for reading, and good luck!