Return empty data frame from dapply() for empty input - #6899
Open
GuangchuangYu wants to merge 1 commit into
Open
GuangchuangYu wants to merge 1 commit into
GuangchuangYu wants to merge 1 commit into
Conversation
GuangchuangYu
added a commit
to YuLab-SMU/ggtree
that referenced
this pull request
Sep 14, 2026
`xlim()` (and `scale_x_continuous(limits = )`) apply to *every* panel, so on a plot carrying annotation panels added by `geom_facet()` they clip the annotation data as well. Recommend `xlim_tree()` (tree panel only), `xlim_expand()` (one specific panel) or `coord_cartesian()` (zoom without dropping data) instead. When an entire annotation layer ends up outside the limits, ggplot2 <= 4.0.3 aborts with "argument must be coercible to non-negative integer". That is not a ggtree bug: it was root-caused to `ggplot2:::dapply()` splitting zero rows (`max(integer(0))` is `-Inf`, so `seq_len(-Inf)`), reproducible with ggplot2 alone, and fixed upstream in tidyverse/ggplot2#6899. See #658
When a layer's data is entirely removed by scale limits (e.g. xlim()) and
the plot has multiple panels, dapply() tried to split zero rows: id()
returns integer(0), so split_with_index() computed
seq_len(max(integer(0))), i.e. seq_len(-Inf), and aborted with
seq_len(): ! argument must be coercible to non-negative integer
Zero rows means zero groups, so fun() should never be called. dapply()
now short-circuits to an empty data frame, which also avoids the phantom
`PANEL = NA` row that apply_fun() would otherwise produce.
Reproducible without any add-on package:
d1 <- data.frame(panel = "a", x = c(1, 2))
d2 <- data.frame(panel = "b", x = c(10, 20))
ggplot() +
geom_boxplot(data = d1, aes(x = x)) +
geom_boxplot(data = d2, aes(x = x)) +
facet_wrap(~panel) + xlim(0, 5)
Reported downstream as YuLab-SMU/ggtree#658.
GuangchuangYu
force-pushed
the
dapply-empty-data
branch
from
September 14, 2026 03:41
74458ae to
8582525
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A layer whose data is entirely removed by scale limits aborts the plot build when the plot has more than one panel.
Full message on ggplot2 4.0.3:
xlim(0, 5)drops both rows of the second layer, so by the time the stat runsthe layer has zero rows and the plot has two panels. With a single panel, or
with a partially-censored layer, everything works — which is why this is easy to
hit by accident and hard to diagnose.
Cause
Stat$compute_layer()finishes withdapply(data, "PANEL", ...). For an emptydata,dapply()still tries to split:split_with_index()derives its number of groups frommax(f); with no rowsthat is
max(integer(0)), i.e.-Inf, soseq_len(-Inf)throws.There is a second, latent problem behind it: even if the split succeeded, the
callback does
layout$get_scales(data$PANEL[1])with an emptydata, so itpasses
NA.get_scales()then selects every row of a multi-row layout(
self$layout$PANEL == NAisNAfor each row), andself$panel_scales_x[[c(NA, NA)]]is a recursive index, giving! no such index at level 1.Fix
Zero rows means zero groups, so
fun()must not be called at all.dapply()now short-circuits to an empty data frame before splitting:
This resolves both symptoms, and also removes a pre-existing wart: the old
single-group shortcut ran
apply_fun()on an emptydf, which produced aphantom row with
PANEL = NA.Downstream report
Reported as YuLab-SMU/ggtree#658,
where
xlim()combined withgeom_facet()annotation panels hit this. Thereprex above reproduces it with ggplot2 alone — no add-on packages involved.
Tests
New
tests/testthat/test-compat-plyr.Rcovers:fun();to an empty one;
Full suite: no new failures. The only failing test,
test-zzz.R(".onAttachdoes not modify the random stream"), fails identicallywith and without this patch — it is an artefact of running the suite through
pkgload::load_all(), since.onAttach()is not attached to the search paththat way.