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:

quakes_fiji: first 5 of 1000 rows
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
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())
170° 180° -30° -20° North East

“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:

world_borders: first 5 of 4150 rows
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\"")))
-50° 50° preserve = "area" Lat -100° 100° -50° 50° preserve = "angle" Lat Lon

“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"))
-100° 100° -50° 50° A thousand earthquakes, and where Fiji is Lat Lon

“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"))
-100° 100° -50° 50° Sized by magnitude Lat Lon Magnitude 4.00 5.20 6.40

“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"))
-100° 100° -50° 50° Each continent in its own color Lat Lon Continent Asia Europe Africa South America Oceania North America Seven seas (open ocean)

“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:

equator: its one row
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"))
-100° 100° -50° 50° The equator Lat Lon

“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:

capitals: all 5 rows
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"))
London Tokyo New York Sydney Cape Town -100° 100° -50° 50° Five cities Lat Lon

“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:

gapminder_2007: first 5 of 142 rows
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:

far_north: all 4 rows
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" }))
-40° -20° 60° 70° 80° Lat Lon

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"))
-100° 100° -50° 50° Every country, filled by continent Lat Lon Continent Asia Europe Africa South America Oceania North America Seven seas (open ocean)

“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")
-100° 100° -50° 50° Life expectancy at birth, 2007 Lat Lon Life 82.60 62.34 42.08

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")
20° 25° 30° -30° -25° Lesotho is not painted on top of South Africa Lat Lon Life 49.34 45.97 42.59

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.