Skip to content

Inappropriate scoping of after_scale variables when resolving legend data #6264

Closed
@yjunechoe

Description

@yjunechoe

The bug concerns a case that's seemingly specific to stage(start, after_scale), when the after_scale expression uses a variable from the layer's after-scale data which is unavailable from the guide data.

As a preliminary, we know that after scale expressions are ordinarily resolved against both the guide data (first data printed by trace) and the layer data (second data printed by trace).

library(ggplot2)
trace("use_defaults", tracer = quote(print(head(data))), where = Geom)
#> Tracing function "use_defaults" in package ".GlobalEnv"
#> [1] "use_defaults"
invisible(ggplot_build(
  ggplot(mtcars, aes(am)) +
    geom_bar(aes(fill = as.factor(cyl)))
))
#> Tracing use_defaults(..., self = self) on entry 
#>      fill .id
#> 1 #F8766D   1
#> 2 #00BA38   2
#> 3 #619CFF   3
#> Tracing use_defaults(..., self = self) on entry 
#>      fill  y count      prop x flipped_aes PANEL group ymin ymax  xmin xmax
#> 1 #F8766D 19     3 0.2727273 0       FALSE     1     1   16   19 -0.45 0.45
#> 2 #F8766D 13     8 0.7272727 1       FALSE     1     1    5   13  0.55 1.45
#> 3 #00BA38 16     4 0.5714286 0       FALSE     1     2   12   16 -0.45 0.45
#> 4 #00BA38  5     3 0.4285714 1       FALSE     1     2    2    5  0.55 1.45
#> 5 #619CFF 12    12 0.8571429 0       FALSE     1     3    0   12 -0.45 0.45
#> 6 #619CFF  2     2 0.1428571 1       FALSE     1     3    0    2  0.55 1.45
untrace("use_defaults", where = Geom)
#> Untracing function "use_defaults" in package ".GlobalEnv"

One can use a variable from the after scale data like prop to remap to the fill aesthetic using stage(), like so. Note that the example turns the legend off for the layer to demonstrate the expected output:

ggplot(mtcars, aes(am)) +
  geom_bar(
    aes(
      fill = stage(
        start = as.factor(cyl),
        after_scale = alpha(fill, prop)
      )
    ),
    show.legend = FALSE
  )

Image

Now, with the legend turned back on, the plot errors, presumably because while prop exists in the after-scale data, it does not exist in the guides data.

p <- ggplot(mtcars, aes(am)) +
  geom_bar(
    aes(
      fill = stage(
        start = as.factor(cyl),
        after_scale = alpha(fill, prop)
      )
    ),
    show.legend = TRUE # or `= NA`
  )
p
#> Error: object 'prop' not found

Since aes expressions are resolved via tidy-eval, this also leads to a surprising behavior where prop gets scoped in a parent environment, when available, to resolve the guide data.

library(grid)

prop <- c(0, 0.5, 1)
grid.newpage()
ggplotGrob(p) |>
  gtable::gtable_filter("guide-box-right") |>
  grid.draw()

Image

prop <- 1
grid.newpage()
ggplotGrob(p) |>
  gtable::gtable_filter("guide-box-right") |>
  grid.draw()

Image


Two related behaviors worth noting:

  1. When only the after_scale is specified, the layer does not generate a legend at all for the aesthetic:
library(ggplot2)
ggplot(mtcars, aes(am)) +
  geom_bar(
    aes(
      group = as.factor(cyl),
      fill = after_scale(alpha(scales::hue_pal()(3)[group], prop))
       # also `stage(after_scale = alpha(scales::hue_pal()(3)[group], prop))`
    )
  )

Image

  1. When the after-scale expression in stage(start, after_scale) is valid for both sets of data, there is no error and the legend reflects the after-scale transformation.
ggplot(mtcars, aes(am)) +
  geom_bar(
    aes(
      fill = stage(as.factor(cyl), after_scale = alpha(fill, .5))
    )
  )

Image

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions