Search code examples
rggplot2rlang

How to use the rlang `!!!` operator to define a function that wraps around a ggplot call? (Error: Can't use `!!!` at top level)


Context

Reading the vignette Programming with dplyr I tried to use the ... and !!! operators to implement a function that would wrap around ggplot functions and would accept an arbitrary number of arguments that would define which variables in a dataframe were to be mapped to each aesthetic.

My goal

I wanted to define a function plot_points2() such that

  1. plot_points2(df, x = x, y = y, color = z) would be equivalent to df %>% ggplot( mapping = aes(x = x, y = y, color = z) ) + geom_point(alpha = 0.1)
  2. plot_points2(df, x = x, y = z, color = y) would be equivalent to df %>% ggplot( mapping = aes(x = x, y = z, color = y) ) + geom_point(alpha = 0.1)
  3. plot_points2(df, x = x, y = z) would be equivalent to df %>% ggplot( mapping = aes(x = x, y = z) ) + geom_point(alpha = 0.1)

What failed

packages

require(tidyverse)
require(rlang)

reduced example dataset

df <- tibble(g1= sample(x = c(1,2,3), replace = T, size = 10000),
             g2= sample(x = c("a","b","c"), replace = T, size = 10000),
             x = rnorm(10000, 50, 10),
             y = rnorm(10000, 0, 20) + x*2,
             z = rnorm(10000, 10, 5))
df

my attempt

plot_points2 <- function(d, ...){
  args <- quos(...)
  print(args)
  ggplot(data = d, mapping = aes(!!!args)) + geom_point(alpha = 0.1)
}
plot_points2(df, x = x, y = y, color = z)

the error

Error: Can't use `!!!` at top level
Call `rlang::last_error()` to see a backtrace 

Why I think it should work

I figure what I wanted to acomplish isn't much different from an example in the vignette that uses these operators to make a function that wraps around mutate(), and passes multiple arguments that defined the grouping variables (in deed I was able to implement a function that does that to the example dataset above I'm posting as an example), but somehow the latter works and the former doesn't:

this works

add_dif_to_group_mean <- function(df, ...) {
  groups <- quos(...)
  df %>% group_by(!!!groups) %>% mutate(x_dif = x-mean(x), 
                                        y_dif = y-mean(y), 
                                        z_dif = z-mean(z))
}

df %>% add_dif_to_group_mean(g1)
df %>% add_dif_to_group_mean(g1, g2)

this doesn't

plot_points2 <- function(d, ...){
  args <- quos(...)
  print(args)
  ggplot(data = d, mapping = aes(!!!args)) + geom_point(alpha = 0.1)
}
plot_points2(df, x = x, y = y, color = z)

I also read that the problem could be related with aes() being evaluated only when the plot is printed, but in that case I think using !! and unpacking manually should raise the same error but it doesn't:

plot_points2b <- function(d, ...){
  args <- quos(...)
  print(args)
  ggplot(data = d, mapping = aes(x = !!args[[1]], 
                                 y = !!args[[2]],
                                 color = !!args[[3]])) + 
    geom_point(alpha = 0.1)
}
plot_points2b(df, x = x, y = y, color = z)

In deed this last example works fine if you plot 3 variables, but it doesn't allow you to plot a number of variables different from 3

eg: plot_points2b(df, x = x, y = z) is not equivalent to

df %>% ggplot( mapping = aes(x = x, y = z) ) + geom_point(alpha = 0.1)

In stead it raises the error:

Error in args[[3]] : subscript out of bounds 

Anyone knows what concept am I missing here? Thank you in advance!


Solution

  • Your specific use case is an example in ?aes. aes automatically quotes its arguments. One can simply directly pass the dots. Try:

    plot_points3 <- function(d, ...){
      print(aes(...))
      ggplot(d, aes(...)) + geom_point(alpha = 0.1)
    }
    plot_points3(df, x = x, y = y, color = z)
    

    This nicely prints:

    Aesthetic mapping: 
    * `x`      -> `x`
    * `y`      -> `y`
    * `colour` -> `z`
    

    And yields the required plot.