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.
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):
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()
- Why You Can’t Fully Extract Individual ggplots
- Inspecting plot_grid Structure
- Cowplot Functions for Extraction
- Reconstructing Subplots from Components
- Smarter Alternatives to Avoid This Problem
- Full Code Examples
- Sources
- Conclusion
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:
library(ggplot2)
library(cowplot)
p <- package_output()
str(p$grobs, max.level=1) # Spot panels
length(p$layers) # Often 20+ for full plots
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.
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:
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:
- Extract candidate grob:
grob2 <- p$grobs[[grep("panel", names(p$grobs))[2]]] - Convert back-ish: Use ggplotify::as.ggplot(grob2) for a static snapshot.
- Rebuild data-driven: Since you know iris/Species, recreate:
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.
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:
# 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:
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
- Cowplot plot_grid vignette — Detailed mechanics of plot_grid arrangement and grob conversion: https://wilkelab.org/cowplot/articles/plot_grid.html
- 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
- 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
- Cowplot get_plot_component reference — Official docs for extracting named grob components from combined plots: https://wilkelab.org/cowplot/reference/get_plot_component.html
- Cowplot plot_grid reference — Function parameters and as_gtable handling details: https://wilkelab.org/cowplot/reference/plot_grid.html
- Ggpubr multi-graph guide — Comparison of plot_grid alternatives like arrangeGrob: https://www.rdocumentation.org/packages/ggpubr/versions/0.1.3/topics/ggarrange
- 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.