12

I am trying to create a function for creating lollipop plots using ggplot2. I would like to pass all argument within ... to aes() within geom_point(). However, I'd like to exclude the size argument from passing onto aes() within geom_segment() (for obvious reasons if you look at the ouput of a() below). Therefore I capture ... using rlang::enquos() instead of just passing it on as is. In function a() where I pass the dots to aes() within ggplot() this works without a problem. But in function b() I get the error Can't use '!!!' at top level.

I am stuck at this point and would appreciate any input to solve this issue.

library(ggplot2)
data("mtcars")

d <- dplyr::count(mtcars, cyl, am)

a <- function(data, x, y, ...) {
  x <- rlang::enquo(x)
  y <- rlang::enquo(y)
  dots <- rlang::enquos(...)

  ggplot(data, aes(!!x, !!y, !!!dots)) +
    geom_segment(aes(y = 0, xend = !!x, yend = !!y)) +
    geom_point()
}

b <- function(data, x, y, ...) {
  x <- rlang::enquo(x)
  y <- rlang::enquo(y)

  dots <- rlang::enquos(...)
  segment_args <- dots[names(dots) != "size"]

  ggplot(data, aes(!!x, !!y)) +
    geom_segment(aes(y = 0, xend = !!x, yend = !!y, !!!segment_args)) +
    geom_point(aes(!!!dots))
}

a(d, cyl, n, color = factor(am), size = am)


b(d, cyl, n, color = factor(am), size = am)
#> Error: Can't use `!!!` at top level.

Here is my sessionInfo():

R version 3.5.2 (2018-12-20)
Platform: x86_64-apple-darwin16.7.0 (64-bit)
Running under: macOS Sierra 10.12.1

Matrix products: default
BLAS: /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib
LAPACK: /usr/local/Cellar/openblas/0.3.5/lib/libopenblasp-r0.3.5.dylib

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods  
[7] base     

other attached packages:
[1] ggplot2_3.2.1

loaded via a namespace (and not attached):
 [1] Rcpp_1.0.3       digest_0.6.18    withr_2.1.2     
 [4] assertthat_0.2.0 crayon_1.3.4     dplyr_0.8.3     
 [7] grid_3.5.2       R6_2.3.0         gtable_0.2.0    
