Tidy eval and ggplot: Inconsistent results with facet_wrap

ggplot2
rlang

#1

I am trying to write a wrapper function using ggplot2 and rlang (tidy eval). Below is paired down example of what I am trying to do. The function will work as expected for some arguments to facet_wrap but not for others.

We see below that the facetting works correctly when argument equals NULL or when the facet variable is cut. When I try to use other variables in the same data set, then I get a mystery error that I cannot figure out.

I'm hoping that there is something simple that I am missing and someone can shed some light.

Thanks!

gg_histo <- function(data,
                     var,
                     facet = NULL,
                     theme_for_histo = theme_minimal(),
                     title = NULL,
                     subtitle = NULL) {

  #### quo some vars brah --------------------------------

  var_enq <- rlang::enquo(var)
  var_name <- rlang::quo_name(var_enq)
  facet_enq <- rlang::enquo(facet)


  #### Make the main plot --------------------------------
  
  res <- ggplot(data = data,
                aes(x = !! var_enq)) +
    geom_histogram(stat = "bin") +
    labs(title = var_name,
         x = "",
         subtitle = subtitle) +
    theme_for_histo
  
  
  #### Facet if applicable -------------------------------- 
  
  if (!is.null(facet)) {
    
    res <- res + 
      facet_wrap(vars(!! facet_enq), 
                 scales = "free")
  }
  
  
  #### Return the plot -------------------------------- 
  
  return(res)
  
}


#### Some examples -------------------------------- 

library(tidyverse)

dplyr::glimpse(diamonds)
#> Observations: 53,940
#> Variables: 10
#> $ carat   <dbl> 0.23, 0.21, 0.23, 0.29, 0.31, 0.24, 0.24, 0.26, 0.22, ...
#> $ cut     <ord> Ideal, Premium, Good, Premium, Good, Very Good, Very G...
#> $ color   <ord> E, E, E, I, J, J, I, H, E, H, J, J, F, J, E, E, I, J, ...
#> $ clarity <ord> SI2, SI1, VS1, VS2, SI2, VVS2, VVS1, SI1, VS2, VS1, SI...
#> $ depth   <dbl> 61.5, 59.8, 56.9, 62.4, 63.3, 62.8, 62.3, 61.9, 65.1, ...
#> $ table   <dbl> 55, 61, 65, 58, 58, 57, 57, 55, 61, 61, 55, 56, 61, 54...
#> $ price   <int> 326, 326, 327, 334, 335, 336, 336, 337, 337, 338, 339,...
#> $ x       <dbl> 3.95, 3.89, 4.05, 4.20, 4.34, 3.94, 3.95, 4.07, 3.87, ...
#> $ y       <dbl> 3.98, 3.84, 4.07, 4.23, 4.35, 3.96, 3.98, 4.11, 3.78, ...
#> $ z       <dbl> 2.43, 2.31, 2.31, 2.63, 2.75, 2.48, 2.47, 2.53, 2.49, ...

gg_histo(data = diamonds,
         var = price,
         facet = NULL)
#> `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.


gg_histo(data = diamonds,
         var = price,
         facet = cut)
#> `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.


gg_histo(data = diamonds,
         var = price,
         facet = color)
#> Error in gg_histo(data = diamonds, var = price, facet = color): object 'color' not found

gg_histo(data = diamonds,
         var = price,
         facet = clarity)
#> Error in gg_histo(data = diamonds, var = price, facet = clarity): object 'clarity' not found

Created on 2018-09-24 by the reprex package (v0.2.1)


#2

Okay, so I'm not great at tidyeval, and I could be leading you in entirely the wrong direction here (hopefully someone else can chime in). But one of the differences between aes() and vars() is that aes() expects x and y before the dots, whereas vars just expects the dots:

aes(x, y, …)
vars(…)

Since you're just passing x to aes, you don't really have to deal with the dots there at all, but it's possible that you do have to deal with them with vars(). I'm not sure how you'd fix this, though (if it is indeed a problem); I'm still a novice here :pensive:


#3

I've gotten this far: I think the error is being thrown by attempting to evaluate the if condition on

if (!is.null(facet)) {
    
    res <- res + 
      facet_wrap(vars(!! facet_enq), 
                 scales = "free")
  }

If you start up a browser() inside gg_histo() and try to run is.null(facet) at the console prompt, you'll see what I mean. You might also notice that if you have passed facet = color, then running res + facet_wrap(vars(!! facet_enq), scales = "free") at the console while browser()ing works just fine.

I can make gg_histo() work this way:

if (!rlang::quo_is_null(facet_enq)) {
    
    res <- res + 
      facet_wrap(vars(!! facet_enq), 
                 scales = "free")
  }

But I don't know if that's a decent approach or a terrible hack, and I'm still puzzled about why cut wasn't throwing the same error! :confused: I think I figured this one out: it's because cut() exists as a base function! And of course since nothing inside the if actually deals with facet, it doesn't matter if R has evaluated it as base::cut().

Here's a little reprex playing with the core problem:

not_null <- function(x = NULL) {
  !is.null(x)
}

not_null()
#> [1] FALSE
not_null(cut)
#> [1] TRUE
not_null(color)
#> Error in not_null(color): object 'color' not found

not_null <- function(x = NULL) {
  x <- rlang::enquo(x)
  !rlang::quo_is_null(x)
}

not_null()
#> [1] FALSE
not_null(cut)
#> [1] TRUE
not_null(color)
#> [1] TRUE

Created on 2018-09-25 by the reprex package (v0.2.1)

ETA: Thanks for posting this question, btw! I learn a lot puzzling through these things :smile:


#4

Honestly, I could just have a subforum full of tidyeval examples :laughing:


#5

Oh!! I totally forgot about using rlang::quo_is_null()!! Really nice work debugging this! I think that this is a completely decent approach and not hacky at all. The time that I used rlang::quo_is_null() was when I had a function with a group_by() that depended on if the grouping variable was NULL or not.

@jcblum I needed another set of eyes on this today to keep me from spinning my wheels; thank you!