rggplot2patchwork

How to ensure a shared x- and y-axis across multiple ggplot2 plots in patchwork


I am creating a 2x2 matrix of raster plots using ggplot2 and patchwork, where each plot is generated from a different dataset. I want to ensure that the x- and y-axis of these plots, which are equal, are only shown ones (like the example in this post or this post).

Here is my reproducible example:

library(ggplot2)
library(patchwork)

# Create different datasets for each plot
df1 <- expand.grid(x = seq(300, 800, length.out = 50), y = seq(300, 600, length.out = 50))
df1$z <- with(df1, dnorm(x, mean = 500, sd = 50) * dnorm(y, mean = 400, sd = 50))

df2 <- expand.grid(x = seq(300, 800, length.out = 50), y = seq(300, 600, length.out = 50))
df2$z <- with(df2, dnorm(x, mean = 600, sd = 50) * dnorm(y, mean = 450, sd = 50))

df3 <- expand.grid(x = seq(300, 800, length.out = 50), y = seq(300, 600, length.out = 50))
df3$z <- with(df3, dnorm(x, mean = 550, sd = 50) * dnorm(y, mean = 500, sd = 50))

df4 <- expand.grid(x = seq(300, 800, length.out = 50), y = seq(300, 600, length.out = 50))
df4$z <- with(df4, dnorm(x, mean = 650, sd = 50) * dnorm(y, mean = 350, sd = 50))

# Compute global min and max for z-values across all datasets
min_z <- min(c(df1$z, df2$z, df3$z, df4$z), na.rm = TRUE)
max_z <- max(c(df1$z, df2$z, df3$z, df4$z), na.rm = TRUE)

# Create individual plots with a common color scale
p1 <- ggplot(df1, aes(x, y, fill = z)) +
  geom_raster() +
  scale_fill_viridis_c(limits = c(min_z, max_z)) + 
  labs(y = "Excitation Wavelength / nm") +
  theme(axis.title.x = element_blank())

p2 <- ggplot(df2, aes(x, y, fill = z)) +
  geom_raster() +
  scale_fill_viridis_c(limits = c(min_z, max_z)) + 
  theme(axis.title = element_blank())

p3 <- ggplot(df3, aes(x, y, fill = z)) +
  geom_raster() +
  scale_fill_viridis_c(limits = c(min_z, max_z)) + 
  labs(x = "Emission Wavelength / nm", y = "Excitation Wavelength / nm")

p4 <- ggplot(df4, aes(x, y, fill = z)) +
  geom_raster() +
  scale_fill_viridis_c(limits = c(min_z, max_z)) + 
  labs(x = "Emission Wavelength / nm") +
  theme(axis.title.y = element_blank())

# Combine plots in a 2x2 grid with shared axis titles and legend
plot_combined <- (p1 + p2) / (p3 + p4) +
  plot_layout(axis_titles = "collect", guides = "collect") +
  plot_annotation(
    title = "Emission-Excitation-Matrix",
    subtitle = "Rayleigh Filtered Data"
  )

# Show plot
print(plot_combined)

resulting in the following plot enter image description here

Despite using plot_layout(axis_titles = "collect", guides = "collect") from the patchwork package, the axis are not shared for the combined plot.

Is there a way to ensure Only one shared X- and Y-axis title instead of repeated axis labels using the patchwork package? What should be changed in the example?


Solution

  • By using:

    plot_combined <- (p1 + p2) / (p3 + p4) +
    

    I believe you are explicitly defining rows and columns as independent so labels are being collected for each () object. Not sure why `"collect" works for guides and not labels.

    You can simplify your workflow by creating a list of basic plots to supply to wrap_plots(), and then customising plot elements by piping any modifications using &.

    In response to your comment regarding spacing, I'd argue following Edward Tufte's canonical advice regarding reducing the "data-ink ratio" is probably a good place to start. I have provided two simple options, but the patchwork documentation discusses many ways to control the plot layout.

    library(ggplot2)
    library(patchwork)
    
    # Generate basic ggplot() objects as a list
    plots <- lapply(list(df1, df2, df3, df4), function(df) {
      
      ggplot(df, aes(x, y, fill = z)) +
        geom_raster()
      
    })
    
    

    Now generate plot with expand = FALSE to reduce visual spacing, and add annotation, label, and colour elements:

    wrap_plots(plots) +
      plot_layout(
        nrow = 2,
        ncol = 2,
        axis_titles = "collect",
        guides = "collect"
        ) +
      plot_annotation(
        title = "Emission-Excitation-Matrix",
        subtitle = "Rayleigh Filtered Data") &
      labs(
        x = "Emission Wavelength / nm",
        y = "Excitation Wavelength / nm"
        ) &
      scale_fill_viridis_c(limits = c(min_z, max_z)) &
      coord_cartesian(expand = FALSE)
    

    1

    An improvement, but IMHO also applying axes = "collect" creates a much less cluttered result:

    wrap_plots(plots) +
      plot_layout(
        nrow = 2,
        ncol = 2,
        axis_titles = "collect",
        axes = "collect",
        guides = "collect"
        ) +
      plot_annotation(
        title = "Emission-Excitation-Matrix",
        subtitle = "Rayleigh Filtered Data"
        ) &
      labs(
        x = "Emission Wavelength / nm",
        y = "Excitation Wavelength / nm"
        ) &
      scale_fill_viridis_c(limits = c(min_z, max_z)) &
      coord_cartesian(expand = FALSE)
    

    2

    In this case, it will default to a 2x2 grid so nrow = 2 and ncol = 2 are not need but I included them for completeness.