Skip to contents

Overview

When combining multiple plots with patchwork, legends often need special handling:

  • Duplicate legends - Same aesthetic mapped in multiple plots creates redundancy
  • Alignment issues - Legends may not align properly across panels
  • Spanning - Legends should sometimes span multiple rows

ggguides provides collect_legends() and collect_axes() to address these challenges.

The Problem: Duplicate Legends

library(patchwork)

p1 <- ggplot(mtcars, aes(mpg, wt, color = factor(cyl))) +
  geom_point() + labs(title = "Plot 1", color = "Cylinders")
p2 <- ggplot(mtcars, aes(mpg, hp, color = factor(cyl))) +
  geom_point() + labs(title = "Plot 2", color = "Cylinders")

# Default patchwork: duplicate legends
p1 | p2
fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 2 3 4 5 10 15 20 25 30 35 mpg wt fill: #F5F6F8;' /> fill: #F5F6F8;' /> Cylinders fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 4 6 8 Plot 1 fill: #F5F6F8;' /> 100 200 300 10 15 20 25 30 35 mpg hp fill: #F5F6F8;' /> fill: #F5F6F8;' /> Cylinders fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 4 6 8 Plot 2

Basic Legend Collection

Use collect_legends() to gather legends from all plots in a composition:

fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 2 3 4 5 10 15 20 25 30 35 mpg wt Plot 1 fill: #F5F6F8;' /> 100 200 300 10 15 20 25 30 35 mpg hp Plot 2 fill: #F5F6F8;' /> fill: #F5F6F8;' /> Cylinders fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 4 6 8

Position Options

Control where the collected legend appears:

collect_legends(p1 | p2, position = "bottom")
fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 2 3 4 5 10 15 20 25 30 35 mpg wt Plot 1 fill: #F5F6F8;' /> 100 200 300 10 15 20 25 30 35 mpg hp Plot 2 fill: #F5F6F8;' /> fill: #F5F6F8;' /> Cylinders fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 4 6 8
collect_legends(p1 | p2, position = "left")
fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 2 3 4 5 10 15 20 25 30 35 mpg wt Plot 1 fill: #F5F6F8;' /> 100 200 300 10 15 20 25 30 35 mpg hp Plot 2 fill: #F5F6F8;' /> fill: #F5F6F8;' /> Cylinders fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 4 6 8

Stacked Plots

For vertically stacked plots, legends can be centered or span the full height. Using different plot heights makes the spanning behavior more visible.

Default: Centered

# Create plots with different heights using plot_layout
p3 <- ggplot(mtcars, aes(mpg, disp, color = factor(cyl))) +
  geom_point() + labs(title = "Plot 3", color = "Cylinders")

# Stack with different heights: 1, 1/2, 1/4
stacked <- (p1 / p2 / p3) + plot_layout(heights = c(4, 2, 1))
collect_legends(stacked, position = "right")
fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 2 3 4 5 10 15 20 25 30 35 mpg wt Plot 1 fill: #F5F6F8;' /> 100 200 300 10 15 20 25 30 35 mpg hp Plot 2 fill: #F5F6F8;' /> 100 200 300 400 10 15 20 25 30 35 mpg disp Plot 3 fill: #F5F6F8;' /> fill: #F5F6F8;' /> Cylinders fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 4 6 8

Spanning Full Height

Use span = TRUE to make the legend fill the full height:

gt <- collect_legends(stacked, position = "right", span = TRUE)
grid::grid.draw(gt)
fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 2 3 4 5 10 15 20 25 30 35 mpg wt Plot 1 fill: #F5F6F8;' /> 100 200 300 10 15 20 25 30 35 mpg hp Plot 2 fill: #F5F6F8;' /> 100 200 300 400 10 15 20 25 30 35 mpg disp Plot 3 fill: #F5F6F8;' /> fill: #F5F6F8;' /> Cylinders fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 4 6 8

Attaching to Specific Rows

Attach the legend to specific row(s) instead of spanning all:

# Legend attached to row 1 only (the tallest plot)
gt <- collect_legends(stacked, position = "right", span = 1)
grid::grid.draw(gt)
fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 2 3 4 5 10 15 20 25 30 35 mpg wt Plot 1 fill: #F5F6F8;' /> 100 200 300 10 15 20 25 30 35 mpg hp Plot 2 fill: #F5F6F8;' /> 100 200 300 400 10 15 20 25 30 35 mpg disp Plot 3 fill: #F5F6F8;' /> fill: #F5F6F8;' /> Cylinders fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 4 6 8
# Legend attached to rows 1 and 2
gt <- collect_legends(stacked, position = "right", span = 1:2)
grid::grid.draw(gt)
fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 2 3 4 5 10 15 20 25 30 35 mpg wt Plot 1 fill: #F5F6F8;' /> 100 200 300 10 15 20 25 30 35 mpg hp Plot 2 fill: #F5F6F8;' /> 100 200 300 400 10 15 20 25 30 35 mpg disp Plot 3 fill: #F5F6F8;' /> fill: #F5F6F8;' /> Cylinders fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 4 6 8

Collecting Duplicate Axes

When stacking plots vertically, the x-axis may be duplicated. Use collect_axes() to remove redundant axes:

