Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What does 'Can't use `!!!` at top level.' mean and how to resolve it?

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
like image 612
Thomas Neitmann Avatar asked Jan 03 '20 12:01

Thomas Neitmann


4 Answers

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().

like image 88
Thomas Neitmann Avatar answered Nov 06 '22 16:11

Thomas Neitmann


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

like image 34
David Kun Avatar answered Nov 06 '22 15:11

David Kun


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(...))
}
like image 2
Ismail Müller Avatar answered Nov 06 '22 16:11

Ismail Müller


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)
like image 1
Romain Avatar answered Nov 06 '22 17:11

Romain