Topic 13.2
Projections and geoPath
In one line
A projection turns [longitude, latitude] on a ball into [x, y] on a flat screen, and every way of doing it bends something: shape, area, distance or direction. geoPath draws projected shapes, and fitSize makes a map fill its box.
Think of it like this
Peeling an orange and pressing the peel flat. It cannot lie flat without tearing or stretching. You choose where it stretches. Flatten it one way and the poles become huge. Flatten it another way and the edges curl. No way keeps everything true.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- projection
- A function that turns [longitude, latitude] into [x, y] pixels. Every projection bends something.
- conformal
- Keeps angles and small shapes right (Mercator). It enlarges areas away from the equator.
- equal-area
- Keeps areas in true proportion (Equal Earth), at the cost of some bent shapes.
- geoPath
- D3's function that turns GeoJSON into an SVG path string, using a projection.
- fitSize
- A projection method that sets scale and translate so a shape fills a width-by-height box.
- graticule
- The grid of latitude and longitude lines, drawn so a viewer can see how the projection bends the world.
- clipping
- Cutting away the parts of a shape that cannot be drawn, such as the far side of a globe.
Step by step
01Start with the coast, fitted to the box
The map begins with a projection and one path. d3.geoMercator().fitExtent(...) is told to fit the coast inside a 360 × 300 box with an 8-pixel margin. d3.geoPath(projection) turns the coast into one path string. The island appears, filling the box.
02Add the six districts
One path per district, drawn with a join (Phase 5). A pastel colour scale tells them apart, and a second path, from topojson.mesh, draws the white borders once.
03Add the river
The river is a line, not an area, so it is stroked with fill: none. Without fill: none, the browser would close the line and fill the shape between its ends.
04Add the stations: projecting points
Points are not shapes, so there is no path for them. Each station goes through projection([lon, lat]), and the two numbers become cx and cy. Those are the same two lines the dashboard map uses in Topic 13.7.
05See what each projection bends
The first example draws the world three times with the same data, and measures Greenland against the Democratic Republic of the Congo in each picture. The Mercator picture shows 15.39, Equal Earth 0.94 (the truth) and Orthographic 0.22, because Greenland sits near the globe's edge, where shapes are squeezed.
06Fit, project and meet the edge cases
The second example prints what the default projection does to Ardenvale (a dot), what fitSize and fitExtent change, how one station goes to pixels and back, and what happens with the poles, the far side of a globe, a single point and no data.
Try it yourself
- 1
Change the projection of the island
In the second example, replace
d3.geoMercator().fitSize([W, H], districts)(the line that makesfitted) withd3.geoNaturalEarth1().fitSize([W, H], districts). The island is a little wider in the picture, and Quay moves from x = 274.4 to 263.8, about 10 pixels. - 2
Rotate the globe
In the first example, change the orthographic projection to
d3.geoOrthographic().rotate([-30, -40]).fitSize([200, 200], sphere). The globe is now centred on the Atlantic, near Ardenvale. The last line prints 0.88 instead of 0.22: Greenland is no longer at the edge, where shapes are squeezed. - 3
Fit a station box
In the second example, add
const west = stations.filter((s) => s.district === "Westpark");and fit a projection to{ type: "MultiPoint", coordinates: west.map((s) => [s.lon, s.lat]) }instead of the districts. Three points give a finite scale (about 2,467,518) and a box from y = 119 to 281.
Code & diagrams
Antarctica is cut off in the Mercator picture by the square the projection clips to. The Equal Earth figure matches the true ratio.
Preview
Console
Nothing logged yet.Expected console output
True area: Greenland is 0.94 times the Democratic Republic of the Congo
Mercator | Greenland looks 15.39 times the Congo
Equal Earth | Greenland looks 0.94 times the Congo
Orthographic | Greenland looks 0.22 times the CongoThe numbers are pixels in a 500 × 400 box. A projection returns numbers for any point, so nothing in this example throws: you have to look at what comes back.
Preview
Console
Nothing logged yet.Expected console output
default projection, island bounds: 398, 135, 398, 136
fitSize 500x400, bounds: 16, 0, 484, 400 | scale: 290,085
fitExtent with a 20 px margin, bounds: 39, 20, 461, 380
Quay [lon, lat] -> [ '274.4', '256.8' ] pixels | the centre pixel is [ '-30.5990', '39.4001' ] [lon, lat]
Mercator y pixel at latitude 60, 85, 90, -90: 49, -229, -5460, Infinity
Orthographic, [150, 0] is on the far side: [ 604.75, 250 ] | visible: false
fitSize on one point: Infinity
fitSize on nothing, then draw nothing: path is null
Mercator | width 467.8 x height 400.0
Equal Earth | width 456.6 x height 400.0
Natural Earth | width 481.3 x height 400.0Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Fit to a single point
In the second example, before the line const none =, add const lone = d3.geoMercator().fitSize([W, H], { type: "Point", coordinates: [quay.lon, quay.lat] }); and d3.select("#map").append("path").attr("d", d3.geoPath(lone)(districts.features[0]));.
Break #2
Forget to fit
In the second example, change the line const fitted = ... to const fitted = d3.geoMercator(); (no fitSize).
Myth vs fact
Myth
Mercator is the correct map and the others are distorted.
Fact
Every flat map distorts. Mercator is the right choice for navigation, because it keeps compass bearings straight, and a poor one for comparing sizes.
Myth
On a small island the projection never matters.
Fact
It matters little for the outline, as the width table shows. It still matters for any measurement in pixels. Use d3.geoArea and d3.geoDistance for real areas and distances, never the pixel sizes.
Myth
projection(point) returns null when a point cannot be seen.
Fact
For a whole shape, geoPath clips. For a point, projection returns numbers anyway, as the far-side line shows.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
fitSizemeasures the shape with the projection's current scale and then adjusts it, so the order matters: setrotate()orcenter()first, and fit last. A fit before a rotate is thrown away. - ▸
Mercator's clip square is what limits the world to about 85.05° of latitude. This is why Web map tiles (Google, OpenStreetMap, Mapbox) are square and stop short of the poles.
- ▸
geoPathwith no projection is the way to draw a map that is already in pixels. Some files (the US Censusus-atlasones) are pre-projected, and projecting them again squashes the map into a corner. - ▸
Projection functions are cheap, but
fitSizeruns over every point of the shape. Do it once per size change, not on every frame of an animation.
Remember this
- 1
The problem this solves: the screen is flat and the Earth is not. A projection is a function: it takes
[lon, lat]and returns[x, y]in pixels. Every projection changes something, so you pick the one whose lie hurts your question the least. - 2
What gets bent. Mercator keeps shapes and directions right in small places, but stretches areas towards the poles: Greenland looks 15.4 times the size of the Democratic Republic of the Congo, when it is really 0.94 times. Equal Earth keeps areas true and bends shapes a little. Orthographic looks like the globe seen from space, so it is honest about shape in the middle and squeezes the edges; the far half is hidden.
- 3
D3 projections are objects with settings.
d3.geoMercator(),d3.geoEqualEarth(),d3.geoNaturalEarth1()andd3.geoOrthographic()all work the same way.projection([lon, lat])returns[x, y].projection.invert([x, y])goes back..scale(),.translate(),.center()and.rotate()set where and how big. The defaults are made for the whole world in a 960 × 500 box, so a small island drawn with the defaults is a dot. - 4
**
fitSizeandfitExtentfind scale and translate for you.**projection.fitSize([width, height], geojson)makes the shape just fill the box.fitExtent([[x0, y0], [x1, y1]], geojson)fills a box with a margin. They change the projection in place and return it. Give them real shapes:fitSizeon a single point gives a scale ofInfinity, because a point has no size. - 5
**
geoPathdraws.**d3.geoPath(projection)returns a function that turns a GeoJSON shape into an SVG path string, sopath(feature)is thedattribute. It also measures:path.area(f),path.bounds(f)andpath.centroid(f)are in pixels. Without a projection,d3.geoPath()treats coordinates as pixels already (used for pre-projected maps). - 6
Projecting a point by hand does not clip.
projection([lon, lat])always returns numbers, even for a point on the far side of an orthographic globe, where it lands in a mirrored spot.geoPathclips shapes to the visible side, but plain point projection does not. For globes, test visibility withd3.geoDistance(point, centre) < Math.PI / 2. Also, Mercator stretches without limit: in the default Mercator, latitude 85 is at y = −229 (above the top of the box), and the south pole is atInfinity. - 7
At city size the choice barely matters. The last lines of the second example fit Ardenvale, about 10 km across, with three projections. At the same height the widths differ by a few percent (456.6 to 481.3 pixels out of 500). For a country or the world the choice matters a great deal.
Explain it without notes
What does a projection do, and why can no flat map be accurate everywhere?
Why does Greenland look so large in Mercator? What would you use to compare areas instead?
What does fitSize set, and what goes wrong if you give it one point?
Why can drawing stations on an orthographic globe with projection(point) produce wrong dots?
Practice
Write visible(projection, point) for an orthographic globe. It returns the [x, y] pixel for visible points and null for hidden ones. Test it with [0, 0] and [150, 0].
Write fitTo(projection, features, size) that fits the projection to the features, and returns null instead of a broken projection when features is empty.
Trade-offs
- ↔
Mercator is familiar and works for city and street maps. For a world or country map that shows amounts, an equal-area projection is more honest.
- ↔
Fitting to the data makes every map fill its box, but two maps fitted separately have different scales. For a map series, fit once to the whole region and reuse it.
Done when you can
I can say what a projection does, and name one thing each of Mercator, Equal Earth and Orthographic bends.
I can fit a map to its box with
fitSizeorfitExtent.I can draw a polygon with
geoPathand a point withprojection([lon, lat]).I can say why
fitSizeon one point or on no data fails and test for it.I can say why far-side points on a globe need their own visibility check.