Search code examples
rr-highchartertidyeval

Determine how to evaluate missing data variables properly in a function that uses '{{'


I'm attempting to build a function to produce box plots via highcharter. Once upon a time a function existed in highcharter called hcboxplot which has since been deprecated and which also uses deprecated tidy functions at this point.

Therefore, I'm now using highcharter::data_to_boxplot which creates a data.frame with list columns where each list includes boxplot.stats points. In an effort to simplify things for my team and I; I want to write a wrapper function for data_to_boxplot that builds out a basic boxplot.

Since data_to_boxplot uses data variables I need to embrace -> {{}} my arguments in order to evaluate them in the right context. Maybe there's another way? I'm very new to tidy evaluation as I mainly work with data.table and am having difficulty with handling optional parameters.

Example output:

library(data.table)
library(highcharter)
library(tidyr)

# dummy data
tmp = data.table(
  x1 = runif(30) * 10,
  grp = rep(c("a","b","c"),10),
  grp2 = rep(c("d","e"),15)
)

# transformed for hc
stat = data_to_boxplot(tmp, x1, grp, grp2)

# sample plot
highchart(type = "chart") %>%
  hc_add_series_list(stat)

Issue - when using a wrapper:

hc_boxplot = function(data, x, var = NULL, var2 = NULL, outliers = FALSE, ...) {
  arggs = list(...)

  d = data_to_boxplot(data = data, variable = {{x}}, group_var = {{var}}, group_var2 = {{var2}}, add_outliers = outliers, arggs)
  
  # basic chart
  highchart(type = "chart") %>%
    hc_add_series_list(d)
}

hc_boxplot(data = tmp, x = x1, var = grp, name = "test")
# Error in setnames(x, value) : - This refers to the excluded var2 arg
# Can't assign 1 names to a 0 column data.table

This works for the most part when all arguments are used but if I omit var2 or var & var2. Those args are still evaluated using {{}} even though to my understanding they should be missing. Below is a snippet from the data_to_boxplot code where the wrong portion of the if statement is evaluated. Since data_to_boxplot doesn't treat var2 as missing.

# where var2 == group_var2
# evaluates to TRUE instead of FALSE and proceeds to build an empty table
  if (!missing(group_var2)) { 
    dg2 <- data %>% select({
      {
        group_var2
      }
    })
  }
  else {
    dg2 <- data.frame(rep(NA, nrow(dx)))
  }

Since the arguments are not environment variables, I'm wondering how I can signal to data_to_boxplot in my wrapper that the omitted arguments are in fact missing.


Additionally, I'm having trouble passing in named arguments via (...) to data_to_boxplot. In my example above I use name = "test" which should name the series but am getting an undesired result. Where a new column is added named "arggs" with "test" in it instead of updated the column titled "name".

# A tibble: 2 × 5
  name  data       id    type    arggs       
  <chr> <list>     <chr> <chr>   <named list>
1 d     <list [3]> d     boxplot <chr [1]>   
2 e     <list [3]> e     boxplot <chr [1]> 

Appreciate any help on this as I'm sufficiently stuck.


Solution

  • This is a bug in highcharter. They are currently using this pattern in data_to_boxplot():

    if (!missing(group_var)) {
      dg <- data %>% select({{ group_var }})
    }
    

    But using missing() on arguments that might contain {{ doesn't work. What they should do instead:

    # Defuse `group_var` into a quosure so we can inspect it
    group_var <- enquo(group_var)
    
    # Inspect the defused quosure with `quo_is_missing()`
    if (!quo_is_missing(group_var)) {
      # Inject it back into the data-masking expression
      dg <- data %>% select(!!group_var)
    }
    

    Until this is fixed upstream, you can work around. This is not so trivial because missing values are involved:

    hc_boxplot = function(data, x, var, var2, outliers = FALSE, ...) {
      arggs = list(...)
    
      # Capture defused expressions so we can inspect them and remove
      # missing values
      defused_args = enquos(
        variable = x,
        group_var = var,
        group_var2 = var2
      )
    
      # Remove any missing arguments
      defused_args = Filter(\(x) !rlang::quo_is_missing(x), defused_args)
    
      # We'll inject the compacted list of defused expressions with `!!!`
      # back into `data_to_boxplot()`. This is equivalent to using
      # `do.call()` for a list of arguments.  In data-masking functions,
      # only `...` supports `!!!` automatically. To use it on named
      # arguments like `variable` and `group_var1`, we need to explicitly
      # enable `!!!` with `inject()`.
      d = inject(
        data_to_boxplot(
          data,
          !!!defused_args,
          add_outliers = outliers,
          !!!arggs
        )
      )
    
      # basic chart
      highchart(type = "chart") %>%
        hc_add_series_list(d)
    }