Multipanel figures
Skill BioTender-max/awesome-bio-agent-skills/skills/bioskills/multipanel-figures
A curated collection of AI agent skills for biomedical research, covering genomics, proteomics, single-cell analysis, clinical AI, and protein design.
npx -y skills add BioTender-max/awesome-bio-agent-skills --skill multipanel-figuresAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
What its author says it does
Copied from the file, not written here
Compose multi-panel publication figures with patchwork, cowplot, gridExtra (R), or matplotlib GridSpec/subfigures (Python) including shared axes/legends/guides collection, panel labels in Nature/Cell convention, and journal-spec sizing. Covers patchwork ≥1.2.0 axes='collect' feature, Type-42 font embedding, and the cairo_pdf save path. Use when composing 2+ subpanels into a single figure for journal submission.
SKILL.md
11.9 KB, as published. Nobody here has run it
Version Compatibility
Reference examples tested with: patchwork 1.2+ (axes='collect' requires this version, released 2024-01-05), cowplot 1.1+, ggplot2 3.5+, matplotlib 3.8+ (subfigures stable since 3.4).
Before using code patterns, verify installed versions match. If versions differ:
- R:
packageVersion('<pkg>')then?function_name - Python:
pip show <package>thenhelp(module.function)
If code throws ImportError, AttributeError, or TypeError, introspect the installed package and adapt the example to match the actual API rather than retrying.
Multi-Panel Figures
"Combine plots into a multi-panel figure" -> Arrange individual plots into a single composed figure with consistent sizing, shared legends/axes, and panel labels (a, b, c) in the Nature/Cell convention. The decision space: which composition library (patchwork most modern in R; matplotlib subfigures in Python), how to share legends and axes, and how to size at journal specifications.
- R:
patchwork(modern; supports axes/guides collection since 1.2),cowplot(older; align_plots),gridExtra(basic grid arrange) - Python:
matplotlib.gridspec.GridSpec,fig.subfigures()(matplotlib 3.4+)
The Single Most Important Modern Insight -- Axes Collection Requires patchwork ≥ 1.2.0
patchwork 1.2.0 (released 2024-01-05) added axes = 'collect' and axis_titles = 'collect' to plot_layout(). These collect repeated axes / titles across subplots into a single shared axis label — the same way guides = 'collect' (available since patchwork 1.0) collects legends.
Without this, multi-panel figures with shared axes show redundant labels on every subplot (visually cluttered AND non-Nature compliant). Verify patchwork version is ≥ 1.2.0; older versions silently ignore the axes argument.
patchwork -- Modern R Composition
Goal: Compose 4 ggplot objects into a 2×2 panel figure with shared legend, collected axes, and bold panel labels (a, b, c, d) in upper-left of each subplot.
Approach: Combine plots with +, /, | operators; apply plot_layout(guides='collect', axes='collect') for shared elements; add plot_annotation(tag_levels='a') for Nature-style panel labels.
library(patchwork)
library(ggplot2)
p1 <- ggplot(df, aes(x, y)) + geom_point() + theme_classic()
p2 <- ggplot(df, aes(group, value)) + geom_boxplot() + theme_classic()
p3 <- ggplot(df, aes(x)) + geom_histogram() + theme_classic()
p4 <- ggplot(df, aes(x, y, color = group)) + geom_point() + theme_classic()
# 2x2 grid
fig <- (p1 + p2) / (p3 + p4) +
plot_annotation(tag_levels = 'a',
theme = theme(plot.tag = element_text(face = 'bold', size = 10))) +
plot_layout(guides = 'collect', # share legends
axes = 'collect', # share axes (patchwork >= 1.2.0)
axis_titles = 'collect')
ggsave('figure1.pdf', fig, width = 180, height = 140, units = 'mm', device = cairo_pdf)
patchwork Operators
p1 + p2 # side-by-side
p1 / p2 # vertical stack
(p1 | p2) / p3 # mixed: top row two, bottom one
p1 + p2 + p3 + plot_layout(ncol = 3)
p1 + p2 + plot_layout(widths = c(2, 1)) # 2:1 width ratio
# Complex grid via design string
design <- "
AAB
AAB
CCC
"
p1 + p2 + p3 + plot_layout(design = design)
# Inset
p1 + inset_element(p2, left = 0.6, bottom = 0.6, right = 1, top = 1)
cowplot -- Alternative with Alignment Focus
library(cowplot)
# plot_grid is the workhorse
combined <- plot_grid(p1, p2, p3, p4,
ncol = 2, labels = 'AUTO', # 'AUTO' = A, B, C, D
label_size = 12, label_fontface = 'bold',
align = 'hv', # align horizontally + vertically
rel_widths = c(1, 1), rel_heights = c(1, 1))
# Nested grids
top_row <- plot_grid(p1, p2, ncol = 2, labels = c('A', 'B'))
bottom <- plot_grid(p3, p4, ncol = 2, labels = c('C', 'D'))
combined <- plot_grid(top_row, bottom, nrow = 2, rel_heights = c(1, 1.2))
ggsave('figure.pdf', combined, width = 180, height = 140, units = 'mm', device = cairo_pdf)
cowplot is older but its alignment behavior is sometimes more reliable than patchwork on edge cases (axes-with-titles of different lengths).
matplotlib GridSpec (Python)
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
fig = plt.figure(figsize=(180/25.4, 120/25.4), constrained_layout=True)
gs = GridSpec(2, 3, figure=fig)
ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[0, 1:]) # top right, spans columns 1-2
ax3 = fig.add_subplot(gs[1, :]) # bottom row, spans all columns
ax1.scatter(x, y, s=4, rasterized=True)
ax2.plot(x, y)
ax3.bar(cats, vals)
# Panel labels at (-0.15, 1.05) of each axes
for ax, lbl in zip([ax1, ax2, ax3], 'abc'):
ax.text(-0.15, 1.05, lbl, transform=ax.transAxes,
fontsize=10, fontweight='bold', va='top')
fig.savefig('figure.pdf', dpi=300, bbox_inches='tight')
matplotlib Subfigures
fig = plt.figure(figsize=(180/25.4, 120/25.4), constrained_layout=True)
subfigs = fig.subfigures(1, 2, width_ratios=[2, 1])
# Left subfigure has 2 stacked panels
axs_left = subfigs[0].subplots(2, 1)
axs_left[0].plot(x, y)
axs_left[1].scatter(x, y, rasterized=True)
# Right subfigure has one panel
ax_right = subfigs[1].subplots(1, 1)
ax_right.imshow(matrix)
subfigs[1].colorbar(ax_right.images[0], ax=ax_right, shrink=0.5)
Subfigures are stronger than GridSpec for complex compositions because each subfigure has its own constrained_layout.
Journal Sizing
| Journal | Single col | Double col | Max height |
|---|---|---|---|
| Nature | 89 mm | 183 mm | 247 mm |
| Cell | 85 mm | 174 mm | 235 mm |
| Science | 55 mm | 120 mm | 220 mm |
| PNAS | 87 mm | 178 mm | 225 mm |
| eLife | 86 mm | 175 mm | ~240 mm |
Always set explicit units in mm; default inches is the most common source of "figure too large" errors.
Panel Labels — Nature/Cell Convention
- Nature: lowercase bold serif (a, b, c) in upper-left corner of each panel; 8 pt
- Cell: uppercase bold sans-serif (A, B, C); placed flush left at panel top
- Science: capital bold (A, B, C)
# patchwork tag_levels for lowercase (Nature)
plot_annotation(tag_levels = 'a',
theme = theme(plot.tag = element_text(face = 'bold', size = 9)))
# 'A' for uppercase (Cell)
plot_annotation(tag_levels = 'A')
# 'i' for roman numerals (sometimes for sub-panels)
# cowplot
plot_grid(..., labels = 'AUTO') # auto uppercase A, B, C
plot_grid(..., labels = 'auto') # auto lowercase a, b, c
Per-Method Failure Modes
patchwork axes='collect' silently ignored
Trigger: Using plot_layout(axes='collect') with patchwork < 1.2.0.
Mechanism: Older versions silently accept the argument but don't act on it.
Symptom: Redundant axes on each subplot; no warning or error.
Fix: packageVersion('patchwork') must be ≥ 1.2.0. Update with install.packages('patchwork').
Default ggsave produces non-portable PDF
Trigger: ggsave('out.pdf', fig) without device = cairo_pdf.
Mechanism: Default pdf() device produces fonts that journals reject on some systems.
Symptom: Submission rejected at automated check; "non-embedded fonts."
Fix: Always device = cairo_pdf.
Figure dimensions in inches when mm intended
Trigger: ggsave('out.pdf', fig, width = 180, height = 140).
Mechanism: Default units = 'in'.
Symptom: Figure file rejected for being 180 × 140 inches.
Fix: Explicit units = 'mm'.
Panel labels not aligned to panel content
Trigger: patchwork plot_annotation(tag_levels) with subplots of different y-axis label widths.
Mechanism: Tag is positioned relative to the plot canvas, including the y-axis label area.
Symptom: Labels are at different horizontal positions in each panel.
Fix: Either standardize y-label widths (pad with whitespace) OR move tags inside the plotting area: theme(plot.tag.position = c(0.02, 0.98)).
cowplot align='v' fails on plots of different widths
Trigger: plot_grid(p_wide, p_narrow, align = 'v').
Mechanism: Vertical alignment requires same x-axis widths.
Symptom: Plots align at the y-axis but x-axis labels are offset.
Fix: Use align = 'hv' if both alignments needed; otherwise patchwork's axes='collect' handles this more gracefully.
Shared legend lost in patchwork
Trigger: (p1 + p2) + plot_layout(guides = 'collect') but p1 and p2 use different scales.
Mechanism: guides='collect' merges identical guides; different scales produce duplicate (not merged) legends.
Symptom: Two legends still appear.
Fix: Standardize the scales across subplots (same scale_color_manual(values=...)); OR drop one legend via & theme(legend.position = 'none') on the redundant plot.
matplotlib GridSpec with constrained_layout=False
Trigger: Older code with plt.subplots no constrained_layout; tight_layout fails on colorbars.
Mechanism: tight_layout doesn't know about post-hoc colorbars.
Symptom: Colorbar overlaps adjacent subplot.
Fix: plt.figure(constrained_layout=True) and use fig.add_subplot(gs[...]). constrained_layout is the default-on choice in matplotlib 3.6+.
Reconciliation
| Pattern | Cause | Action |
|---|---|---|
| patchwork and cowplot align differently | Different alignment algorithms | Try both; cowplot's align='hv' and patchwork's axes='collect' rarely produce identical results |
| Panel labels position differs between sessions | Different y-axis label widths | Standardize across panels |
| Shared legend duplicated | Scales differ across subplots | Use identical scales OR drop legend from N-1 panels |
Quantitative Thresholds
| Threshold | Value | Source |
|---|---|---|
| Nature single column | 89 mm | Nature figure guidelines |
| Nature double column | 183 mm | Nature figure guidelines |
| Body text size | 5-7 pt | Nature rejects outside range |
| Panel label size | 8 pt bold | Nature convention |
| patchwork axes='collect' minimum version | 1.2.0 (2024-01-05) | patchwork release notes |
Common Errors
| Error / symptom | Cause | Solution |
|---|---|---|
| Redundant axis labels per panel | patchwork < 1.2.0 OR axes='collect' not set | Update + add to plot_layout |
| Non-embedded font rejection | Default ggsave device | device = cairo_pdf |
| Figure 180 in × 140 in | Default units = 'in' | units = 'mm' |
| Panel tags misaligned | Different y-label widths | Standardize or move tag inside |
| Cowplot vertical alignment fails | Different x-axis widths | Use 'hv' OR switch to patchwork |
| Two legends instead of shared | Scales differ across subplots | Unify scales |
| matplotlib colorbar overlaps subplot | No constrained_layout | constrained_layout=True |
References
- Pedersen TL. 2024. patchwork: the composer of plots. CRAN package (v1.2.0 release notes).
- Wilke CO. 2017. cowplot: streamlined plot theme and plot annotations for ggplot2. CRAN package.
- Hunter JD. 2007. Matplotlib: A 2D graphics environment. Comput Sci Eng 9(3):90-95.
Related Skills
- data-visualization/ggplot2-fundamentals - Individual ggplot objects
- data-visualization/matplotlib-fundamentals - Python equivalent
- reporting/figure-export - DPI / format / journal-spec compliance
- data-visualization/color-palettes - Consistent palette across subpanels