24  patchwork

Published

July 6, 2023

Modified

January 8, 2024

patchwork makes it simple to combine separate ggplots into the same graphic.

Source: Cédric Scherer, Graphic Design with ggplot2 at RStudio conf 2022. All examples are derived from this workshop.

24.1 Patchwork functions

24.1.1 Composing plots

  • +: Add plots in row order
  • |: Place plots beside each other
  • -: Used to keep each side from each other when building complex plots
  • /: Place plots on top of each other
  • &: Apply elements to all subplots in the composition
  • *: Apply elements to all subplots in the current nesting level
    • Using + to add elements of a plot will affect the last plot
  • (): Use parentheses to group plots

24.1.2 Composing plots with functions

24.1.3 Layout specifications

24.2 Setup

library(patchwork)
library(tidyverse)
library(ggtext)
library(glue)

# Data
bikes <- readr::read_csv(
  here::here("data", "london-bikes-custom.csv"),
  col_types = "Dcfffilllddddc"
)

bikes$season <- forcats::fct_inorder(bikes$season)
bikes_sorted <- bikes  |> 
  filter(!is.na(weather_type)) |>
  group_by(weather_type) |>
  mutate(sum = sum(count)) |>
  ungroup() |>
  mutate(
    weather_type = forcats::fct_reorder(
      str_to_title(str_wrap(weather_type, 5)), sum
    )
  )

24.2.1 Make the plots

Code
# Theme
theme_std <- theme_set(theme_minimal(base_size = 18))
theme_update(
  panel.grid = element_blank(),
  axis.text = element_text(color = "grey50", size = 12),
  axis.title = element_text(color = "grey40", face = "bold"),
  axis.title.x = element_text(margin = margin(t = 12)),
  axis.title.y = element_text(margin = margin(r = 12)),
  axis.line = element_line(color = "grey80", linewidth = 0.4),
  legend.text = element_text(color = "grey50", size = 12),
  plot.tag = element_text(size = 40, margin = margin(b = 15)),
  plot.background = element_rect(fill = "white", color = "white")
)

p1 <- ggplot(
    bikes_sorted,
    aes(x = weather_type, y = count, color = weather_type)
  ) +
  geom_hline(yintercept = 0, color = "grey80", size = .4) +
  stat_summary(
    geom = "point", fun = "sum", size = 12
  ) +
  stat_summary(
    geom = "linerange", ymin = 0, fun.max = function(y) sum(y),
    linewidth = 2, show.legend = FALSE
  ) +
  coord_flip(ylim = c(0, NA), clip = "off") +
  scale_y_continuous(
    expand = c(0, 0), limits = c(0, 8500000),
    labels = scales::comma_format(scale = .0001, suffix = "K")
  ) +
  scale_color_viridis_d(
    option = "magma", direction = -1, begin = .1, end = .9, name = NULL,
    guide = guide_legend(override.aes = list(size = 7))
  ) +
  labs(
    x = NULL, y = "Sum of reported bike shares", tag = "P1",
  ) +
  theme(
    axis.line.y = element_blank(),
    axis.text.y = element_text(color = "grey50", face = "bold",
                               margin = margin(r = 15), lineheight = .9)
  )
#> Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
#> ℹ Please use `linewidth` instead.

p2 <- bikes_sorted |>
  filter(season == "winter", is_weekend == TRUE, day_night == "night") |>
  group_by(weather_type, .drop = FALSE) |>
  mutate(id = row_number()) |>
  ggplot(
      aes(x = weather_type, y = id, color = weather_type)
    ) +
    geom_point(size = 4.5) +
    scale_color_viridis_d(
      option = "magma", direction = -1, begin = .1, end = .9, name = NULL,
      guide = guide_legend(override.aes = list(size = 7))
    ) +
    labs(
      x = NULL, y = "Reported bike shares on\nweekend winter nights", tag = "P2",
    ) +
    coord_cartesian(ylim = c(0.5, NA), clip = "off")

my_colors <- c("#cc0000", "#000080")

p3 <- bikes |>
  group_by(week = lubridate::week(date), day_night, year) |>
  summarize(count = sum(count), .groups = "drop_last") |>
  mutate(avg = mean(count)) |>
  ggplot(aes(x = week, y = count,
             group = interaction(day_night, year))) +
    geom_line(color = "grey65", linewidth = 1) +
    geom_line(aes(y = avg, color = day_night), stat = "unique", linewidth = 1.7) +
    annotate(
      geom = "text", label = c("Day", "Night"), color = my_colors,
      x = c(5, 18), y = c(125000, 29000),
      size = 8, fontface = "bold",
    ) +
    scale_x_continuous(breaks = c(1, 1:10*5)) +
    scale_y_continuous(labels = scales::comma_format()) +
    scale_color_manual(values = my_colors, guide = "none") +
    labs(
      x = "Week of the Year", y = "Reported bike shares\n(cumulative # per week)", tag = "P3",
    )

24.3 Composing plots

(p1 + p2) / p3

Collect the legends and place them on the composed plot. Note that the theme() is added with & to apply the theme to all subplots in the composition. Use * to apply elements to all subplots in the current nesting level. Use + to add element to the previous plot.

((p1 + p2) / p3 & theme(legend.justification = "top")) + 
  plot_layout(guides = "collect")

You can apply a theme to all the plots using & theme().

(p1 + p2) / p3 & 
  theme(legend.position = "none",
        plot.background = element_rect(color = "black",
                                       linewidth = 3)
        )

To adjust the theme of the patchwork composition itself, such as modifying a title, use the theme argument in plot_annotation(). You can also use plot_annotation() to provide tags to the subplots.

# Add titles to plots
pl1 <- p1 + labs(tag = NULL, title = "Plot One") + 
  theme(legend.position = "none")
pl2 <- p2 + labs(tag = NULL, title = "Plot Two") + 
  theme(legend.position = "none")
pl3 <- p3 + labs(tag = NULL, title = "Plot Three") + 
  theme(legend.position = "none")

(pl1 + pl2) / pl3 +
  plot_annotation(
    tag_levels = "1", tag_prefix = "P",
    title = "An overarching title for all 3 plots, placed on the very top while all other titles are sitting below the tags.",
    theme = theme(plot.title = element_text(size = 18))
    )

24.4 Laying out plots

See the Controlling Layouts vignette.

Adjust the widths and heights with plot_layout() using the widths and heights arguments to provide the relative widths and heights of each column and row in the grid.

((p1 + p2) / p3 & theme(legend.position = "none")) +
  plot_layout(heights = c(2, 1), widths = c(2, 1))

You can create a custom layout with a textual representation. # represents an empty area. Each plot is then represented by a capital letter in alphabetical order. Another way to do this is with the area() function, but textual representation gives options for many layouts.

picasso <- "
AAAAAA#BBBB
CCCCCCCCC##
CCCCCCCCC##"

(p1 + p2 + p3 & theme(legend.position = "none")) + 
  plot_layout(design = picasso)

24.5 Inserting elements

You can also add a plot of text with ggtext to add text directly to a composition, see the section on Insets in the Controlling Layouts vignette.

text <- tibble(
  x = 0, y = 0,
  label = glue(
    "Lorem ipsum dolor sit amet, **consectetur adipiscing elit**, ",
    "sed do eiusmod tempor incididunt ut labore et dolore magna ",
    "aliqua. Ut enim ad minim veniam, quis nostrud exercitation ",
    "<b style='color:#000080;'>ullamco laboris nisi</b> ut aliquip ",
    "ex ea commodo consequat. Duis aute irure dolor in reprehenderit ",
    "in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ",
    "Excepteur sint occaecat <b style='color:#cc0000;'>cupidatat non ",
    "proident</b>, sunt in culpa qui officia deserunt mollit anim id ",
    "est laborum."
    )
)

pt <- ggplot(text, aes(x = x, y = y)) +
  ggtext::geom_textbox(
    aes(label = label),
    box.color = NA, width = unit(23, "lines"),
    color = "grey40", size = 6.5, lineheight = 1.4
  ) +
  coord_cartesian(expand = FALSE, clip = "off") +
  theme_void()

(p1 + pt) / p3

Add inset plots with inset_element()

pl1 + inset_element(pl2, l = .6, b = .1, r = 1, t = .6)

Plots with insets can be added to larger compositions.

(pl1 + inset_element(pl2, l = .6, b = .1, r = 1, t = .6) + pt) / pl3