Passing columns from data frame to ggplot2 in a function: why sym worked but ensym did not?

Happy Thursday everyone!

I have a simple tidy evaluation example in which columns from a data frame are passed to a ggplot function. Can anyone tell me why example #1 failed but #2 and #3 worked?

Thanks!

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

Sample data

set.seed(1)
df <- tibble(a = rnorm(30), b = rnorm(30), c = rnorm(30))

Example 1: Not ok (note what happened to purrr::map and ensym)

my_plot_ensym <- function(df, var_x, var_y) 
{
  var_x <- ensym(var_x)
  var_y <- ensym(var_y)
  
  cat('\nThis is what happens behind the scene: \n')
  qq_show(
    ggplot(df, aes(x = !!var_x, y = !!var_y)) + geom_point()
  )
  
  plot1 <- ggplot(df, aes(x = !!var_x, y = !!var_y)) + geom_point()
  return(plot1)
}

input_x <- names(df)[-1]
str(input_x)
#>  chr [1:2] "b" "c"
p1 <- input_x %>% map(., ~ my_plot_ensym(df, .x, "a"))
#> 
#> This is what happens behind the scene: 
#> ggplot(df, aes(x = .x, y = a)) + geom_point()
#> 
#> This is what happens behind the scene: 
#> ggplot(df, aes(x = .x, y = a)) + geom_point()
# p1 <- input_x %>% lapply(., function(x) {my_plot_ensym(df, .x, "a")})
p1[[1]]
#> Error in FUN(X[[i]], ...): object '.x' not found

Example 2: OK after adding is.character() test


my_plot_mod <- function(df, var_x, var_y) 
{
  if (is.character(var_x) && is.character(var_y)) {
    var_x <- ensym(var_x)
    var_y <- ensym(var_y)
  }
  
  cat('\nThis is what happens behind the scene: \n')
  qq_show(
    ggplot(df, aes(x = !!var_x, y = !!var_y)) + geom_point()
  )
  
  plot1 <- ggplot(df, aes(x = !!var_x, y = !!var_y)) + geom_point()
  return(plot1)
}

input_x <- names(df)[-1]
p2 <- input_x %>% map(., ~ my_plot_mod(df, .x, "a"))
#> 
#> This is what happens behind the scene: 
#> ggplot(df, aes(x = b, y = a)) + geom_point()
#> 
#> This is what happens behind the scene: 
#> ggplot(df, aes(x = c, y = a)) + geom_point()
p2[[2]]

Example 3: Also ok after replacing ensym with sym


my_plot_sym <- function(df, var_x, var_y) 
{
  var_x <- sym(var_x)
  var_y <- sym(var_y)
  
  cat('\nThis is what happens behind the scene: \n')
  qq_show(
    ggplot(df, aes(x = !!var_x, y = !!var_y)) + geom_point()
  )
  
  plot1 <- ggplot(df, aes(x = !!var_x, y = !!var_y)) + geom_point()
  return(plot1)
}

p3 <- input_x %>% map(., ~ my_plot_sym(df, .x, "a"))
#> 
#> This is what happens behind the scene: 
#> ggplot(df, aes(x = b, y = a)) + geom_point()
#> 
#> This is what happens behind the scene: 
#> ggplot(df, aes(x = c, y = a)) + geom_point()
p3[[1]]

Taken from Hadley's Advanced R book

19.3.2 Capturing symbols

Sometimes you only want to allow the user to specify a variable name, not an arbitrary expression. In this case, you can use ensym() or ensyms() . These are variants of enexpr() and enexprs() that check the captured expression is either symbol or a string (which is converted to a symbol67). ensym() and ensyms() throw an error if given anything else.

This answer on StackOverflow mentioned that

But it didn't work in this particular example

my_plot_ensym(df, names(df)[2], "a")
#> Error: Only strings can be converted to symbols
last_error()
#> <error>
#> message: Only strings can be converted to symbols
#> class:   `rlang_error`
#> backtrace:
#>  1. global::my_plot_ensym(df, names(df)[2], "a")
#>  2. rlang::ensym(var_x)
#> Call `rlang::last_trace()` to see the full backtrace

my_plot_ensym(df, b, a) ### works
#> 
#> This is what happens behind the scene: 
#> ggplot(df, aes(x = b, y = a)) + geom_point()

ensym will allow you to capture expression provided into the function argument (so by the user). sym will not, and will evaluate what is provided.

test_sym <- function(b) rlang::sym(b)
test_ensym <- function(b) rlang::ensym(b)
# error because a does not exist so can't be evaluated
test_sym(a)
#> Error in is_symbol(x): objet 'a' introuvable
# works because a is captured as expression by ensym
test_ensym(a)
#> a
# see the difference when we set a
a <- 'c'
# a is evaluated
test_sym(a)
#> c
# expression is captured as a symbol
test_ensym(a)
#> a

Created on 2019-03-18 by the reprex package (v0.2.1)

I think this is what happens here. When you use ensym, the expression is capture so .x. When you use sym, .x is evaluated to the string it contains, and it works.

If you unquote .x using !!, it works. Try by replacing with

p1 <- input_x %>% map(., ~ my_plot_ensym(df, !!.x, "a"))

In your last case, names(df)[2] is an expression but not a string. So ensym returns error because it is stricter that enexpr

this is my understanding. hope it helps.

4 Likes

Thank you @cderv! That makes sense. So the reason that Example 2 worked was is.character() evaluated the supplied expression for ensym() to pick up as a string?

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.

If you have a query related to it or one of the replies, start a new topic and refer back with a link.