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.
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 expectednum_columns-wide grid.The root cause is in the internal
Figure_Plot()helper, not inFeaturePlot_scCustom()itself.Reproducer
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):plotis a single ggplot (no split),plot + axis_plotproduces a 2-plot patchwork → 2 design areas → fits.plotis already a multi-panel patchwork (fromsplit.by),plot + axis_plotflattens into an(n_split + 1)-plot patchwork.figure_layoutstill 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-panelfigure_plot = TRUEoutput.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
masteras of 2026-05 —Figure_Plotdefinition is unchanged)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.