---
title: "Map compositions"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Map compositions}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
fig.width = 7,
fig.height = 6,
out.width = "100%"
)
# Interactive widgets embed their JavaScript libraries (several MB each), so
# they only run when pkgdown builds the website, not in the installed vignette.
widgets <- identical(Sys.getenv("IN_PKGDOWN"), "true")
```
```{r setup}
library(geopt)
library(ggplot2)
```
The Açores are about 1,500 km west of the mainland, and Madeira about 1,000 km
to the south-west. Drawn in their true positions, most of a map of Portugal
would be empty ocean. So geopt *composes* the map: it moves the islands next
to the mainland, as insets, and draws frames around them to show they've been
moved. This guide shows how to control that composition, and how to draw the
result with different plotting libraries.
## Default map
`get_map()` returns the composed map. Called with no arguments, it returns
municipalities, with the islands moved next to the mainland and framed:
```{r default}
m <- get_map()
```
The result is a standard `sf` object in WGS84 (EPSG:4326). The frames are
extra rows in it: the `geopt_type` column is `"boundary"` for the regions and
`"frame"` for the frames. On the frames, `geopt_label` holds the frame's name
and `geopt_frame_level` says what it surrounds: 1 for a whole archipelago, 2
for a group of islands, and 3 for a single island.
```{r default-plot}
boundaries <- m[m$geopt_type == "boundary", ]
frames <- m[m$geopt_type == "frame", ]
ggplot() +
geom_sf(data = boundaries, aes(fill = nuts2)) +
geom_sf(data = frames, aes(linewidth = factor(geopt_frame_level)), fill = NA) +
scale_linewidth_manual(values = c(0.8, 0.4, 0.2)) +
guides(linewidth = "none") +
theme_void()
```
## Administrative levels
The `level` argument sets which units to draw: `"freguesia"`, `"municipio"`
(the default), `"distrito"`, `"nuts1"`, `"nuts2"` or `"nuts3"`, or one of the
health geographies, `"uls"`, `"reg_saude"`, `"aces"`, `"uls_municipio"` and
`"aces_municipio"` (see `vignette("health-geographies")`). Every level is built by merging the same
simplified freguesias, so borders line up exactly when two levels are drawn on
top of each other.
```{r levels}
m_nuts2 <- get_map("nuts2")
boundaries <- m_nuts2[m_nuts2$geopt_type == "boundary", ]
frames <- m_nuts2[m_nuts2$geopt_type == "frame", ]
ggplot() +
geom_sf(data = boundaries, aes(fill = nuts2)) +
geom_sf(data = frames, aes(linewidth = factor(geopt_frame_level)), fill = NA) +
scale_linewidth_manual(values = c(0.8, 0.4, 0.2)) +
guides(linewidth = "none") +
theme_void()
```
## How the islands are shown
The `islands` argument sets how the Açores and Madeira appear. It takes one
of these values:
| Value | Açores | Madeira |
|---|---|---|
| `"compact"` (default) | four insets: the western group, the central group, São Miguel, and Santa Maria | Porto Santo in its own small inset, moved next to Madeira |
| `"groups"` | the three official groups: Ocidental, Central and Oriental | the same as `"compact"` |
| `"box"` | a labelled rectangle | a labelled rectangle |
| `"in_place"` | where they really are | where they really are |
| `"none"` | left out | left out |
For anything else, such as a different setting for each archipelago, or no
frames, pass a layout made with `island_layout()`. A string is a shortcut:
`islands = "none"` is the same as `islands = island_layout("none")`.
### Leaving the islands out
To show only the mainland:
```{r mainland-only}
m <- get_map("nuts3", islands = "none")
ggplot(m) +
geom_sf(aes(fill = nuts3)) +
guides(fill = "none") +
theme_void()
```
To leave out one archipelago, set it to `"none"` in `island_layout()`, using
`raa` for the Açores and `ram` for Madeira:
```{r no-raa}
m <- get_map("nuts2", islands = island_layout(raa = "none"))
ggplot(m[m$geopt_type == "boundary", ]) +
geom_sf(aes(fill = nuts2)) +
theme_void()
```
When data is joined with `data` and `by`, rows for a left-out archipelago are
dropped quietly: they matched, they just aren't on the map.
### Island groups
`"compact"` arranges the islands to take the least room, so Santa Maria gets
an inset of its own. `"groups"` draws the Açores in their three official
groups instead, and `ram = "whole"` puts Madeira and Porto Santo in a single
inset, at their true distance apart:
```{r layout-groups}
m <- get_map("nuts3", islands = island_layout(raa = "groups", ram = "whole"))
boundaries <- m[m$geopt_type == "boundary", ]
frames <- m[m$geopt_type == "frame", ]
ggplot() +
geom_sf(data = boundaries, aes(fill = nuts2)) +
geom_sf(data = frames, fill = NA, linewidth = 0.5) +
guides(fill = "none") +
theme_void()
```
There is no `"whole"` for the Açores. The archipelago is wider than all of
mainland Portugal, so in a single inset that fits next to the mainland its
smaller islands would be too small to read. To show all of the Açores as they
really are, map them on their own (see [Maps of one region](#maps-of-one-region)).
### Frames around the islands
The frames are added by default. Map `geopt_frame_level` to line width so the
frame around a whole archipelago is thicker than the frames around single
islands, as in the maps above. To leave the frames out, use
`frames = FALSE`:
```{r no-frames}
m <- get_map("freguesia", islands = island_layout(frames = FALSE))
ggplot(m) +
geom_sf(aes(fill = nuts2), linewidth = 0.05) +
theme_void()
```
### Islands as rectangles
`"box"` replaces each archipelago with a labelled rectangle, which keeps the
map compact when only one value per archipelago is needed. It only works at
levels where each archipelago is a single region: `"nuts1"`, `"nuts2"`,
`"nuts3"` and `"reg_saude"`. At finer levels each archipelago has several
regions, and `"box"` stops with an error rather than draw a stack of
rectangles that no longer reads as a map.
```{r island-box, warning = FALSE}
m <- get_map("nuts3", islands = "box")
ggplot() +
geom_sf(data = m, aes(fill = area_ha), color = "white") +
geom_sf_text(
data = m[!is.na(m$geopt_label), ],
aes(label = geopt_label),
nudge_x = 0.3,
hjust = 0,
size = 4
) +
theme_void()
```
By default the rectangles go at the bottom left of the mainland. Use
`box_position = "top_left"`, or give the longitude and latitude of the
bottom-left corner of the stack as `c(x, y)`:
```{r box-position, warning = FALSE}
m <- get_map("nuts3", islands = island_layout("box", box_position = c(-11.5, 38.5)))
ggplot() +
geom_sf(data = m, aes(fill = area_ha), color = "white") +
geom_sf_text(
data = m[!is.na(m$geopt_label), ],
aes(label = geopt_label),
nudge_x = 0.3,
hjust = 0,
size = 4
) +
theme_void()
```
## Moving and resizing the islands
`island_layout()` returns a list that can be changed before passing it on. Each
archipelago drawn as insets has an `anchor`, the longitude and latitude its
insets are placed around, a `label` for its frame, and `groups`, one entry per
inset, each with its own `offset` from the anchor and `scale`. Printing a
layout shows what it does:
```{r layout-print}
lay <- island_layout()
lay
```
For example, to move the Açores half a degree further west, draw Madeira a
little smaller, and label the Açores in English:
```{r layout-edit}
lay$raa$anchor <- lay$raa$anchor + c(-0.5, 0)
lay$ram$groups$madeira$scale <- 2.5
lay$raa$label <- "Azores"
m <- get_map("nuts3", islands = lay)
frames <- m[m$geopt_type == "frame", ]
ggplot() +
geom_sf(data = m[m$geopt_type == "boundary", ], aes(fill = nuts2)) +
geom_sf(data = frames, fill = NA, linewidth = 0.4) +
guides(fill = "none") +
theme_void()
frames$geopt_label
```
Print `lay$raa` to see every setting, and `?island_layout` for what each one
does. If a change breaks the layout, `get_map()` says which setting is wrong.
## Maps of one region
Every row of `get_map()` keeps its codes and names, so a map of one region is
the whole map, filtered. For an area of the mainland, filter the map as it is:
```{r region-norte}
m <- get_map("municipio")
norte <- m[m$nuts2_cod %in% "PT11", ]
ggplot(norte) +
geom_sf(aes(fill = area_ha)) +
theme_void()
```
Using `%in%` rather than `==` matters here: the frames have no region codes,
and `%in%` drops them where `==` would add empty rows.
For an archipelago, use `islands = "in_place"` first. Otherwise the islands
have already been moved and enlarged into their insets by the time the map
is filtered. With `"in_place"` they keep their true position, and no frames are
drawn:
```{r region-acores, fig.height = 3.5}
m <- get_map("municipio", islands = "in_place")
acores <- m[m$nuts2_cod %in% "PT20", ]
ggplot(acores) +
geom_sf(aes(fill = municipio)) +
guides(fill = "none") +
theme_void()
```
The filtered map is still a `get_map()` map, so `plot_pt()` draws it too, and
data joined with `data` and `by` comes along. Health maps have no NUTS 2
or NUTS 3 codes, because their units don't nest in those regions; filter them
by `nuts1_cod` (`"PT2"` for the Açores, `"PT3"` for Madeira).
## Quick plots
`plot_pt()` draws a map in one line. It builds the map (or takes one
built with `get_map()`), colours it by a column, and draws the frames and
island labels. It returns an ordinary object of the plotting library it used:
a ggplot to extend with `+`, or an echarts4r widget to pipe into `e_*()`
functions. It takes the same `islands` argument as `get_map()`.
```{r plot-pt}
df <- data.frame(
concelho = c("Porto", "Lisboa", "Braga", "Faro", "Lagoa (Açores)"),
score = c(10, 25, 15, 8, 12)
)
plot_pt(df, value = "score", by = c(municipio = "concelho"))
```
Build the map once and draw it with different plotting libraries, for
example when the same map goes into a report (ggplot) and a dashboard
(echarts):
```{r plot-pt-both, eval = FALSE}
m <- get_map("nuts3", islands = "box")
plot_pt(m, value = "area_ha") # ggplot
plot_pt(m, value = "area_ha", renderer = "echarts") # interactive widget
```
The echarts version links values to regions by official code, not by name,
so a value can't end up on the wrong one of two places with the same name.
Tooltips still show the names.
`plot_pt()` only sets the colours (`palette`, `na_color`) and the `legend`.
Everything else is styled the usual way for each library:
```{r plot-pt-style, eval = FALSE}
plot_pt(df, value = "score", by = c(municipio = "concelho")) +
labs(title = "My indicator") +
theme(legend.position = "bottom")
plot_pt(m, value = "area_ha", renderer = "echarts") |>
echarts4r::e_title("Area by NUTS3")
```
## Mapping libraries
The sf objects returned by `get_map()` work with any spatial plotting
library; `plot_pt()` is a shortcut, not a requirement. Below are examples with
the libraries geopt has been tested with.
### ggplot2
ggplot2 with `geom_sf()` is the most straightforward option for static maps:
```{r ggplot2}
m <- get_map("municipio")
boundaries <- m[m$geopt_type == "boundary", ]
frames <- m[m$geopt_type == "frame", ]
ggplot() +
geom_sf(data = boundaries, aes(fill = nuts2)) +
geom_sf(data = frames, aes(linewidth = factor(geopt_frame_level)), fill = NA) +
scale_linewidth_manual(values = c(0.8, 0.4, 0.2)) +
guides(linewidth = "none") +
theme_void()
```
### tmap
tmap supports both static (`"plot"`) and interactive (`"view"`) modes. The
same code produces a static plot or a leaflet widget depending on the active
mode:
```{r tmap, eval = requireNamespace("tmap", quietly = TRUE)}
library(tmap)
m <- get_map("nuts2")
boundaries <- m[m$geopt_type == "boundary", ]
frames <- m[m$geopt_type == "frame", ]
# Static mode
tmap_mode("plot")
tm_shape(boundaries) +
tm_polygons(fill = "nuts2") +
tm_shape(frames) +
tm_lines(
lwd = "geopt_frame_level",
lwd.scale = tm_scale_ordinal(values = c(0.8, 0.4, 0.2)),
lwd.legend = tm_legend_hide()
)
```
Switch to interactive mode with `tmap_mode("view")` to get a zoomable leaflet
map with the same code.
### echarts4r
echarts4r creates interactive maps. geopt has two helper functions for it:
`frame_data_echarts()` styles the frames, and `box_labels_echarts()` labels the
rectangles when `islands = "box"`.
**With islands as insets:**
```{r echarts-inset, eval = widgets && requireNamespace("echarts4r", quietly = TRUE)}
library(echarts4r)
m <- get_map("municipio", islands = island_layout(raa = "groups", ram = "whole"))
m$name <- ifelse(is.na(m$municipio), m$geopt_label, m$municipio)
geojson <- geojsonsf::sf_geojson(m)
m |>
e_charts(name) |>
e_map_register("pt", geojson) |>
e_map(area_ha, map = "pt", roam = TRUE, data = frame_data_echarts(m, "area_ha")) |>
e_visual_map(area_ha) |>
e_tooltip()
```
**With `islands = "box"`:**
`box_labels_echarts()` adds a label next to each rectangle:
```{r echarts-box, eval = widgets && requireNamespace("echarts4r", quietly = TRUE)}
m <- get_map("nuts3", islands = "box")
m$name <- ifelse(is.na(m$geopt_label), m$nuts3, m$geopt_label)
geojson <- geojsonsf::sf_geojson(m)
m |>
e_charts(name) |>
e_map_register("pt", geojson) |>
e_map(area_ha, map = "pt", roam = TRUE) |>
box_labels_echarts(m) |>
e_visual_map(area_ha) |>
e_tooltip()
```
### plotly
plotly offers two approaches for interactive sf maps.
**Option 1: `plot_ly()` with sf objects directly:**
```{r plotly-sf, eval = widgets && requireNamespace("plotly", quietly = TRUE), warning = FALSE, message = FALSE}
library(plotly)
m <- get_map("nuts3", islands = island_layout(frames = FALSE))
boundaries <- sf::st_cast(m, "MULTIPOLYGON")
plot_ly(
boundaries,
split = ~nuts3,
color = ~nuts3,
text = ~paste0(nuts3, "
Area: ", round(area_ha), " ha"),
hoverinfo = "text",
hoveron = "fills"
) |>
add_sf(showlegend = FALSE)
```
**Option 2: `ggplotly()` to convert a ggplot** (requires plotly >= 4.12 for
compatibility with ggplot2 >= 4.0):
```{r plotly-ggplotly, eval = widgets && requireNamespace("plotly", quietly = TRUE) && packageVersion("plotly") >= "4.12.0", warning = FALSE}
p <- ggplot(boundaries) +
geom_sf(
aes(
fill = nuts3,
text = paste0(nuts3, "
Area: ", round(area_ha), " ha")
)
) +
guides(fill = "none") +
theme_void()
plotly::ggplotly(p, tooltip = "text", showlegend = FALSE)
```