Topic 13.1
Geographic Data: GeoJSON and TopoJSON
In one line
A map is data first: a list of places, each with shapes made of [longitude, latitude] points. GeoJSON writes every shape out in full, TopoJSON stores each shared border once, and a ring written in the wrong direction can make a small island fill the whole world.
Think of it like this
A jigsaw puzzle of the island. Each piece is a district. Two neighbouring pieces share an edge. If every piece is described on its own, the shared edge is written twice. If you describe the edges once and say which pieces use them, the list is shorter and the pieces can never drift apart.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- GeoJSON
- A JSON format for places: features with a geometry (a shape made of [longitude, latitude] points) and properties.
- TopoJSON
- A smaller form of GeoJSON that stores each shared border once, as an arc, and lists which arcs each shape uses.
- longitude, latitude
- East-west and north-south position on the Earth, in degrees. In GeoJSON the order is [longitude, latitude].
- ring
- A closed outline: a list of points whose last point repeats the first. A polygon has one outer ring and any number of hole rings.
- winding
- The direction a ring is listed in, clockwise or anticlockwise. D3 uses it to decide which side of the ring is inside.
- arc
- In TopoJSON, one piece of line that one or more shapes share.
- mesh
- A single line object made of the borders between shapes. Drawing it once gives neat borders without drawing every edge twice.
Step by step
01Look inside the TopoJSON file
The first example fetches ardenvale.topojson and prints its parts. It has three objects: districts (6 polygons), coast (the island's outline) and river (a line). They share 33 arcs, and a bbox that says the island lies between −30.645° and −30.553° east-west.
The points are stored as small steps. The transform holds the scale and starting point that topojson.feature uses to turn them back into degrees.
02Turn it into GeoJSON
topojson.feature(topo, topo.objects.districts) returns a FeatureCollection with 6 features. Each feature keeps its id (such as harbour) and properties.name. The first point of Harbour is about −30.6024, 39.3703: longitude first, a negative number because Ardenvale is west of Greenwich.
const districts = topojson.feature(topo, topo.objects.districts); // FeatureCollection with 6 features03Compare the sizes, and the two ways to fail
The same example prints the size of the file against the GeoJSON made from it, for Ardenvale and for the world. It then tests a station's point in Harbour both ways, [lon, lat] (inside) and [lat, lon] (not inside anywhere), and loads a wrong file name to show the 404 message.
04Find the borders, then break a ring
The second example calls topojson.mesh twice: once for the borders between two districts, once for the outer edge. Then it reverses Harbour's ring and asks D3 for the area. The right ring is 8.0 km². The reversed one is 510 million km², the whole planet.
The picture shows what the browser draws with each: the good one a small shape, the bad one a full box of colour.
Try it yourself
- 1
Reverse a different district
In the second example, change
districts.features[0]todistricts.features[5](Westpark). The reversed area is the whole Earth again, minus 4.1 km² instead of 8.0, andfixWindingbrings it back to 4.1. Every district fails the same way. - 2
Count the arcs of one border
In the second example, change the filter of
innerto(a, b) => a !== b && a.id === "harbour". Only the borders of Harbour with a neighbour remain: the log says1 linesinstead of5 lines(the borders of Harbour join into one line). - 3
Read another object
In the first example, replace
topo.objects.districtsin thefeaturecall withtopo.objects.river. The log now saysFeatureCollection with 1 features: river. The next line then stops withTypeError: harbour.geometry.coordinates[0][0].map is not a function: a river is aLineString, a plain list of points with no rings and no inside, so the polygon code no longer fits. Puttopo.objects.districtsback.
Code & diagrams
Station S01 (Quay) is one of the CSV's rows. Ardenvale is a made-up island, so no real map shows it.
Preview
Console
Nothing logged yet.Expected console output
type: Topology | objects: districts, coast, river
shared arcs: 33 | bbox: -30.64517, 39.36961, -30.55277, 39.43066
FeatureCollection with 6 features: harbour, old-town, university, riverside, hillcrest, westpark
harbour Polygon first point [lon, lat]: -30.6024, 39.3703
Ardenvale: 2684 characters as TopoJSON, 6623 as GeoJSON
World 1:110m: 107760 characters as TopoJSON, 436088 as GeoJSON, 177 countries
[lon, lat] = -30.59415, 39.39147 -> Harbour
[lat, lon] = 39.39147, -30.59415 -> nowhere
Could not load ardenvale.json (the server answered 404).Reversing a ring is also how to see the bug without a broken file. In a real project the cause is a file made by another tool.
Preview
Console
Nothing logged yet.Expected console output
borders between districts: 5 lines | outer edge: 1 line | merged: MultiPolygon
Harbour as stored: 8.0 km2
Harbour reversed: 510,065,873 km2 | the whole Earth: 510,065,881 km2
After fixWinding: 8.0 km2 | unchanged one: 8.0 km2
Pixels covered (the box has 27,000): good 3,909 | bad is larger than the whole box: trueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Load a file that is not there
In the first example, change load("ardenvale.topojson") on the line that starts const { text, json: topo } to load("ardenvale.json").
Break #2
Swap longitude and latitude
In the first example, change the first where([quay.lon, quay.lat]) to where([quay.lat, quay.lon]) only (leave the second line).
Myth vs fact
Myth
TopoJSON is a different kind of map data.
Fact
It is GeoJSON stored smaller. topojson.feature turns it back into ordinary GeoJSON, and everything after that is the same.
Myth
The order is latitude, longitude, because that is how people say it.
Fact
GeoJSON, D3 and most mapping libraries use [longitude, latitude]. Some libraries (Leaflet) use the other order for their own objects, so check each tool.
Myth
If the map shows a coloured rectangle, the data file is broken.
Fact
More often only the ring direction is flipped. The data is fine and the area test above finds it.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
d3.geoAreareturns steradians on a unit sphere. A shape over 2π (a hemisphere) is almost certainly wound the wrong way, which makes a one-line check in a loader. Tools such as@turf/rewindandgeojson-rewindfix the direction for a whole file. - ▸
TopoJSON quantisation rounds every point to a grid, so very fine details of shared borders can move by a fraction of the grid step. The grid here is about 0.0000009 degrees, around 10 centimetres. World maps use a much coarser grid (0.0036 degrees, about 400 metres), which is why a 110m map is for world views and not for a city.
- ▸
topojson.meshis also faster to draw: onepathfor all inner borders replaces thousands of segments drawn twice, and anti-aliasing no longer leaves faint seams between neighbours. - ▸
Gzip or brotli shrink GeoJSON and TopoJSON a lot because coordinates repeat digits, but the browser still has to parse the whole thing. Simplify geometry (mapshaper,
topojson-simplify) for the zoom level you display.
Remember this
- 1
The problem this solves: a drawing tool needs shapes, and shapes on the Earth need agreed rules: which number comes first, how the outline is listed, and what "inside" means. GeoJSON is the common format for those rules. TopoJSON is a smaller form of it. Without the rules, maps come out upside down, empty, or painted over the whole planet.
- 2
GeoJSON is plain JSON with fixed words. A
Featurehas ageometry(the shape),properties(a name, a number, anything) and often anid. AFeatureCollectionis a list of features. The geometry types arePoint,LineString,Polygonand theirMulti…forms. APolygonis a list of rings: the first ring is the outer edge, any others are holes. A ring is a list of points that ends where it starts. - 3
**A coordinate is
[longitude, latitude], in that order.** Longitude (east-west, −180 to 180) comes first, so it behaves likex. Latitude (north-south, −90 to 90) comes second, likey. Many map apps and spoken English say "latitude, longitude", so swapping them is the most common mistake. For Ardenvale, about 30.6° west and 39.4° north, the point is[-30.6, 39.4]. Swapped,[39.4, -30.6]is 39.4° east and 30.6° south: a spot in the Indian Ocean, thousands of kilometres away. - 4
TopoJSON stores shared borders once. The file has a list of
arcs(pieces of line), and each shape lists the arc numbers it uses; a negative number means "this arc backwards". The points are also stored as small whole-number steps (transformgives the scale and the starting point), which shrinks the file. A browser cannot draw TopoJSON directly:topojson.feature(topology, object)turns an object into GeoJSON, andtopojson.mesh(...)returns just the borders. - 5
Ring direction decides what is inside. On a sphere, a ring splits the globe into two parts, and a flat rule cannot say which is "inside". D3 uses the winding: with the points going clockwise (seen from above the surface), the inside is the small part. A ring listed anticlockwise means "everything except this island", so D3 fills the whole world and the island becomes a hole. This is the famous "my map is all one colour" bug. GeoJSON files made for flat maps often have the other direction, and D3 does not fix it for you.
- 6
File size is a real limit. Ardenvale is small: 2,684 characters as TopoJSON, 6,623 as GeoJSON. The world at 1:110 million (very rough borders) is 107,760 characters as TopoJSON and 436,088 as GeoJSON, four times more. Finer world maps (1:50 million, 1:10 million) are many times bigger still. Ship TopoJSON, simplify it for the size the map will be shown at, and let the server compress it.
Explain it without notes
What is the order of a coordinate in GeoJSON, and what goes wrong if you reverse it?
What does TopoJSON store that GeoJSON does not, and why is the file smaller?
Why does a polygon with the wrong winding fill the whole map in D3?
How would you detect a wrongly wound polygon in code?
Practice
Write loadGeo(name) that fetches a TopoJSON file, throws a clear error on a bad status, and returns the first object's features. Test it with the correct and a wrong name.
Write checkStations(stations, districts) that returns the list of station ids not inside the district their CSV row names. Run it on the real stations; then swap one station's coordinates to see it found.
Trade-offs
- ↔
TopoJSON is smaller but needs
topojson-client(a small library) and one conversion step. For a handful of shapes, plain GeoJSON is simpler. - ↔
Simplifying a map shrinks it and speeds it up, but borders shared between countries stay aligned only if you simplify the topology (not each GeoJSON shape on its own).
Done when you can
I can say what is in a GeoJSON feature and in what order a coordinate is written.
I can turn TopoJSON into GeoJSON and draw only the borders with
mesh.I can explain why a ring in the wrong direction fills the whole world, and test for it with
geoArea.I can check that a file loaded (
response.ok) before I use it.I can say why a world map file must be simplified for the size it is drawn at.