rggplot2data-visualizationgridextrapatchwork

R: How can I add global/spanning X and Y axes to a grid of ggplot plots with patchwork?


I am trying to create a global/spanning Y-axis title and a global/spanning X-axis title for a plot containing several ggplot objects arranged in a grid with clockwork. A global axis title is one that replaces the multiple axis titles along some margin of the plot with a single title, a shown in Step 2 to Step 4 below. Although this post shows how to create a global Y axis title, it does not show how to create do so for both the X and Y axes.

My issue is that my attempt at creating a global X-axis title places it far away from the other plots and shifts the Y-axis title so it is not centered in the plot. In the following reproducible examples, I show my starting point, successful creation of one global axis, and then (starting at Step 3) my failed attempts to add more alongside what I expected to produce. Does anyone know how I can add the global x-axes close to the rest of the plot without shifting the y-axis substantially toward the top of the figure?

1. X and Y axis titles for each cell

This is my "starting" plot: it has a 2x2 layout with axis titles for each cell. I'll want to replace these with fewer axis titles.

library(ggplot2)
library(patchwork)

# Create a list of 4 plots, then render
plot_list <- rep(list(ggplot(mtcars, aes(mpg, disp)) +geom_point()), 4)
wrap_plots(plot_list, nrow = 2)

2. Add a global Y-axis title

The cell-specific Y-axis titles are successfully replaced with a single, global Y-axis title. However, each cell still has its own X-axis title.

# Same list of 4 plots
plot_list <- rep(list(ggplot(mtcars, aes(mpg, disp)) +geom_point()), 4)

# Loop through list to remove each plot's Y-axis
for(i in 1:length(plot_list)){
  plot_list[[i]] <- plot_list[[i]] + theme(axis.title.y = element_blank())
}

# Create geom_text figure with Y-axis title that will fill the first column
y_lab_big <- ggplot() + 
  annotate(geom = "text", x = 1, y = 1, 
           label = "Here is some example long Y-axis text.", angle = 90) +
  coord_cartesian(clip = "off")+
  theme_void()

# Specify the text plot is NOT stacked and is much less wide
(y_lab_big | wrap_plots(plot_list, nrow = 2)) +
  plot_layout(widths = c(.1, 1))

3. Try to add a global X-axis title

I successfully replace each cell's x-axis title with a single, global x-axis title. However, it is far from the 2x2 grid and the y-axis title is now not centered on the 2x2 grid.

plot_list <- rep(list(ggplot(mtcars, aes(mpg, disp)) + geom_point()), 4)

# Loop through list to remove BOTH X-axis and Y-axis from every plot
for(i in 1:length(plot_list)){
  plot_list[[i]] <- plot_list[[i]] + theme(axis.title.y = element_blank(),
                                 axis.title.x = element_blank())
}

# Create geom_text figures for *Y-axis* title (just like Example 2)
y_lab_big <- 
  ggplot() + 
  annotate(geom = "text", x = 1, y = 1, label = "Here is some example long Y-axis text.", angle = 90) +
  coord_cartesian(clip = "off")+
  theme_void()

# Create geom_text figures for *X-axis* title (different angle)
x_lab_big <- 
  ggplot() + 
  annotate(geom = "text", x = 1, y = 1, label = "Big X Text", angle = 0) +
  coord_cartesian(clip = "off")+
  theme_void()

# Same as Step 2 but add x-axis title using / to indicate that it should
# be stacked below the plot_list plots and specify heights to the thirst row
# (which should be the x-axis title) is relatively small.
(y_lab_big | (wrap_plots(plot_list, nrow = 2)) / x_lab_big) +
  plot_layout(widths = c(.1, 1),
              heights = c(1,1,.1))

Ideally looks like below, with y_title centered between [1.1] and [2,1] and x_title centered between [3,2] [3,3].

matrix(c("y_title", "y_title", NA,
         "scatter1","scatter2", "x_title",
         "scatter3","scatter4","x_title"), nrow = 3, ncol = 3)
#>      [,1]      [,2]       [,3]      
#> [1,] "y_title" "scatter1" "scatter3"
#> [2,] "y_title" "scatter2" "scatter4"
#> [3,] NA        "x_title"  "x_title"

4. Adding more than one global X-axis title

Here I attempt to add a second global X-axis title so that each column receives its own x-axis title but all rows share the same axis title. The issue remains the same.

plot_list <- rep(list(ggplot(mtcars, aes(mpg, disp)) + geom_point()), 4)

# Loop through list to remove BOTH X-axis and Y-axis from every plot
for(i in 1:length(plot_list)){
  plot_list[[i]] <- plot_list[[i]] + theme(axis.title.y = element_blank(),
                                           axis.title.x = element_blank())
}

