Skip to content

Return empty data frame from dapply() for empty input - #6899

Open
GuangchuangYu wants to merge 1 commit into
tidyverse:mainfrom
GuangchuangYu:dapply-empty-data
Open

GuangchuangYu wants to merge 1 commit into
tidyverse:mainfrom
GuangchuangYu:dapply-empty-data

Conversation

@GuangchuangYu

Copy link
Copy Markdown
Contributor

Problem

A layer whose data is entirely removed by scale limits aborts the plot build when the plot has more than one panel.

library(ggplot2)

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)
#> Error in `seq_len()`:
#> ! argument must be coercible to non-negative integer

Full message on ggplot2 4.0.3:

Problem while computing stat.
i Error occurred in the 2nd layer.
Caused by error in `seq_len()`:
! argument must be coercible to non-negative integer

xlim(0, 5) drops both rows of the second layer, so by the time the stat runs
the 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 with dapply(data, "PANEL", ...). For an empty
data, dapply() still tries to split:

ids <- id(grouping_cols, drop = drop)              # integer(0)
group_rows <- split_with_index(seq_len(nrow(df)), ids)

split_with_index() derives its number of groups from max(f); with no rows
that is max(integer(0)), i.e. -Inf, so seq_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 empty data, so it
passes NA. get_scales() then selects every row of a multi-row layout
(self$layout$PANEL == NA is NA for each row), and
self$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:

if (nrow(df) == 0L) {
  return(data_frame0())
}

This resolves both symptoms, and also removes a pre-existing wart: the old
single-group shortcut ran apply_fun() on an empty df, which produced a
phantom row with PANEL = NA.

Downstream report

Reported as YuLab-SMU/ggtree#658,
where xlim() combined with geom_facet() annotation panels hit this. The
reprex above reproduces it with ggplot2 alone — no add-on packages involved.

Tests

New tests/testthat/test-compat-plyr.R covers:

  • normal split/apply/reassemble behaviour;
  • empty input returns an empty data frame without calling fun();
  • the single-group shortcut still applies to a non-empty single group, but not
    to an empty one;
  • the end-to-end reprex above builds.

Full suite: no new failures. The only failing test,
test-zzz.R (".onAttach does not modify the random stream"), fails identically
with and without this patch — it is an artefact of running the suite through
pkgload::load_all(), since .onAttach() is not attached to the search path
that way.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant