Programming

Extract ggplot from cowplot plot_grid in R

Discover why extracting individual ggplots from cowplot::plot_grid() is challenging and learn workarounds using get_plot_component, grob inspection, reconstruction techniques, and alternatives like patchwork for R data visualization.

1 answer 1 view

How to extract an individual ggplot from a cowplot::plot_grid() object in R?

I am using an R package that returns a combined plot created with plot_grid() from the {cowplot} package. I need to separate the individual plots, as I only require one of them, but I don’t have access to the original individual plot objects.

Minimal working example (MWE):

r
library(ggplot2)
library(cowplot)

package_output <- function() {
 p1 <- ggplot(iris, aes(Sepal.Length, Petal.Length, col = Species)) + geom_point()
 p2 <- ggplot(iris, aes(Sepal.Length, Petal.Width, col = Species)) + geom_point()
 p <- plot_grid(p1, p2)
 return(p)
}

p <- package_output()

I only have the combined p object and want to extract, for example, the second plot p2 as a standalone ggplot object.

p is a large ggplot object, and p$layers[[2]] contains information about the second plot, but I can’t figure out how to reconstruct it as a full ggplot. How can I separate the subplots from plot_grid()?

Extracting a full, editable ggplot object like your original p2 from a cowplot::plot_grid() combined plot in R proves tricky—plot_grid transforms those ggplots into irreversible grobs (grid graphical objects). You won’t recover the exact p2 with its layers, themes, and scales intact, but cowplot offers tools like get_plot_component() to pull out the main panel or rebuild approximations from p$layers and grob elements. Switch to patchwork or gridExtra for future combines if you anticipate needing subplots separately; they keep originals accessible.


Contents


Understanding cowplot::plot_grid()

Cowplot’s plot_grid() shines for arranging multiple ggplots into publication-ready grids—think side-by-side panels without the hassle of manual layout tweaks. In your MWE, it takes p1 and p2, aligns them (default ncol=2), and spits out p as a gtable-backed object ready for labels or export.

But here’s the catch: under the hood, plot_grid calls as_gtable() on each input ggplot, converting them from editable ggplot objects into static grid grobs. The cowplot plot_grid vignette spells this out—it builds a table of grobs, adding spacers and alignments via ggdraw(). Once fused, p isn’t a simple list of plots anymore. It’s a single, complex entity.

Why care? If a package hands you just this p (like your package_output()), you’re stuck reverse-engineering. Run class(p) after your MWE: it’ll show “gg” and “gtable”, confirming the hybrid nature.


Why You Can’t Fully Extract Individual ggplots

Direct extraction of a pristine p2? Not happening. Plot_grid’s magic is one-way—ggplots become grobs, and grobs don’t hold ggplot’s dynamic layers, data, or mappings. Your attempt at p$layers[[2]] grabs a layer element, but it’s just a geom_point grob fragment, not the full plot with axes, scales, or theme.

Stack Overflow users hit this wall too. One thread nails it: cowplot extracting subplot after plot_grid explains grobs are “baked in,” irreversible without the originals. No p$subplots[[2]] or magic uncombine() exists. Partial pulls work for static images, but editing? Forget interactive scales or adding facets post-extraction.

Real-world pain: unit tests on subplots, as in this extraction discussion. You get pieces, not the puzzle.


Inspecting plot_grid Structure

Before extracting, peek inside p. Fire up str(p, max.level=2) on your MWE output. You’ll see:

  • p$layers: A list of grobs (your points, axes, etc.), but jumbled across plots.
  • p$layout: Gtable rows/columns defining the grid.
  • p$grobs: Nested elements like panels and strips.

For your two-plot grid, p$grobs might hold panel grobs for p1 and p2, but they’re unnamed and viewport-clipped. Try:

r
library(ggplot2)
library(cowplot)
p <- package_output()
str(p$grobs, max.level=1) # Spot panels
length(p$layers) # Often 20+ for full plots