# Create geom_text figures for *Y-axis* title (just like Example 2)
y_lab_big <-  ggplot() + 
  annotate(geom = "text", x = 1, y = 1, label = "Here is some example long Y-axis text.", angle = 90) +
  coord_cartesian(clip = "off")+
  theme_void()

# Create geom_text figures for *X-axis* title (different angle)
x_lab_big <- ggplot() + 
  annotate(geom = "text", x = 1, y = 1, label = "X Text 1", angle = 0) +
  coord_cartesian(clip = "off")+
  theme_void()

x_lab_big2 <- ggplot() + 
  annotate(geom = "text", x = 1, y = 1, label = "X Text 2", angle = 0) +
  coord_cartesian(clip = "off")+
  theme_void()

# Same as previous but add x-axis title using / to indicate that it should
# be stacked below the plot_list plots and specify heights to the thirst row
# (which should be the x-axis title) is relatively small.
(y_lab_big | (wrap_plots(plot_list, nrow = 2)) / (x_lab_big | x_lab_big2)) +
  plot_layout(widths = c(.1, 1),
              heights = c(1, .1))

Ideally looks like below, with y_title centered between [1,1] and [2,1], x_title1 centered in [3,2], and x_title3 centered in [3,3].

matrix(c("y_title", "y_title", NA,
         "scatter1","scatter2", "x_title1",
         "scatter3","scatter4","x_title2"), nrow = 3, ncol = 3)
#>      [,1]      [,2]       [,3]      
#> [1,] "y_title" "scatter1" "scatter3"
#> [2,] "y_title" "scatter2" "scatter4"
#> [3,] NA        "x_title1" "x_title2"

Although there are alternative solutions (ggplot: how to add common x and y labels to a grid of plots) I am trying to stay with using patchwork.


Solution

  • UPDATE In patchwork >= 1.2.0 the feature to collect the axes was added, so achieving the desired result could be more easily achieved using axes="collect" to get shared axes titles:

    library(ggplot2)
    library(patchwork)
    
    wrap_plots(plot_list, nrow = 2) +
      plot_layout(axes = "collect") &
      labs(y = "Here is some example long Y-axis text.")
    

    For the second use case one still has to remove the titles from the plots in the top row and use axes="collect_y" to have only a shared y axis title:

    plot_list <- lapply(seq_along(plot_list), \(i) {
      if (i %in% 1:2) {
        plot_list[[i]] <- plot_list[[i]] + labs(x = NULL)
      }
      plot_list[[i]]
    })
    
    wrap_plots(plot_list, nrow = 2) +
      plot_layout(axes = "collect_y") &
      labs(y = "Here is some example long Y-axis text.")
    

    Original answer

    Hopefully patchwork will gain this feature in one of the next releases. In the meanwhile one option would be to stick with patchwork and get some support from cowplot. Adapting my answer on R ggplot2 patchwork common axis labels to your case(s):

    Note: That works nice if there is no legend. With a legend it gets probably more fiddling.

    library(ggplot2)
    library(patchwork)
    library(cowplot)
    
    p <- ggplot(mtcars, aes(mpg, disp)) +
      geom_point()
    plot_list <- rep(list(p + labs(x = NULL, y = NULL)), 4)
    
    p_axis <- p + labs(x = "Big X Text", y = "Here is some example long Y-axis text.")
    x_axis <- cowplot::get_plot_component(p_axis, "xlab-b")
    y_axis <- cowplot::get_plot_component(p_axis, "ylab-l")
    
    design = "
    FAB
    FCD
    #EE
    "
    
    c(plot_list, list(x_axis, y_axis)) |> 
      wrap_plots() + 
      plot_layout(heights = c(20, 20, 1), widths = c(1, 25, 25), design = design)
    

    And for the e.g. two separate x titles you could do:

    Note: For that case another option would be to remove the axis titles for only the top two plots.

    
    p_axis1 <- p + labs(x = "X Text 1", y = "Here is some example long Y-axis text.")
    p_axis2 <- p + labs(x = "X Text 2", y = "Here is some example long Y-axis text.")
    x_axis1 <- cowplot::get_plot_component(p_axis1, "xlab-b")
    x_axis2 <- cowplot::get_plot_component(p_axis2, "xlab-b")
    
    design = "
    GAB
    GCD
    #EF
    "
    
    
    c(plot_list, list(x_axis1, x_axis2, y_axis)) |> 
      wrap_plots() + 
      plot_layout(heights = c(20, 20, 1), widths = c(1, 25, 25), design = design)