How to replace the deprecated ggplot2 function aes_string: accepting an arbitrary number of named strings to specify aesthetic mappings?
Asked Answered
L

4

16

aes_string had some convenient behaviours that I made use of when programming with ggplot2. But aes_string has been deprecated (noticeably since ggplot2 version 3.4.0 I believe). I am struggling with how to nicely replace it.

Specifically, I previously created functions that accepted arbitrary string arguments through the ellipsis, and passed these to aes_string via do.call, as shown in the first reprex below.

Since noticing the deprecation warning I have tried to avoid aes_string, and found myself effectively just mimicking it in a rather "hacky" looking way. Presumably, whatever flaw in aes_string led to its deprecation, would also apply to my hacky workaround. See the second reprex.

Is there a more elegant solution? I want to continue passing the variable names as strings.

Reprex of my old approach with aes_string

library(ggplot2)

plotterOld <- function(...) {
  args <- list(...)
  pointAes <- do.call(aes_string, args = args)
  ggplot(mpg, aes(displ, cty)) +
    geom_point(mapping = pointAes)
}

plotterOld(colour = "cyl", size = "year")
#> Warning: `aes_string()` was deprecated in ggplot2 3.0.0.
#> ℹ Please use tidy evaluation ideoms with `aes()`

plot 1

# it can accept NULLs, and e.g. intuitively doesn't map size to anything
plotterOld(colour = "cyl", size = NULL)

plot 2

# no arguments also works fine
plotterOld()

plot 3

Created on 2022-11-11 with reprex v2.0.2


Reprex of my hacky attempt at replacing aes_string's behaviour?

library(ggplot2)

# arbitrary aesthetics passed as strings using ellipses, aes, quo and .data
myAesString <- function(...) {
  dots <- list(...)
  # early exits
  stopifnot(rlang::is_named2(dots))
  if (length(dots) == 0) {
    return(NULL)
  }
  
  # initialise empty mapping object and fill it with quosures where appropriate
  mapping <- aes()
  for (n in names(dots)) {
    v <- dots[[n]]
    if (!is.null(v)) {
      if (!rlang::is_string(v)) stop(n, " must be a string or NULL")
      mapping[[n]] <- quo(.data[[v]])
    }
  }
  return(mapping)
}

plotterNew <- function(...) {
  pointAes <- myAesString(...)
  ggplot(mpg, aes(displ, cty)) +
    geom_point(mapping = pointAes)
}

plotterNew(colour = "cyl", size = "year")

plot 4

plotterNew(colour = "cyl", size = NULL, shape = "drv")

plot 5

plotterNew()

plot 6


# seems to work fine
p <- plotterNew(colour = "cyl", size = "year")
p$layers[[1]]$mapping
#> Aesthetic mapping: 
#> * `colour` -> `.data[["cyl"]]`
#> * `size`   -> `.data[["year"]]`

Created on 2022-11-11 with reprex v2.0.2

Session info
sessioninfo::session_info()
#> ─ Session info ───────────────────────────────────────────────────────────────
#>  setting  value
#>  version  R version 4.2.1 (2022-06-23)
#>  os       macOS Big Sur ... 10.16
#>  system   x86_64, darwin17.0
#>  ui       X11
#>  language (EN)
#>  collate  en_GB.UTF-8
#>  ctype    en_GB.UTF-8
#>  tz       Europe/Amsterdam
#>  date     2022-11-11
#>  pandoc   2.18 @ /Applications/RStudio.app/Contents/MacOS/quarto/bin/tools/ (via rmarkdown)
#> 
#> ─ Packages ───────────────────────────────────────────────────────────────────
#>  package     * version date (UTC) lib source
#>  assertthat    0.2.1   2019-03-21 [1] CRAN (R 4.2.0)
#>  cli           3.4.1   2022-09-23 [1] CRAN (R 4.2.0)
#>  colorspace    2.0-3   2022-02-21 [1] CRAN (R 4.2.0)
#>  curl          4.3.3   2022-10-06 [1] CRAN (R 4.2.0)
#>  DBI           1.1.3   2022-06-18 [1] CRAN (R 4.2.0)
#>  digest        0.6.30  2022-10-18 [1] CRAN (R 4.2.1)
#>  dplyr         1.0.10  2022-09-01 [1] CRAN (R 4.2.0)
#>  evaluate      0.18    2022-11-07 [1] CRAN (R 4.2.0)
#>  fansi         1.0.3   2022-03-24 [1] CRAN (R 4.2.0)
#>  farver        2.1.1   2022-07-06 [1] CRAN (R 4.2.0)
#>  fastmap       1.1.0   2021-01-25 [1] RSPM (R 4.2.0)
#>  fs            1.5.2   2021-12-08 [1] RSPM (R 4.2.0)
#>  generics      0.1.3   2022-07-05 [1] CRAN (R 4.2.0)
#>  ggplot2     * 3.4.0   2022-11-04 [1] CRAN (R 4.2.1)
#>  glue          1.6.2   2022-02-24 [1] CRAN (R 4.2.0)
#>  gtable        0.3.1   2022-09-01 [1] CRAN (R 4.2.0)
#>  highr         0.9     2021-04-16 [1] RSPM (R 4.2.0)
#>  htmltools     0.5.3   2022-07-18 [1] CRAN (R 4.2.0)
#>  httr          1.4.4   2022-08-17 [1] CRAN (R 4.2.0)
#>  knitr         1.40    2022-08-24 [1] CRAN (R 4.2.0)
#>  labeling      0.4.2   2020-10-20 [1] CRAN (R 4.2.0)
#>  lifecycle     1.0.3   2022-10-07 [1] CRAN (R 4.2.0)
#>  magrittr      2.0.3   2022-03-30 [1] CRAN (R 4.2.0)
#>  mime          0.12    2021-09-28 [1] RSPM (R 4.2.0)
#>  munsell       0.5.0   2018-06-12 [1] CRAN (R 4.2.0)
#>  pillar        1.8.1   2022-08-19 [1] CRAN (R 4.2.0)
#>  pkgconfig     2.0.3   2019-09-22 [1] CRAN (R 4.2.0)
#>  purrr         0.3.5   2022-10-06 [1] CRAN (R 4.2.0)
#>  R.cache       0.16.0  2022-07-21 [1] CRAN (R 4.2.0)
#>  R.methodsS3   1.8.2   2022-06-13 [1] CRAN (R 4.2.0)
#>  R.oo          1.25.0  2022-06-12 [1] CRAN (R 4.2.0)
#>  R.utils       2.12.1  2022-10-30 [1] CRAN (R 4.2.0)
#>  R6            2.5.1   2021-08-19 [1] CRAN (R 4.2.0)
#>  reprex        2.0.2   2022-08-17 [1] CRAN (R 4.2.0)
#>  rlang         1.0.6   2022-09-24 [1] CRAN (R 4.2.0)
#>  rmarkdown     2.18    2022-11-09 [1] CRAN (R 4.2.1)
#>  rstudioapi    0.14    2022-08-22 [1] CRAN (R 4.2.0)
#>  scales        1.2.1   2022-08-20 [1] CRAN (R 4.2.0)
#>  sessioninfo   1.2.2   2021-12-06 [1] RSPM (R 4.2.0)
#>  stringi       1.7.8   2022-07-11 [1] CRAN (R 4.2.0)
#>  stringr       1.4.1   2022-08-20 [1] CRAN (R 4.2.0)
#>  styler        1.8.1   2022-11-07 [1] CRAN (R 4.2.0)
#>  tibble        3.1.8   2022-07-22 [1] CRAN (R 4.2.0)
#>  tidyselect    1.2.0   2022-10-10 [1] CRAN (R 4.2.0)
#>  utf8          1.2.2   2021-07-24 [1] CRAN (R 4.2.0)
#>  vctrs         0.5.0   2022-10-22 [1] CRAN (R 4.2.0)
#>  withr         2.5.0   2022-03-03 [1] CRAN (R 4.2.0)
#>  xfun          0.34    2022-10-18 [1] CRAN (R 4.2.0)
#>  xml2          1.3.3   2021-11-30 [1] RSPM (R 4.2.0)
#>  yaml          2.3.6   2022-10-18 [1] CRAN (R 4.2.1)
#> 
#>  [1] /Library/Frameworks/R.framework/Versions/4.2/Resources/library
#> 
#> ──────────────────────────────────────────────────────────────────────────────
```
Limburg answered 12/11, 2022 at 15:39 Comment(0)
G
6

One option would be to convert your list of quoted strings to symbols using sym:

library(ggplot2)

plotterOld <- function(...) {
  args <- lapply(list(...), function(x) if (!is.null(x)) sym(x))
  
  pointAes <- do.call(aes, args = args)
  ggplot(mpg, aes(displ, cty)) +
    geom_point(mapping = pointAes)
}

UPDATE And we could simplify even further by using !!! to get rid of do.call:

plotterOld <- function(...) {
  args <- lapply(list(...), function(x) if (!is.null(x)) sym(x))
  
  ggplot(mpg, aes(displ, cty)) +
    geom_point(mapping = aes(!!!args))
}
plotterOld(colour = "cyl", size = "year")


plotterOld(colour = "cyl", size = NULL)


plotterOld()

Greece answered 12/11, 2022 at 15:53 Comment(0)
F
3

You can use ensyms to convert named string arguments to named symbol arguments, so the equivalent to your old plotting function could be

library(ggplot2)

plotterNew <- function(...) {
  ggplot(mpg, aes(displ, cty)) +
    geom_point(mapping = aes(!!!ensyms(...)))
}

plotterNew(colour = "cyl", size = "year")

Created on 2022-11-12 with reprex v2.0.2

Fraction answered 12/11, 2022 at 16:2 Comment(4)
Nice. I knew there was an even simpler one using rlang. (:Greece
@Greece yes I think you reinvented ensyms with lapply(list(...), function(x) if (!is.null(x)) sym(x)). ggplot2 now exports some of these rlang functions too, so you don't need to load rlang to get ensyms.Fraction
Haha. Yep. Point is that I'm so used to using {{ and .data that I complete forgot about good old ensym, enquo, .... :DGreece
Thanks very much for your quick answers Stefan and Allan! sym() seems to be what I want, or probably data_sym() - I added an answer with some comparisonsLimburg
L
3

The answers of Stefan suggesting sym() and Allan Cameron suggesting ensyms() both really helped point me in the right direction for understanding this problem, so all credit to them.

This answer compares both approaches and adds a third approach with data_sym()

Two behaviours of aes_string, seen below, I was able to replicate using sym(), but not with ensyms()

  1. aes_string can accept NULL as an argument's value,
  2. aes_string can use strings already stored in objects
library(rlang)
library(ggplot2)

plotterAesString <- function(...) {
  pointAes <- do.call(aes_string, args = list(...))
  ggplot(mpg, aes(displ, cty)) +
    geom_point(mapping = pointAes)
}

colourObject <- "cyl" 
plotterAesString(colour = colourObject, size = NULL, shape = "drv") 
#> Warning: `aes_string()` was deprecated in ggplot2 3.0.0.
#> ℹ Please use tidy evaluation ideoms with `aes()`

Stefan's suggestion of sym() led me to this approach, which seems to be a pretty great replacement for the use of aes_string


plotterSym <- function(...) {
  args <- list(...)
  args <- lapply(args, function(x) if (is_string(x)) sym(x) else x)
  ggplot(mpg, aes(displ, cty)) + geom_point(mapping = aes(!!!args))
}

plotterSym(colour = colourObject, size = NULL, shape = "drv") 

I post this answer because the data_sym() function from rlang protects against the problem of accidentally matching an environmental variable instead of a data variable. (The original aes_string approach shares this behaviour!)


driv <- "misspelled variable?"
plotterSym(shape = "driv") 


plotterDataSym <- function(...) {
  args <- list(...)
  args <- lapply(args, function(x) if (is_string(x)) data_sym(x) else x)
  ggplot(mpg, aes(displ, cty)) + geom_point(mapping = aes(!!!args))
}

It works just like the sym approach

plotterDataSym(colour = colourObject, size = NULL, shape = "drv") 

But errors on misspelled names, which is desirable behaviour for me.


plotterDataSym(shape = "driv") 
#> Error in `geom_point()`:
#> ! Problem while computing aesthetics.
#> ℹ Error occurred in the 1st layer.
#> Caused by error in `.data$driv`:
#> ! Column `driv` not found in `.data`.

#> Backtrace:
#>      ▆
#>   1. ├─base::tryCatch(...)
#>   2. │ └─base (local) tryCatchList(expr, classes, parentenv, handlers)
#>   3. │   ├─base (local) tryCatchOne(...)
#>   4. │   │ └─base (local) doTryCatch(return(expr), name, parentenv, handler)
#>   5. │   └─base (local) tryCatchList(expr, names[-nh], parentenv, handlers[-nh])
#>   6. │     └─base (local) tryCatchOne(expr, names, parentenv, handlers[[1L]])
#>   7. │       └─base (local) doTryCatch(return(expr), name, parentenv, handler)
#>   8. ├─base::withCallingHandlers(...)
#>   9. ├─base::saveRDS(...)
#>  10. ├─base::do.call(...)
#>  11. ├─base (local) `<fn>`(...)
#>  12. ├─global `<fn>`(input = base::quote("pious-rhino_reprex.R"))
#>  13. │ └─rmarkdown::render(input, quiet = TRUE, envir = globalenv(), encoding = "UTF-8")
#>  14. │   └─knitr::knit(knit_input, knit_output, envir = envir, quiet = quiet)
#>  15. │     └─knitr:::process_file(text, output)
#>  16. │       ├─base::withCallingHandlers(...)
#>  17. │       ├─knitr:::process_group(group)
#>  18. │       └─knitr:::process_group.block(group)
#>  19. │         └─knitr:::call_block(x)
#>  20. │           └─knitr:::block_exec(params)
#>  21. │             └─knitr:::eng_r(options)
#>  22. │               ├─knitr:::in_input_dir(...)
#>  23. │               │ └─knitr:::in_dir(input_dir(), expr)
#>  24. │               └─knitr (local) evaluate(...)
#>  25. │                 └─evaluate::evaluate(...)
#>  26. │                   └─evaluate:::evaluate_call(...)
#>  27. │                     ├─evaluate (local) handle(...)
#>  28. │                     │ └─base::try(f, silent = TRUE)
#>  29. │                     │   └─base::tryCatch(...)
#>  30. │                     │     └─base (local) tryCatchList(expr, classes, parentenv, handlers)
#>  31. │                     │       └─base (local) tryCatchOne(expr, names, parentenv, handlers[[1L]])
#>  32. │                     │         └─base (local) doTryCatch(return(expr), name, parentenv, handler)
#>  33. │                     ├─base::withCallingHandlers(...)
#>  34. │                     ├─base::withVisible(value_fun(ev$value, ev$visible))
#>  35. │                     └─knitr (local) value_fun(ev$value, ev$visible)
#>  36. │                       └─knitr (local) fun(x, options = options)
#>  37. │                         ├─base::withVisible(knit_print(x, ...))
#>  38. │                         ├─knitr::knit_print(x, ...)
#>  39. │                         └─knitr:::knit_print.default(x, ...)
#>  40. │                           └─evaluate (local) normal_print(x)
#>  41. │                             ├─base::print(x)
#>  42. │                             └─ggplot2:::print.ggplot(x)
#>  43. │                               ├─ggplot2::ggplot_build(x)
#>  44. │                               └─ggplot2:::ggplot_build.ggplot(x)
#>  45. │                                 └─ggplot2:::by_layer(...)
#>  46. │                                   ├─rlang::try_fetch(...)
#>  47. │                                   │ ├─base::tryCatch(...)
#>  48. │                                   │ │ └─base (local) tryCatchList(expr, classes, parentenv, handlers)
#>  49. │                                   │ │   └─base (local) tryCatchOne(expr, names, parentenv, handlers[[1L]])
#>  50. │                                   │ │     └─base (local) doTryCatch(return(expr), name, parentenv, handler)
#>  51. │                                   │ └─base::withCallingHandlers(...)
#>  52. │                                   └─ggplot2 (local) f(l = layers[[i]], d = data[[i]])
#>  53. │                                     └─l$compute_aesthetics(d, plot)
#>  54. │                                       └─ggplot2 (local) compute_aesthetics(..., self = self)
#>  55. │                                         └─ggplot2:::scales_add_defaults(...)
#>  56. │                                           └─base::lapply(aesthetics[new_aesthetics], eval_tidy, data = data)
#>  57. │                                             └─rlang (local) FUN(X[[i]], ...)
#>  58. ├─driv
#>  59. ├─rlang:::`$.rlang_data_pronoun`(.data, driv)
#>  60. │ └─rlang:::data_pronoun_get(...)
#>  61. └─rlang:::abort_data_pronoun(x, call = y)
#>  62.   └─rlang::abort(msg, "rlang_error_data_pronoun_not_found", call = call)


A note on my tests of the ensyms approach suggested by Allan Cameron I found this approach can't accept objects containing strings, or NULL arguments and I couldn't find a way to work around these problems with ensyms


plotterEnsyms <- function(...) {
  ggplot(mpg, aes(displ, cty)) + geom_point(mapping = aes(!!!ensyms(...)))
}

# 1. uses name of object as symbol, instead of converting the string itself

plotterEnsyms(colour = colourObject)


# 2. can't accept NULLs, (and ensyms directly uses the args, so NULLs can't be removed first, I think...)
plotterEnsyms(size = NULL)
#> Error in `sym()`:
#> ! Can't convert NULL to a symbol.

#> Backtrace:
#>      ▆
#>   1. └─global plotterEnsyms(size = NULL)
#>   2.   ├─ggplot2::geom_point(mapping = aes(!!!ensyms(...)))
#>   3.   │ └─ggplot2::layer(...)
#>   4.   ├─ggplot2::aes(!!!ensyms(...))
#>   5.   │ └─ggplot2:::arg_enquos("x")
#>   6.   │   └─rlang::eval_bare(expr[[2]][[2]][[2]], env)
#>   7.   └─rlang::ensyms(...)
#>   8.     └─rlang:::map(...)
#>   9.       └─base::lapply(.x, .f, ...)
#>  10.         └─rlang (local) FUN(X[[i]], ...)
#>  11.           └─rlang::sym(expr)
#>  12.             └─rlang:::abort_coercion(x, "a symbol")
#>  13.               └─rlang::abort(msg, call = call)

Created on 2022-11-13 with reprex v2.0.2

Limburg answered 13/11, 2022 at 20:6 Comment(0)
C
0

The 'tinycodet' R package provides aes_pro(), which is equivalent to ggplot2::aes(), except it uses formula inputs instead of non-standard evaluation. So you can do the following:

library(ggplot2)

plotterOld <- function(...) {
  pointAes <-tinycodet::aes_pro(...)
  ggplot(mpg, aes(displ, cty)) +
    geom_point(mapping = pointAes)
}

plotterOld(colour = ~cyl, size = ~year)
Confederate answered 5/3, 2024 at 11:25 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.