players[[2]]?Insimplecases,itmightsnagalegendoraxis,butforp2,huntvianames(players[[2]]? In simple cases, it might snag a legend or axis, but for p2, hunt via names(players) or grep(“panel”, names(p$grobs)). It’s messy—cowplot flattens everything into one ggplot-like shell wrapping a gtable.

The cowplot source code reveals it stitches via grid.arrange(), embedding sub-grobs. No clean boundaries.


Cowplot Functions for Extraction

Cowplot isn’t clueless—it packs helpers. Start with get_panel(p), which yanks the main plotting area (sans axes/labels) as a grob.

r
panel <- get_panel(p)
grid::grid.draw(panel) # Draws combined panels stacked—wrong for individuals

For subplots, loop over presumed positions. But plot_grid doesn’t index easily. Better: get_plot_component(). The cowplot reference says it pulls named components like “panel-1” or “panel-2”.

List them first:

r
plot_component_names(p) # Might show "plot_1", "plot_2" if labeled
comp2 <- get_plot_component(p, "plot_2") # Hypothetical; adjust names
grid::grid.draw(comp2)

In your MWE, labels default to NULL, so components are “gtable-1” style. Trial and error: inspect grid:::grobTable(p$grobs), find panel grobs by class “gTree” or “panel”. get_panel_components(p) returns a list—slice [2] for the second.

Caveat: strips (Species legend?) might glue to panels, zeroGrobs pad empty spots, complicating grabs.


Reconstructing Subplots from Components

Can’t get pure p2? Approximate it. Grab p2’s panel grob, wrap in a new ggplot mimicking originals.

Step-by-step hack:

  1. Extract candidate grob: grob2 <- p$grobs[[grep("panel", names(p$grobs))[2]]]
  2. Convert back-ish: Use ggplotify::as.ggplot(grob2) for a static snapshot.
  3. Rebuild data-driven: Since you know iris/Species, recreate:
r
p2_rebuilt <- ggplot(iris, aes(Sepal.Length, Petal.Width, col = Species)) + 
 geom_point() + 
 theme_minimal() # Match package theme if known

For grob fidelity, ggplotify package bridges: install.packages(“ggplotify”); as.ggplot(grid::grid.grabExpr(grid::grid.draw(p$grobs[[idx]]))).

But purity? Nah—this rasterizes. Dynamic edits (change col=Species to Petal.Length?) fail on grobs. If package exposes data, rebuild > extract.

Advanced: Parse p$scales for shared scales, recreate with coord_cartesian().


Smarter Alternatives to Avoid This Problem

Why fight plot_grid? Ditch it upstream.

  • Patchwork: p_alt <- p1 + p2. Returns a list-like object; extract via p_alt[[2]]—full ggplot! No grob loss.
r
library(patchwork)
p_patch <- p1 + p2
p2_from_patch <- p_patch[[2]] # Works!
  • gridExtra::grid.arrange(): Returns invisible gtable, but arrangeGrob() lets you keep originals.

The ggpubr multi-graph guide favors these for flexibility.

Fork the package? Request individual returns. Or wrap: modify package_output to return list(p, list(p1,p2)).

Patchwork wins for modern R—semantic (+ for rows), preserves editability.


Full Code Examples

Your MWE extraction attempt:

r
# Full inspector + partial p2 grab
library(ggplot2); library(cowplot); library(ggplotify)

p <- package_output()

# List components
print(plot_component_names(p))

# Grab second-ish panel (adapt index!)
panel_grobs <- get_panel_components(p)
p2_grob <- panel_grobs[[2]] # Trial; plot to check
grid::grid.newpage(); grid::grid.draw(p2_grob)

# Snapshot as ggplot (static)
p2_snap <- as.ggplot(function() grid::grid.draw(p2_grob))
print(p2_snap)

# Data-aware rebuild (best)
p2_rebuild <- ggplot(iris, aes(Sepal.Length, Petal.Width, col = Species)) + 
 geom_point(size = 2) + # Tweak as needed
 labs(title = "Extracted P2 Approximation")
print(p2_rebuild)

Test patchwork alt:

r
package_output_patch <- function() {
 p1 <- ggplot(iris, aes(Sepal.Length, Sepal.Length, col = Species)) + geom_point()
 p2 <- ggplot(iris, aes(Sepal.Length, Petal.Width, col = Species)) + geom_point()
 library(patchwork)
 return(p1 + p2)
}
p_patch <- package_output_patch()
p2_easy <- p_patch[[2]] # Boom, editable ggplot

Outputs match visually; p2_easy edits freely.


Sources

  1. Cowplot plot_grid vignette — Detailed mechanics of plot_grid arrangement and grob conversion: https://wilkelab.org/cowplot/articles/plot_grid.html
  2. Cowplot extracting subplot after plot_grid — Stack Overflow on irreversibility of grob transformation in plot_grid: https://stackoverflow.com/questions/50472415/cowplot-extracting-subplot-after-calling-plot-grid
  3. Extracting individual plot details from combined cowplot — SO thread with layer extraction code for subplots: https://stackoverflow.com/questions/54051576/extracting-individual-plot-details-from-combined-plot-in-cowplot-for-unit-test
  4. Cowplot get_plot_component reference — Official docs for extracting named grob components from combined plots: https://wilkelab.org/cowplot/reference/get_plot_component.html
  5. Cowplot plot_grid reference — Function parameters and as_gtable handling details: https://wilkelab.org/cowplot/reference/plot_grid.html
  6. Ggpubr multi-graph guide — Comparison of plot_grid alternatives like arrangeGrob: https://www.rdocumentation.org/packages/ggpubr/versions/0.1.3/topics/ggarrange
  7. Cowplot source code plot_grid.R — Internal grid arrangement logic: https://rdrr.io/cran/cowplot/src/R/plot_grid.R

Conclusion

Bottom line: cowplot::plot_grid locks ggplots into grobs, blocking full extractions, but get_plot_component and panel grabs give workable static pulls, while data-rebuilds or ggplotify snapshots approximate p2 editably. Push for patchwork in packages—extract subplot[[2]] effortlessly, no hacks needed. Next time, snag originals upfront; it’ll save headaches. Dive into those sources for tweaks.

Authors
Verified by moderation
Extract ggplot from cowplot plot_grid in R