Insets with ggplot2 and tmap – and mapsf!

[This article was first published on One world | Projects, maps and coding - R Project, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

A map on a map

3 min.

This post is dedicated to Dominic Royé, AKA \@dr_xeo

A common challenge when creating maps is how to include an inset map on your visualization. An inset map is nothing more than a smaller map usually included on a corner that may provide additional context to the overall map. It is also useful for representing spatial units that may form part of a country but its geographical location would imply an imperfect visualization, or even to include small units that otherwise won’t be shown on the map.

I have already covered this using the base plot() function, but this time I would show how to produce these insets using the ggplot2 and the tmap packages. In short: use cowplot package.

Test case: Canary Island as an inset

On this example, I would create a map of Spain using mapSpain and creating an inset for the Canary Islands.

The “true” map of Spain is:


regions <- esp_get_ccaa(moveCAN = FALSE)

ggplot(regions) +

plot of chunk 20220303_truemap

I would use a different CRS for each part of Spain. In the case of mainland Spain I would use ETRS89 / UTM 30N (EPSG:25830) and for the Canary Islands I would use REGCAN95 / UTM 28N (EPSG:4083)

main <- regions %>%
  filter( != "Canarias") %>%

ggplot(main) +

plot of chunk 20220303_mainsub

island <- regions %>%
  filter( == "Canarias") %>%

ggplot(island) +

plot of chunk 20220303_mainsub

So that was easy! Just a couple of maps using ggplot2. Let’s start mixing and matching!

On ggplot2

We have already created two quick maps on ggplot2. Now, to produce our map with insets we would:

  1. Produce two plots: The main plot and the sub plot providing a minimal style. We would store them as ggplot2 objects.

  2. We would combine both objects with cowplot.

# Main plot
main_gg <- ggplot(main) +
  geom_sf() +
  theme_void() +
    plot.background = element_rect(fill = "grey85", colour = NA),
    # Add a bit of margin on the bottom left
    # We would place the inset there
    plot.margin = margin(l = 80, b = 80)

# Sub plot
sub_gg <- ggplot(island) +
  geom_sf() +
  theme_void() +
  # Add a border to the inset
    panel.border = element_rect(fill = NA, colour = "black"),
    plot.background = element_rect(fill = "grey95")

We have our objects in place, and now is when the magic happens! With cowplot we can combine both maps on a single one. You may need to play a bit with the parameters x, y hjust and vjust of the sub plot to improve the placement:


ggdraw() +
  draw_plot(main_gg) +
    height = 0.2,
    x = -0.25,
    y = 0.08

plot of chunk 20220303_insetggplot

Note also that this approach is valid not only for maps, but for all type of plot produced by ggplot2, since this package is not specific for map objects:

# Combining non-spatial plots

mass_flipper <- ggplot(
  data = penguins,
    x = flipper_length_mm,
    y = body_mass_g
) +
    color = species,
    shape = species
  size = 3,
  alpha = 0.8
  ) +
  theme_minimal() +
  scale_color_manual(values = c("darkorange", "purple", "cyan4"))

flipper_hist <- ggplot(data = penguins, aes(x = flipper_length_mm)) +
  geom_histogram(aes(fill = species),
    alpha = 0.5,
    position = "identity",
    show.legend = FALSE
  ) +
  scale_fill_manual(values = c("darkorange", "purple", "cyan4")) +
  theme_void() +
  theme(plot.background = element_rect(fill = "white"))

# Non-sense plot!
ggdraw() +
  draw_plot(mass_flipper) +
    scale = 0.25,
    y = 0.3,
    x = -0.2

plot of chunk 20220303_insetggplot_nonsense

On tmap

We can follow a similar approach on tmap. On versions v3.x.x (there is a new revamped version on development) we can use tmap_grob() to convert the tmap objects to the objects that cowplot can handle.


main_tmap <- tm_shape(main) +
  tm_polygons() +
    inner.margins = c(.3, .3, 0, 0),
    frame = FALSE

main_tmap <- tmap_grob(main_tmap)

sub_tmap <- tm_shape(island) +

sub_tmap <- tmap_grob(sub_tmap)

Once that we have these new “grobs”, we can use the same approach than we applied to ggplot2 objects.

ggdraw() +
  draw_plot(main_tmap) +
    height = 0.3,
    x = -0.2

plot of chunk 20220303_insettmap

Update: On mapsf

Timotheé Giraud (AKA \@rgeomatic), the developer of mapsf, shared also how to create inset maps using that package:


mf_inset_on(island, pos = "bottomright", cex = .3)
box(lwd = .5)

plot of chunk 20220303_insetmapsf

To leave a comment for the author, please follow the link and comment on their blog: One world | Projects, maps and coding - R Project. offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)