Skip to content

Figure_Plot drops panels when input is a multi-panel patchwork (figure_plot = TRUE + split.by) #265

Description

@Pereira-Lab-UF

Summary

Calling FeaturePlot_scCustom(figure_plot = TRUE, split.by = <multi-level meta column>) silently drops most of the per-split panels — patchwork emits "Too few patch areas to hold all plots. Dropping plots." and the remaining 1–2 panels render stretched into a single column rather than the expected num_columns-wide grid.

The root cause is in the internal Figure_Plot() helper, not in FeaturePlot_scCustom() itself.

Reproducer

library(Seurat)
library(scCustomize)
data("pbmc_small")
# pbmc_small has a "groups" metadata column with two levels — minimal repro
p <- FeaturePlot_scCustom(
  seurat_object = pbmc_small,
  features      = "CD3E",
  split.by      = "groups",   # 2 levels in pbmc_small
  num_columns   = 2,
  figure_plot   = TRUE
)
p
# Warning message:
# Too few patch areas to hold all plots. Dropping plots
# Expected: 1x2 grid of CD3E expression for both split levels + arrow axis legend
# Actual:   only one split level visible + the arrow axis legend

For higher-cardinality splits (e.g. 7 time points × num_columns = 4), only 1–2 of the 7 panels actually render.

Root cause

R/Plotting_Utilities_Internal.R (~lines 510–542):

Figure_Plot <- function(plot) {
  x_lab_reduc <- plot$labels$x
  y_lab_reduc <- plot$labels$y
  plot <- plot & NoAxes()
  axis_plot <- ggplot(...) + ...     # the arrow axis legend
  figure_layout <- c(
    area(t = 1,  l = 2, b = 11, r = 11),
    area(t = 10, l = 1, b = 12, r = 2))
  plot_figure <- plot + axis_plot + plot_layout(design = figure_layout)
  return(plot_figure)
}
  • If plot is a single ggplot (no split), plot + axis_plot produces a 2-plot patchwork → 2 design areas → fits.
  • If plot is already a multi-panel patchwork (from split.by), plot + axis_plot flattens into an (n_split + 1)-plot patchwork. figure_layout still has 2 areas → patchwork warns and drops the excess panels.

Proposed fix

Wrap the input in patchwork::wrap_elements() whenever it is already a patchwork, so the whole composite occupies exactly one design slot. Pulling axis labels from the first panel keeps the arrow-legend behavior consistent:

 Figure_Plot <- function(
     plot
 ){
-  # pull axis labels
-  x_lab_reduc <- plot$labels$x
-  y_lab_reduc <- plot$labels$y
+  # pull axis labels — fall back to the first panel for multi-panel input
+  is_composite <- inherits(plot, "patchwork") && length(plot$patches$plots) > 0
+  if (is_composite) {
+    first_panel <- plot$patches$plots[[1]]
+    x_lab_reduc <- first_panel$labels$x %||% plot$labels$x
+    y_lab_reduc <- first_panel$labels$y %||% plot$labels$y
+  } else {
+    x_lab_reduc <- plot$labels$x
+    y_lab_reduc <- plot$labels$y
+  }

   plot <- plot & NoAxes()

+  # Multi-panel input from `split.by` must occupy a single design area;
+  # without wrap_elements() it expands into n+1 plots and patchwork drops
+  # the excess panels ("Too few patch areas to hold all plots").
+  if (is_composite) {
+    plot <- wrap_elements(plot)
+  }
+
   axis_plot <- ggplot(data.frame(x = 100, y = 100), aes(x = .data[["x"]], y = .data[["y"]])) +
     geom_point() +
     xlim(c(0, 10)) + ylim(c(0, 10)) +
     theme_classic() +
     ylab(y_lab_reduc) + xlab(x_lab_reduc) +
     theme(plot.background  = element_rect(fill = "transparent", colour = NA),
           panel.background = element_rect(fill = "transparent"),
           axis.text.x      = element_blank(),
           axis.text.y      = element_blank(),
           axis.ticks       = element_blank(),
           axis.line        = element_line(arrow = arrow(angle = 15, length = unit(.5, "cm"), type = "closed")))

   figure_layout <- c(
     area(t = 1,  l = 2, b = 11, r = 11),
     area(t = 10, l = 1, b = 12, r = 2))

   plot_figure <- plot + axis_plot +
     plot_layout(design = figure_layout)

   return(plot_figure)
 }

With this patch, FeaturePlot_scCustom(features = X, split.by = time_point, num_columns = 4, figure_plot = TRUE) on a dataset with 7 time points renders the full 4×2 grid of per-time-point panels — each with no panel axes, retaining the strip titles ("0.5h", "Roots", …), and the shared UMAP arrow legend in the bottom-left corner. Same visual style as the single-panel figure_plot = TRUE output.

I've been running this fix in a local fork against scCustomize 3.2.4 (in a Shiny atlas with split-by-time-point feature plots) and it behaves correctly across cardinalities from 2 to 18 split levels.

Environment

  • scCustomize 3.2.4 (also reproduces on master as of 2026-05 — Figure_Plot definition is unchanged)
  • patchwork 1.2.0
  • ggplot2 3.5.x
  • Seurat 5.5.0
  • R 4.3.2

Happy to open a PR with this change + a NEWS.md entry under "Fixes" in the 3.3.0 section — just let me know your preferred workflow.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions