Propagate variable name created by combining symbol and string forward for use in ggplot aes()

This is a potential duplicate but I cannot get the solutions in the older threads such as this one to work for me. I cannot find an exact duplicate of this situation because none of the existing examples in the other threads refer to ggplot. For my own edification I would like to know how to get this to work without a workaround.

The situation is this. I am creating a function to which a variable name is passed. In the function, new columns are created by summarizing, with names like mean_{{variable}}. Then, the goal is to make a ggplot using the column mean_{{variable}}. My problem is that I can't get the concatenated variable name to propagate forward correctly using tidy evaluation. Of course, I can always use a workaround and not make the summary column names depend on the variable name, but I would like to know how to do this correctly.

Example code that doesn't work:

data(mtcars)

library(dplyr)
library(ggplot2)

summ_plot <- function(v) {
  # This part works correctly and creates a summary column called mean_mpg
  mtcars_summ <- mtcars %>%
    group_by(cyl) %>%
    summarize('mean_{{v}}' := mean({{v}}))
  
  # I know this does not work but I have no idea what to wrap mean_{{v}} in!
  mtcars_plot <- ggplot(mtcars_summ, aes(x = cyl, y = 'mean_{{v}}')) + geom_col()
  
  return(list(mtcars_summ, mtcars_plot))
}

summ_plot(mpg)

I have no idea which of the many mystifying variants of !!, sym, and quo to wrap mean_{{v}} in, inside the call to aes, to make aes interpret it as mean_mpg. Please help!!!

I'm not sure if you consider this a workaround, but since mtcars_summ within your function will always be a 2-column output, with the second column name being the desired "mean_{{v}}", you can use aes_str() to pass string values to the plot. In the example below, I pass the second value in names(mtcars_summ).

library(dplyr)
library(ggplot2)

summ_plot <- function(v) {
  mtcars_summ <- mtcars %>%
    group_by(cyl) %>%
    summarize('mean_{{v}}' := mean({{v}})) 
  
  mtcars_plot <- ggplot(mtcars_summ, 
                        aes_string(x = 'cyl', 
                                   y = names(mtcars_summ)[2]
                                   )
                        ) + 
    geom_col()
  
  return(list(mtcars_summ, mtcars_plot))
}

summ_plot(mpg)
#> [[1]]
#> # A tibble: 3 × 2
#>     cyl mean_mpg
#>   <dbl>    <dbl>
#> 1     4     26.7
#> 2     6     19.7
#> 3     8     15.1
#> 
#> [[2]]

Created on 2023-04-21 with reprex v2.0.2

1 Like

Thanks for the idea! Though I guess I would classify it as a "workaround." I am interested, in a pedagogical sense, if this is possible to do purely with tidy evaluation idioms. Especially since it seems like aes_string() has been deprecated (I don't agree with that decision and I hope it is retained indefinitely for backward compatibility). I will keep working at it!

Wow, this took an incredible amount of trial and error, but I was able to figure out a way to do this without the use of aes_string(). It may not be the canonical or recommended way so if anyone has a better solution please let me know.

My approach was to coerce the symbol that the user input to a string with as.character(ensym()), and use paste() to concatenate it with the prefix. Then in all subsequent function calls I can use !!sym() with the concatenated string.

Here it is in all its glory:

data(mtcars)

library(dplyr)
library(ggplot2)

summ_plot <- function(v) {
  
  mean_name <- paste0('mean_', as.character(ensym(v)))
  
  mtcars_summ <- mtcars %>%
    group_by(cyl) %>%
    summarize(!!sym(mean_name) := mean({{v}}))
  
  mtcars_plot <- ggplot(mtcars_summ, aes(x = cyl, y = !!sym(mean_name))) + geom_col()
  
  return(list(mtcars_summ, mtcars_plot))
}

summ_plot(qsec)
summ_plot(mpg)
# etc ...

It is a very esoteric use case but this may be helpful for others. Incidentally this uses some rlang functions but they are now imported by ggplot2 as well so you don't need to call library(rlang).

I think as_label is the function that tidyeval makes available for converting quosures to text strings. In your example, the code would be:

mean_name <- paste0('mean_', as_label(enquo(v)))

as_label is an rlang function, but it's reexported by ggplot2, so it's available without loading rlang. The help for ggplot2::as_label has additional details.

1 Like

That works too. Thanks!

But this is why the tidyeval stuff always confuses me. How is it that as.character(ensym(v)), as_label(enquo(v)), and as_label(enquo(v)) all give the same result? But as.character(enquo(v)) returns an error. I can't really understand that based on the documentation.

I too find tidyeval confusing. I wouldn't expect as.character to be generally useful here, because it's a base R function and not designed with non-standard evaluation in mind. To make your head spin a bit more, check out the help for rlang::as_label:

See also as_name() for transforming symbols back to a string. Unlike as_label(), as_name() is a well defined operation that guarantees the roundtrip symbol -> string -> symbol.

In general, if you don't know for sure what kind of object you're dealing with (a call, a symbol, an unquoted constant), use as_label() and make no assumption about the resulting string. If you know you have a symbol and need the name of the object it refers to, use as_name(). For instance, use as_label() with objects captured with enquo() and as_name() with symbols captured with ensym().

Remind me again why putting quotation marks in code is such a burden on users that people programming functions (often also users) have to go through this ordeal every time they want to make a simple function to programmatically create 5 plots?

2 Likes

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.