R ggplot2 - Multiple Plots (patchwork/gridExtra)

• The patchwork package provides intuitive operators (+, /, |) for combining ggplot2 plots with minimal code, making it the modern standard for multi-plot layouts

Key Insights

• The patchwork package provides intuitive operators (+, /, |) for combining ggplot2 plots with minimal code, making it the modern standard for multi-plot layouts • gridExtra offers fine-grained control through grid.arrange() and tableGrob() functions, ideal for complex layouts requiring precise positioning and annotation • Both packages handle plot alignment, shared legends, and nested layouts differently—choose patchwork for simplicity and gridExtra for maximum customization

Basic Plot Combination with Patchwork

The patchwork package revolutionizes multi-plot composition with operator-based syntax. Install and load the required packages:

install.packages("patchwork")
library(ggplot2)
library(patchwork)

# Create sample plots
p1 <- ggplot(mtcars, aes(x = wt, y = mpg)) +
  geom_point(color = "steelblue") +
  labs(title = "Weight vs MPG") +
  theme_minimal()

p2 <- ggplot(mtcars, aes(x = hp, y = mpg)) +
  geom_point(color = "coral") +
  labs(title = "Horsepower vs MPG") +
  theme_minimal()

p3 <- ggplot(mtcars, aes(x = factor(cyl), fill = factor(cyl))) +
  geom_bar() +
  labs(title = "Cylinder Distribution") +
  theme_minimal() +
  theme(legend.position = "none")

# Horizontal combination
p1 + p2

# Vertical stacking
p1 / p2

# Grid layout
(p1 | p2) / p3

The pipe operator | places plots side-by-side, / stacks vertically, and parentheses control grouping. This syntax eliminates verbose function calls and produces publication-ready layouts instantly.

Advanced Patchwork Layouts

Complex layouts require nested operations and dimension control:

# Create additional plots
p4 <- ggplot(mtcars, aes(x = disp, y = qsec)) +
  geom_point(aes(color = factor(am))) +
  labs(title = "Displacement vs Quarter Mile") +
  theme_minimal()

# Asymmetric layout with plot_layout()
layout <- (p1 | p2 | p3) / p4
layout + plot_layout(heights = c(1, 2))

# Control widths in horizontal arrangements
p1 + p2 + p3 + plot_layout(widths = c(2, 1, 1))

# Nested complex layout
top_row <- p1 + p2 + plot_layout(widths = c(2, 1))
bottom_row <- p3 + p4
final_plot <- top_row / bottom_row + 
  plot_layout(heights = c(1, 1.5))

final_plot

Use plot_layout() to specify relative dimensions. The guides parameter controls legend behavior:

# Collect legends
p1 + p2 + p3 + plot_layout(guides = "collect")

# Keep legends separate
p1 + p2 + plot_layout(guides = "keep")

Annotations and Theming with Patchwork

Add global annotations and apply consistent themes across all plots:

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

# Add plot annotations
combined + 
  plot_annotation(
    title = "Vehicle Performance Analysis",
    subtitle = "Data from mtcars dataset",
    caption = "Source: 1974 Motor Trend US magazine",
    tag_levels = "A"
  )

# Apply theme to all plots
combined & theme_bw()

# Apply theme to specific plots
combined & theme(plot.title = element_text(size = 10))

# Tag customization
combined + 
  plot_annotation(tag_levels = "1") & 
  theme(plot.tag = element_text(size = 8, face = "bold"))

The & operator applies modifications to all plots in the composition, while + adds elements to the patchwork object itself.

gridExtra for Precise Control

gridExtra provides grid.arrange() for situations requiring exact positioning:

library(gridExtra)

# Basic grid arrangement
grid.arrange(p1, p2, p3, p4, ncol = 2)

# Custom layout matrix
layout_matrix <- rbind(c(1, 1, 2),
                       c(3, 4, 4))