# Plots stacked vertically - x-axis is duplicated
p_top <- ggplot(mtcars, aes(mpg, wt)) +
  geom_point() + labs(y = "Weight")

p_bottom <- ggplot(mtcars, aes(mpg, disp)) +
  geom_point() + labs(y = "Displacement")

# Without axis collection (both have x-axis)
p_top / p_bottom
fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 2 3 4 5 10 15 20 25 30 35 mpg Weight fill: #F5F6F8;' /> 100 200 300 400 10 15 20 25 30 35 mpg Displacement
# With axis collection (removes redundant x-axis from top)
collect_axes(p_top / p_bottom)
fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 2 3 4 5 Weight fill: #F5F6F8;' /> 100 200 300 400 10 15 20 25 30 35 mpg Displacement

Combining with Styling

ggguides functions work together:

p1_styled <- p1 + legend_style(size = 11, title_face = "bold")
p2_styled <- p2 + legend_style(size = 11, title_face = "bold")

collect_legends(p1_styled | p2_styled, position = "right")
fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 2 3 4 5 10 15 20 25 30 35 mpg wt Plot 1 fill: #F5F6F8;' /> 100 200 300 10 15 20 25 30 35 mpg hp Plot 2 fill: #F5F6F8;' /> fill: #F5F6F8;' /> Cylinders fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 4 6 8

Complex Layouts

Handle more complex patchwork layouts:

p4 <- ggplot(mtcars, aes(qsec, wt, color = factor(cyl))) +
  geom_point() + labs(title = "Plot 4", color = "Cylinders")

# 2x2 grid
layout <- (p1 | p2) / (p3 | p4)
collect_legends(layout, position = "right")
fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 2 3 4 5 10 15 20 25 30 35 mpg wt Plot 1 fill: #F5F6F8;' /> 100 200 300 10 15 20 25 30 35 mpg hp Plot 2 fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 100 200 300 400 10 15 20 25 30 35 mpg disp Plot 3 fill: #F5F6F8;' /> 2 3 4 5 16 18 20 22 qsec wt Plot 4 fill: #F5F6F8;' /> fill: #F5F6F8;' /> Cylinders fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 4 6 8

cowplot / Grid Users

ggguides provides cowplot-compatible functions that work without patchwork:

get_legend() - Extract Legend

# Extract legend from a plot
leg <- get_legend(cp1)

# Use with cowplot::plot_grid() or grid::grid.draw()
grid::grid.newpage()
grid::grid.draw(leg)
fill: #F5F6F8;' /> fill: #F5F6F8;' /> Cylinders fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 4 6 8

shared_legend() - Combine Plots with Shared Legend

# Side-by-side with shared legend on right
gt <- shared_legend(cp1, cp2, ncol = 2, position = "right")
grid::grid.newpage()
grid::grid.draw(gt)
fill: #F5F6F8;' /> fill: #F5F6F8;' /> 2 3 4 5 10 15 20 25 30 35 mpg wt Plot 1 fill: #F5F6F8;' /> fill: #F5F6F8;' /> 100 200 300 10 15 20 25 30 35 mpg hp Plot 2 fill: #F5F6F8;' /> fill: #F5F6F8;' /> Cylinders fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 4 6 8
# Stacked with legend at bottom
gt <- shared_legend(cp1, cp2, cp3, ncol = 1, position = "bottom")
grid::grid.newpage()
grid::grid.draw(gt)
fill: #F5F6F8;' /> fill: #F5F6F8;' /> 2 3 4 5 10 15 20 25 30 35 mpg wt Plot 1 fill: #F5F6F8;' /> fill: #F5F6F8;' /> 100 200 300 10 15 20 25 30 35 mpg hp Plot 2 fill: #F5F6F8;' /> fill: #F5F6F8;' /> 100 200 300 400 10 15 20 25 30 35 mpg disp Plot 3 fill: #F5F6F8;' /> fill: #F5F6F8;' /> Cylinders fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 4 6 8
# 2x2 grid
gt <- shared_legend(cp1, cp2, cp3, cp4, ncol = 2, nrow = 2, position = "right")
grid::grid.newpage()
grid::grid.draw(gt)
fill: #F5F6F8;' /> fill: #F5F6F8;' /> 2 3 4 5 10 15 20 25 30 35 mpg wt Plot 1 fill: #F5F6F8;' /> fill: #F5F6F8;' /> 100 200 300 10 15 20 25 30 35 mpg hp Plot 2 fill: #F5F6F8;' /> fill: #F5F6F8;' /> 100 200 300 400 10 15 20 25 30 35 mpg disp Plot 3 fill: #F5F6F8;' /> fill: #F5F6F8;' /> 2 3 4 5 16 18 20 22 qsec wt Plot 4 fill: #F5F6F8;' /> fill: #F5F6F8;' /> Cylinders fill: #F5F6F8;' /> fill: #F5F6F8;' /> fill: #F5F6F8;' /> 4 6 8

All ggguides styling functions (legend_style(), legend_wrap(), etc.) work on individual plots regardless of layout package.

Summary

Function Purpose Key Parameters
collect_legends() Gather legends from patchwork position, span
collect_axes() Remove duplicate axes guides
get_legend() Extract legend as grob -
shared_legend() Combine plots with shared legend ncol, nrow, position

Learn more: