| east | north | elevation | magnitude | slab |
|---|---|---|---|---|
| 181.62 | -20.42 | -562 | 4.8 | 540-630 km |
| 181.03 | -20.62 | -650 | 4.2 | 630-720 km |
| 184.10 | -26.00 | -42 | 5.4 | 0-90 km |
| 181.66 | -17.97 | -626 | 4.1 | 540-630 km |
| 181.96 | -20.42 | -649 | 4.0 | 630-720 km |
35 Map
Where did it happen? Some data answers that with a place on the earth, and a place is two numbers. Longitude says how far east or west, and latitude how far north or south. Both are angles, measured in degrees.
Every plot in this book has used two numbers for position. So a map is not a new kind of picture. It is the same sentence you already write, drawn in a space that treats the two numbers as angles on a sphere.
quakes_fiji records earthquakes near Fiji, one row per quake:
data(quakes_fiji) + point + x(east) + y(north) + map()data(quakes_fiji) + point + x(col.east) + y(col.north) + map()data(quakes_fiji) + point + x(:east) + y(:north) + map()plot(data(quakes_fiji), point, x(col.east), y(col.north), map())“Given quakes Fiji: points, x is east, y is north, in the map space.”
That is a thousand earthquakes recorded near Fiji. x is longitude and y is latitude, and map() is the whole of what makes it a map. The axes are labeled in degrees, because that is what the columns hold.
In the web edition, + magnifies a crowded part, − makes the picture smaller, and fit shows the whole map. A map needs that more than most plots do, because places stay crowded however far you zoom in. Zooming is a view: it magnifies the picture and changes nothing the plot claims.
35.1 Flattening the sphere loses something
The earth is round and the page is flat. No projection can flatten a sphere without distorting it, and the choice is not between a good projection and a bad one. You choose which distortion you accept.
Two properties matter most: a region’s area, and a small shape’s form. A projection can keep one of them, never both. preserve says which:
| lon | lat | country | continent | piece |
|---|---|---|---|---|
| 66.519 | 37.363 | Afghanistan | Asia | p001 |
| 69.196 | 37.151 | Afghanistan | Asia | p001 |
| 70.807 | 38.486 | Afghanistan | Asia | p001 |
| 71.845 | 36.738 | Afghanistan | Asia | p001 |
| 73.260 | 37.495 | Afghanistan | Asia | p001 |
(data(world_borders) + path + x(lon) + y(lat) + group(piece) +
map(preserve = "area") + title("preserve = \"area\"")) /
(data(world_borders) + path + x(lon) + y(lat) + group(piece) +
map(preserve = "angle") + title("preserve = \"angle\""))((data(world_borders) + path + x(col.lon) + y(col.lat) + group(col.piece) +
map(preserve = "area") + title("preserve = \"area\"")) /
(data(world_borders) + path + x(col.lon) + y(col.lat) + group(col.piece) +
map(preserve = "angle") + title("preserve = \"angle\"")))(data(world_borders) + path + x(:lon) + y(:lat) + group(:piece) +
map(preserve = "area") + title("preserve = \"area\"")) /
(data(world_borders) + path + x(:lon) + y(:lat) + group(:piece) +
map(preserve = "angle") + title("preserve = \"angle\""))below(plot(data(world_borders), path, x(col.lon), y(col.lat),
group(col.piece), map({ preserve: "area" }),
title("preserve = \"area\"")),
plot(data(world_borders), path, x(col.lon), y(col.lat),
group(col.piece), map({ preserve: "angle" }),
title("preserve = \"angle\"")))“Given the world borders: paths, x is lon, y is lat, grouped by piece, in the map space preserving area, above the same preserving angle.”
Look at Greenland, at the top left of each map, and compare it with Africa. In the first, Greenland is small, which is correct: Africa is about fourteen times larger. In the second, Greenland has grown until it looks as large as Africa. The second map keeps every small shape’s true form and gets the sizes wrong.
Both plots name lon on x, so / gives them one axis and one width. The projection sets each height, and the two heights differ. Keeping every small shape’s form is what stretches the second one north and south.
preserve = "area" is the default, because a map is usually read by comparing how much space something takes. A projection that inflates the far north tells the reader something false about every number placed there.
The world map above is a table like any other, and the next section reads it column by column.
35.2 Drawing the outlines
To draw a boundary of your own, you need to know what shape of table the engine reads. The table has one row for every point along a coastline or a border:
knitr::kable(head(world_borders, 4))| lon | lat | country | continent | piece |
|---|---|---|---|---|
| 66.519 | 37.363 | Afghanistan | Asia | p001 |
| 69.196 | 37.151 | Afghanistan | Asia | p001 |
| 70.807 | 38.486 | Afghanistan | Asia | p001 |
| 71.845 | 36.738 | Afghanistan | Asia | p001 |
path connects rows in the order the table gives them, which is exactly what tracing a coastline needs. group(piece) says where one outline ends and the next begins. A ring is one closed loop of points, ending where it returns to its starting point. A country is not always a single ring. An island is a ring of its own, and a country that encloses another needs a second ring for the hole. So piece counts rings rather than countries.
Without that split, path would join the last point of one country to the first point of the next, and draw a line across the ocean between them.
35.3 The second layer is the data
The chapter’s first plot drew the earthquakes with no coastline, so nothing said where on the earth they were. An outline on its own is only a background. It becomes a plot when something is drawn on top of it, and that is ordinary layering. The outline is one data() and one mark, and the earthquakes are another.
data(world_borders) + path + x(lon) + y(lat) + group(piece) +
style(color = "#c9ccd6") +
data(quakes_fiji) + point + x(east) + y(north) +
style(color = "tomato", opacity = 0.35, size = 3) +
map() + title("A thousand earthquakes, and where Fiji is")(data(world_borders) + path + x(col.lon) + y(col.lat) + group(col.piece) +
style(color = "#c9ccd6") +
data(quakes_fiji) + point + x(col.east) + y(col.north) +
style(color = "tomato", opacity = 0.35, size = 3) +
map() + title("A thousand earthquakes, and where Fiji is"))data(world_borders) + path + x(:lon) + y(:lat) + group(:piece) +
style(color = "#c9ccd6") + data(quakes_fiji) + point + x(:east) +
y(:north) + style(color = "tomato", opacity = 0.35, size = 3) + map() +
title("A thousand earthquakes, and where Fiji is")plot(data(world_borders), path, x(col.lon), y(col.lat), group(col.piece),
style({ color: "#c9ccd6" }), data(quakes_fiji), point, x(col.east),
y(col.north), style({ color: "tomato", opacity: 0.35, size: 3 }), map(),
title("A thousand earthquakes, and where Fiji is"))“Given the world borders: paths, x is lon, y is lat, grouped by piece; then given quakes Fiji: points, x is east, y is north, in the map space.”
The earthquakes now have a coastline around them, so a reader can see where they are. Nothing in the sentence is new: two tables, two marks, and the second data() binds to the mark that follows it.
35.4 A quantity goes on a channel
The earthquakes differ in magnitude, and the picture so far shows only where they were. A flat plot, a cube and a polar plot all leave you an axis to measure along. A bar stands on one axis and measures its length along the other. A map has no axis left, because longitude and latitude use both.
So a quantity moves to a channel. Size each place by it:
data(world_borders) + path + x(lon) + y(lat) + group(piece) +
style(color = "#c9ccd6") +
data(quakes_fiji) + point + x(east) + y(north) + size(magnitude) +
style(color = "tomato", opacity = 0.4) +
map() + title("Sized by magnitude")(data(world_borders) + path + x(col.lon) + y(col.lat) + group(col.piece) +
style(color = "#c9ccd6") +
data(quakes_fiji) + point + x(col.east) + y(col.north) + size(col.magnitude) +
style(color = "tomato", opacity = 0.4) +
map() + title("Sized by magnitude"))data(world_borders) + path + x(:lon) + y(:lat) + group(:piece) +
style(color = "#c9ccd6") + data(quakes_fiji) + point + x(:east) +
y(:north) + size(:magnitude) + style(color = "tomato", opacity = 0.4) +
map() + title("Sized by magnitude")plot(data(world_borders), path, x(col.lon), y(col.lat), group(col.piece),
style({ color: "#c9ccd6" }), data(quakes_fiji), point, x(col.east),
y(col.north), size(col.magnitude),
style({ color: "tomato", opacity: 0.4 }), map(),
title("Sized by magnitude"))“Given the world borders: paths, x is lon, y is lat, grouped by piece; then given quakes Fiji: points, x is east, y is north, size by magnitude, in the map space.”
That plot has a name, the proportional-symbol map, and no single atom draws it. It is point with size bound, in this space.
Color works the same way, and takes a category rather than a quantity:
data(world_borders) + path + x(lon) + y(lat) + group(piece) +
color(continent) + map() +
title("Each continent in its own color")(data(world_borders) + path + x(col.lon) + y(col.lat) + group(col.piece) +
color(col.continent) + map() +
title("Each continent in its own color"))data(world_borders) + path + x(:lon) + y(:lat) + group(:piece) +
color(:continent) + map() + title("Each continent in its own color")plot(data(world_borders), path, x(col.lon), y(col.lat), group(col.piece),
color(col.continent), map(), title("Each continent in its own color"))“Given the world borders: paths, x is lon, y is lat, grouped by piece, color by continent, in the map space.”
35.5 Meridians and parallels
A map often needs a reference line, and the most common one is the equator. A rule draws a line at one position and spans the axis it does not name. In a flat space that is a reference line. Here, a rule at one latitude is a parallel, and a rule at one longitude is a meridian. The plot below draws one parallel, the equator:
| lat |
|---|
| 0 |
data(world_borders) + path + x(lon) + y(lat) + group(piece) +
style(color = "#c9ccd6") +
data(equator) + rule + y(lat) + style(color = "#d4674f") +
map() + title("The equator")(data(world_borders) + path + x(col.lon) + y(col.lat) + group(col.piece) +
style(color = "#c9ccd6") +
data(equator) + rule + y(col.lat) + style(color = "#d4674f") +
map() + title("The equator"))data(world_borders) + path + x(:lon) + y(:lat) + group(:piece) +
style(color = "#c9ccd6") + data(equator) + rule + y(:lat) +
style(color = "#d4674f") + map() + title("The equator")plot(data(world_borders), path, x(col.lon), y(col.lat), group(col.piece),
style({ color: "#c9ccd6" }), data(equator), rule, y(col.lat),
style({ color: "#d4674f" }), map(), title("The equator"))“Given the world borders: paths, x is lon, y is lat, grouped by piece; then given the lines of table: a rule, y is lat, in the map space.”
A rule takes its position from a column, like every other mark, so the line above comes from a one-row table. That is the same way a reference line is drawn on any other plot.
Nothing was added to the grammar to draw that. The same sentence that puts a reference line on a scatter puts the equator on a map. The space gives the mark a new meaning, and the mark itself is unchanged.
35.6 Naming places
A coastline alone does not say which part of the world it is. A few named cities do. text writes a string at a position, and a position here is a place:
| lon | lat | name |
|---|---|---|
| -0.13 | 51.51 | London |
| 139.69 | 35.69 | Tokyo |
| -74.01 | 40.71 | New York |
| 151.21 | -33.87 | Sydney |
| 18.42 | -33.92 | Cape Town |
data(world_borders) + path + x(lon) + y(lat) + group(piece) +
style(color = "#c9ccd6") +
data(capitals) + point + x(lon) + y(lat) +
style(color = "#3d5a80", size = 5) +
data(capitals) + text + x(lon) + y(lat) + label(name) +
style(nudge = "up", size = 11) +
map() + title("Five cities")(data(world_borders) + path + x(col.lon) + y(col.lat) + group(col.piece) +
style(color = "#c9ccd6") +
data(capitals) + point + x(col.lon) + y(col.lat) +
style(color = "#3d5a80", size = 5) +
data(capitals) + text + x(col.lon) + y(col.lat) + label(col.name) +
style(nudge = "up", size = 11) +
map() + title("Five cities"))data(world_borders) + path + x(:lon) + y(:lat) + group(:piece) +
style(color = "#c9ccd6") + data(capitals) + point + x(:lon) + y(:lat) +
style(color = "#3d5a80", size = 5) + data(capitals) + text + x(:lon) +
y(:lat) + label(:name) + style(nudge = "up", size = 11) + map() +
title("Five cities")plot(data(world_borders), path, x(col.lon), y(col.lat), group(col.piece),
style({ color: "#c9ccd6" }), data(capitals), point, x(col.lon),
y(col.lat), style({ color: "#3d5a80", size: 5 }), data(capitals), text,
x(col.lon), y(col.lat), label(col.name),
style({ nudge: "up", size: 11 }), map(), title("Five cities"))“Given the world borders: paths, x is lon, y is lat, grouped by piece; then given the capitals: points, x is lon, y is lat; then given the capitals: text, x is lon, y is lat, label by name, in the map space.”
35.7 What a map refuses
You may want to count the earthquakes by longitude, which on a flat page is bar * count. A mark that measures along an axis has no axis here. gog says so rather than drawing something that looks like a map and is not one:
data(quakes_fiji) + bar * count + x(east) + map()data(quakes_fiji) + bar * count + x(col.east) + map()data(quakes_fiji) + bar * count + x(:east) + map()plot(data(quakes_fiji), layer(bar, count), x(col.east), map())Error:
! gog: `bar` measures along an axis, and a `map()` plot has none to spare — longitude and latitude use both. Drop `map()` to draw `bar` flat, or use `point`, `text`, `path`, `rule`, or `zone`. To carry a quantity on a map, put it on a channel instead of an axis: `point + size(<column>)` sizes each place by it, and `color(<column>)` shades it.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The message names the marks that do draw, and it says what to do instead. Put the quantity on size, as the magnitude map above does, or on color.
A position on a map is a place, so a category cannot be one:
| country | continent | year | life | population | gdp |
|---|---|---|---|---|---|
| Afghanistan | Asia | 2007 | 43.828 | 31889923 | 974.5803 |
| Albania | Europe | 2007 | 76.423 | 3600523 | 5937.0295 |
| Algeria | Africa | 2007 | 72.301 | 33333216 | 6223.3675 |
| Angola | Africa | 2007 | 42.731 | 12420476 | 4797.2313 |
| Argentina | Americas | 2007 | 75.320 | 40301927 | 12779.3796 |
data(gapminder_2007) + point + x(continent) + y(life) + map()data(gapminder_2007) + point + x(col.continent) + y(col.life) + map()data(gapminder_2007) + point + x(:continent) + y(:life) + map()plot(data(gapminder_2007), point, x(col.continent), y(col.life), map())Error:
! gog: `x(continent)` is a category, and a `map()` position is a place — `x` is longitude and `y` is latitude, both in degrees. A category has neither. Give the map the coordinates and put `continent` on a channel that decodes it: `color(continent)`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
And the panel’s shape belongs to the projection:
data(quakes_fiji) + point + x(east) + y(north) + map() + theme(ratio = 1)data(quakes_fiji) + point + x(col.east) + y(col.north) + map() + theme(ratio = 1)data(quakes_fiji) + point + x(:east) + y(:north) + map() +
theme(ratio = 1)plot(data(quakes_fiji), point, x(col.east), y(col.north), map(),
theme({ ratio: 1 }))Error:
! gog: `theme(ratio = )` shapes a panel, and a `map()` panel is shaped by the projection — an equal-area map stretched to fit a box is no longer equal-area, though it still looks like a map. Drop it. To change how much of the page the plot takes, use `theme(width =, height =)`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
An equal-area map stretched to fit a box is no longer equal-area, though it still looks like a map. That is the kind of error a reader cannot see, so the engine refuses it instead of accepting the number and ignoring it.
35.8 Where Mercator stops
preserve = "angle" is the Mercator projection, the one most web maps use. It sends the poles infinitely far away, so it has to stop before it reaches them. It stops at 85.05 degrees. A row beyond that is drawn at the edge, and gog reports how many:
| lon | lat |
|---|---|
| 0 | 60 |
| 15 | 78 |
| -45 | 83 |
| 10 | 89 |
data(far_north) + point + x(lon) + y(lat) + map(preserve = "angle")data(far_north) + point + x(col.lon) + y(col.lat) + map(preserve = "angle")data(far_north) + point + x(:lon) + y(:lat) + map(preserve = "angle")plot(data(far_north), point, x(col.lon), y(col.lat),
map({ preserve: "angle" }))Read the message above the plot. One row sits beyond the limit and was drawn at the edge instead of where it belongs. Nothing was dropped, because dropping it would misreport how many places there are. preserve = "area" reaches both poles and has no such limit.
35.9 Filling the regions: the choropleth
Every country so far has been drawn as an outline. Filling each country by a value is the most familiar kind of map, and it has a name: the choropleth.
The mark is zone, which shades a region. Everywhere else in this book a zone takes its sides from its own row: a category’s slot, bounds, bin or density. A coastline is a fifth way, and it is the first that gives many rows for one shape. So the sentence needs a word for which rows belong to which region, and group is that word already:
data(world_borders) + zone + x(lon) + y(lat) + group(country) +
color(continent) + map() + title("Every country, filled by continent")(data(world_borders) + zone + x(col.lon) + y(col.lat) + group(col.country) +
color(col.continent) + map() + title("Every country, filled by continent"))data(world_borders) + zone + x(:lon) + y(:lat) + group(:country) +
color(:continent) + map() + title("Every country, filled by continent")plot(data(world_borders), zone, x(col.lon), y(col.lat),
group(col.country), color(col.continent), map(),
title("Every country, filled by continent"))“Given the world borders: zones, x is lon, y is lat, grouped by country, color by continent, in the map space.”
Compare that with the earlier map that colored each continent. Only two things changed: path became zone, and group(piece) became group(country). A path traces one ring at a time, so it groups by ring. A zone fills a whole country, so it groups by country.
35.10 A value on the map
Where in the world do people live longest? color reads a number as readily as a category, so the same sentence shades each country by a value. The value comes from a table you already know: gapminder_2007, the same 142 countries A first plot draws.
Joining two tables is the host language’s job, not the grammar’s. In R the join itself is one match() call. Every host language has its own one-call lookup. The plot sentence under the join is the same in all four languages:
filled <- world_borders
filled$life <- gapminder_2007$life[match(filled$country,
gapminder_2007$country)]
filled <- filled[!is.na(filled$life), ]
data(world_borders) + zone + x(lon) + y(lat) + group(country) +
style(color = "#e6e6ec") +
data(filled) + zone + x(lon) + y(lat) + group(country) + color(life) +
map() + title("Life expectancy at birth, 2007")Two layers again, and the first one is not decoration. Of the 176 countries in the outline table, 120 have a number in gapminder_2007. The pale gray layer underneath is what the reader sees for the other 56. Without it those countries would simply be absent, and a reader could not tell “no data” from “no country”.
Read the map and the scatter together. The chapter A first plot shows life expectancy against income as a cloud of points. Here is the same column, and the map answers a question the scatter cannot: where.
35.11 Rings, and the country inside another country
A region is made of rings, and the rings are found by a rule rather than by a column: a ring ends where it returns to the point it started from. Boundary data is written that way, so nothing extra has to be said.
That rule is what lets a country contain a hole. Lesotho lies entirely inside South Africa, and South Africa’s boundary has a second ring around Lesotho:
enclave <- world_borders[world_borders$country %in%
c("South Africa", "Lesotho"), ]
enclave$life <- gapminder_2007$life[match(enclave$country,
gapminder_2007$country)]
data(enclave) + zone + x(lon) + y(lat) + group(country) + color(life) +
map() + title("Lesotho is not painted on top of South Africa")Lesotho is a hole in South Africa, not a patch laid over it. The difference matters because the two countries have different values. A patch is correct only when it is drawn second, and nothing fixes the drawing order.
If a ring in your data does not close, gog cannot tell where one ring ends and the next begins. It draws the region, warns you that the ring ends had to be guessed, and names what to fix.
Every sentence in this chapter also draws on the sphere itself. That is Globe: the same marks at the same places, seen from one side.