[10] magrittr_1.5     scales_1.0.0     pillar_1.4.2    
[13] rlang_0.4.2      lazyeval_0.2.1   rstudioapi_0.10 
[16] labeling_0.3     tools_3.5.2      glue_1.3.0      
[19] purrr_0.3.3      munsell_0.5.0    compiler_3.5.2  
[22] pkgconfig_2.0.2  colorspace_1.4-0 tidyselect_0.2.5
[25] tibble_2.1.3
Thomas Neitmann
  • 2,552
  • 1
  • 16
  • 31
  • Can't reproduce. Please add your `sessionInfo`. – NelsonGon Jan 03 '20 at 12:26
  • Only two key differences between my SessionInfo and yours. I'm using R 3.6.1(shouldn't matter) and `lazyeval` 0.2.2(most probable reason) although this [issue](https://github.com/r-lib/rlang/issues/228) suggests `lazyeval` was "abandoned" in favor of tidy eval. – NelsonGon Jan 03 '20 at 12:32
  • I ran the code as you have it and it works just fine for me. I'm on 3.5.3 – MCP_infiltrator Jan 03 '20 at 13:48
  • 1
    @NelsonGon Updating to version 0.2.2 of `lazyeval` did not resolve the issue. I'll try updating my `R` version. – Thomas Neitmann Jan 03 '20 at 13:56
  • @MCP_infiltrator could you add your `sessionInfo()`. I'd like to see the differences between mine and yours. – Thomas Neitmann Jan 03 '20 at 13:57
  • sessionInfo() R version 3.5.3 (2019-03-11) Platform: x86_64-w64-mingw32/x64 (64-bit) Running under: Windows 10 x64 (build 18362) Matrix products: default locale: [1] LC_COLLATE=English_United States.1252 LC_CTYPE=English_United States.1252 [3] LC_MONETARY=English_United States.1252 LC_NUMERIC=C [5] LC_TIME=English_United States.1252 attached base packages: stats, graphics, grDevices, utils, datasets, methods, base other attached packages: [1] ggplot2_3.2.1 dplyr_0.8.3 R6_2.4.1 rlang_0.4.2 lazyeval_0.2.2 rstudioapi_0.10 – MCP_infiltrator Jan 03 '20 at 14:21
  • doesn't work for me either working on R 3.6.1 and `lazyeval` 0.2.2. packages: `other attached packages: [1] ggplot2_3.2.0 loaded via a namespace (and not attached): [1] Rcpp_1.0.2 digest_0.6.20 withr_2.1.2 assertthat_0.2.1 crayon_1.3.4 dplyr_0.8.3 grid_3.6.1 R6_2.4.1 gtable_0.3.0 magrittr_1.5 scales_1.0.0 pillar_1.4.3 rlang_0.4.2 lazyeval_0.2.2 [15] rstudioapi_0.10 labeling_0.3 tools_3.6.1 glue_1.3.1 purrr_0.3.2 munsell_0.5.0 compiler_3.6.1 pkgconfig_2.0.3 colorspace_1.4-1 tidyselect_0.2.5 tibble_2.1.3 ` – user63230 Jan 03 '20 at 14:34
  • 1
    It's not an explanation, but it appears that calling `!!!` within `ggplot()` does count as a quoting environment, whereas calling it within `geom_point()` does not – Romain Jan 03 '20 at 15:12
  • If you're actually always using the dots for `size` and `color`, why not give them as explicit arguments ? In the form `function(data, x, y, color, size)`, so that you can unquote separately with `!!` – Romain Jan 03 '20 at 15:17
  • @Romain your observation seems to be right but that surprises me quite a bit because `aes()` is a quoting function. Therefore, it shouldn't matter whether it'd be called within `ggplot()` or `geom_*()`. Having `size` and `color` as explicit argument would definitely be a solution. However, this is a special use case. While allowing users to do this I don't want to clutter the argument list with these additional args. – Thomas Neitmann Jan 03 '20 at 15:21
  • 1
    @NelsonGon which OS are you running R on? – Thomas Neitmann Jan 03 '20 at 15:42
  • @Tommy Currently on Window$ 10. – NelsonGon Jan 03 '20 at 15:45
  • @Tommy windows 10 too `Platform: x86_64-w64-mingw32/x64 (64-bit) Running under: Windows 10 x64 (build 18362)` – user63230 Jan 03 '20 at 15:49
  • Installing the newest version of R and tidyverse didn't resolve the issue. – Thomas Neitmann Jan 03 '20 at 15:50
  • @Tommy in that case, for that particular lolliplot function you could override the `size` argument of `geom_segment` with a fixed value. I edited my answer accordingly – Romain Jan 03 '20 at 15:50

4 Answers4

10

Apparently this is a known issue of aes() as you can verify here. A workaround is this:

b <- function(data, x, y, ...) {
  x <- rlang::enquo(x)
  y <- rlang::enquo(y)

  dots <- rlang::enquos(...)
  segment_args <- dots[names(dots) != "size"]

  ggplot(data, aes(!!x, !!y)) +
    geom_segment(aes(, y = 0, xend = !!x, yend = !!y, !!!segment_args)) +
    geom_point(aes(, , !!!dots))
}

Notice the single comma in geom_segment() and the double comma in geom_point().

Thomas Neitmann
  • 2,552
  • 1
  • 16
  • 31
  • I think you can mark this as the accepted answer, since it answers the problem in the most generic way ! – Romain Jan 03 '20 at 16:49
4

If you follow the instructions of rlang, you get some further details:

> rlang::last_error()
<error>
message: Can't use `!!!` at top level.
class:   `rlang_error`
backtrace:
 1. global::b(d, cyl, n, color = factor(am), size = am)
 4. ggplot2::aes(y = 0, xend = !!x, yend = !!y, !!!segment_args)
 5. rlang::enquos(x = x, y = y, ..., .ignore_empty = "all")
 6. rlang:::endots(...)
 7. rlang:::map(...)
 8. base::lapply(.x, .f, ...)
 9. rlang:::FUN(X[[i]], ...)
Call `rlang::last_trace()` to see the full backtrace

Then

> rlang::last_trace()
    █
 1. └─global::b(d, cyl, n, color = factor(am), size = am)
 2.   ├─ggplot2::geom_segment(aes(y = 0, xend = !!x, yend = !!y, !!!segment_args))
 3.   │ └─ggplot2::layer(...)
 4.   └─ggplot2::aes(y = 0, xend = !!x, yend = !!y, !!!segment_args)
 5.     └─rlang::enquos(x = x, y = y, ..., .ignore_empty = "all")
 6.       └─rlang:::endots(...)
 7.         └─rlang:::map(...)
 8.           └─base::lapply(.x, .f, ...)
 9.             └─rlang:::FUN(X[[i]], ...)

So it appears the issue is with !!!segment_args

EDIT 1: just playing around but since segment_args is currently a single value, I tried the following and the error indeed disappears:

b <- function(data, x, y, ...) {
  x <- rlang::enquo(x)
  y <- rlang::enquo(y)

  dots <- rlang::enquos(...)
  print(dots)
  segment_args <- dots[[setdiff(names(dots), "size")]]
  print(names(dots))

  print(segment_args)

  ggplot(data, aes(!!x, !!y)) +
    geom_segment(aes(y = 0, xend = !!x, yend = !!y, !!segment_args)) +
    geom_point(aes(!!!dots))
}

This only confirms that the issue is with the usage of !!! since the above gives now an error for aes(!!!dots) instead and it depends on the fact that in the example there is only one element in segment_args, but it may give a handhold for further investigation

David Kun
  • 72
  • 3
2

I don't think you need to quote / unquote anymore. Instead, you can use the double bracket {{ x }} and leave the dots as dots ...

The following works and is much easier to understand:

b <- function(data, x, y, ...) {
  ggplot(data, aes( {{x}} , {{y}} )) +
    geom_segment(aes(y = 0, xend = {{x}}, yend = {{y}}, ...)) +
    geom_point(aes(...))
}
Ismail Müller
  • 395
  • 1
  • 7
  • Thank you for the suggestion Ismail. The reason I didn't leave the dots 'as is' is because I don't want to pass the `size` argument onto `geom_segment()`. What you propose does essentially what my function `a()` does but that's not what I want. – Thomas Neitmann Jan 03 '20 at 13:12
1

EDIT 2 :

You could override the size value for geom_segment, so that you don't have to manipulate the quoted dots before :

b <- function(data, x, y, ...) {
  x <- enquo(x)
  y <- enquo(y)
  dots <- enquos(...)

  ggplot(data, aes(!!x, !!y, !!!dots)) +
    geom_segment(aes(y = 0, xend = !!x, yend = !!y), size = 1) +
    geom_point(aes())
}

b(d, cyl, n)
b(d, cyl, n, color = factor(am))
b(d, cyl, n, color = factor(am), size = am)

EDIT : given my commentary about providing explicit argument, I tried this and it seems to work

b <- function(data, x, y, color, size) {
  x <- enquo(x)
  y <- enquo(y)
  color <- enquo(color)
  size <- enquo(size)

  ggplot(data, aes(!!x, !!y, color = !!color)) +
    geom_segment(aes(y = 0, xend = !!x, yend = !!y)) +
    geom_point(aes(size=!!size))
}

Given your example, I'd suggest the following workaround where the needed variables are created within the function rather than being passed from ..., so that you don't have to unquote within calls to geom_xxx.

library(dplyr)
library(rlang)
library(ggplot2)

data("mtcars")
d <- dplyr::count(mtcars, cyl, am)

b <- function(data, x, y, aspect) {
  x <- enquo(x)
  y <- enquo(y)
  aspect <- enquo(aspect)

  data <- data %>% mutate(
    color = factor(!!aspect),
    size = !!aspect
  )

  ggplot(data, aes(!!x, !!y, color = color)) +
    geom_segment(aes(y = 0, xend = !!x, yend = !!y)) +
    geom_point(aes(size=size))
}

b(d, cyl, n, am)
Romain
  • 1,931
  • 1
  • 13
  • 24