grid.arrange(p1, p2, p3, p4, layout_matrix = layout_matrix)

# Specify heights and widths
grid.arrange(p1, p2, p3, p4, 
             ncol = 2,
             heights = c(2, 1),
             widths = c(1, 2))

The layout matrix approach offers pixel-perfect control over plot placement, essential for publications with strict formatting requirements.

Combining Plots and Tables

gridExtra excels at mixing plots with tables:

# Create summary table
summary_data <- data.frame(
  Metric = c("Mean MPG", "Mean HP", "Median Weight"),
  Value = c(round(mean(mtcars$mpg), 2),
            round(mean(mtcars$hp), 2),
            round(median(mtcars$wt), 2))
)

table_grob <- tableGrob(summary_data, rows = NULL,
                        theme = ttheme_minimal(
                          core = list(fg_params = list(hjust = 0, x = 0.1)),
                          colhead = list(fg_params = list(fontface = "bold"))
                        ))

# Combine plot and table
grid.arrange(p1, table_grob, ncol = 2, widths = c(2, 1))

# Complex layout with multiple tables
grid.arrange(
  p1, p2,
  table_grob, p3,
  ncol = 2,
  heights = c(2, 1.5)
)

Shared Legends and Axes

Both packages handle shared elements differently. Patchwork approach:

# Create plots with same aesthetic
p_shared1 <- ggplot(mtcars, aes(x = wt, y = mpg, color = factor(cyl))) +
  geom_point() +
  theme_minimal()

p_shared2 <- ggplot(mtcars, aes(x = hp, y = mpg, color = factor(cyl))) +
  geom_point() +
  theme_minimal()

# Collect common legend
p_shared1 + p_shared2 + 
  plot_layout(guides = "collect") &
  theme(legend.position = "bottom")

gridExtra approach requires manual legend extraction:

library(grid)

# Extract legend function
get_legend <- function(plot) {
  tmp <- ggplot_gtable(ggplot_build(plot))
  leg <- which(sapply(tmp$grobs, function(x) x$name) == "guide-box")
  legend <- tmp$grobs[[leg]]
  return(legend)
}

# Remove legends from plots
p_no_legend1 <- p_shared1 + theme(legend.position = "none")
p_no_legend2 <- p_shared2 + theme(legend.position = "none")

# Extract and arrange with shared legend
shared_legend <- get_legend(p_shared1)

grid.arrange(
  p_no_legend1, p_no_legend2,
  shared_legend,
  ncol = 3,
  widths = c(2, 2, 0.5)
)

Saving Combined Plots

Export multi-plot compositions with proper dimensions:

# Patchwork save
combined_plot <- (p1 + p2) / (p3 + p4)
ggsave("patchwork_output.png", combined_plot, 
       width = 12, height = 10, dpi = 300)

# gridExtra save requires explicit device handling
png("gridextra_output.png", width = 3600, height = 3000, res = 300)
grid.arrange(p1, p2, p3, p4, ncol = 2)
dev.off()

# PDF output for publications
pdf("combined_plots.pdf", width = 10, height = 8)
print(combined_plot)
dev.off()

Performance Considerations

For large datasets or many plots, consider rendering optimization:

# Pre-build plots to avoid recomputation
p1_built <- ggplot_build(p1)
p2_built <- ggplot_build(p2)

# Use cowplot for memory-efficient combinations (alternative)
library(cowplot)
plot_grid(p1, p2, p3, p4, ncol = 2, labels = "AUTO")

# Reduce plot complexity before combining
p_simplified <- ggplot(mtcars[sample(nrow(mtcars), 100), ], 
                       aes(x = wt, y = mpg)) +
  geom_point(size = 1) +
  theme_minimal(base_size = 8)

Choose patchwork for rapid prototyping and clean code. Use gridExtra when layouts demand precise control or table integration. Both packages handle alignment automatically, but patchwork’s operator syntax reduces cognitive overhead for common use cases.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.