7

I would like to write a function that performs aesthetic mappings in ggplot. The function is supposed to have two arguments: var is supposed to be mapped to aesthetic. The first code block below actually works.

However, I would like to do the mapping not within the initial ggplot function but rather in the geom_point function. Here I receive the following error message:

Error: := can only be used within a quasiquoted argument

1. Block: works fine

library(ggplot2)
myfct <- function(aesthetic, var){
  aesthetic <- enquo(aesthetic)
  var <- enquo(var)
  ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, !! (aesthetic) := !!var)) + 
    geom_point()
}
myfct(size, Petal.Width)

2. Block: throws an error

library(ggplot2)
myfct <- function(aesthetic, var){
  aesthetic <- enquo(aesthetic)
  var <- enquo(var)
  ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width)) + 
    geom_point(aes(!! (aesthetic) := !!var))
}
myfct(size, Petal.Width)

The same behavior occurs also if the argument aesthetic is passed as string with sym.

# 1. block
myfct <- function(aesthetic, var){
  aesthetic <- sym(aesthetic)
  ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, !! aesthetic := {{var}})) + 
    geom_point()
}
myfct("size", Petal.Width)
# works

# 2. block 
myfct <- function(aesthetic, var){
  aesthetic <- sym(aesthetic)
  ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width)) +
    geom_point(aes(!! aesthetic := {{var}}))
}
myfct("size", Petal.Width)
# doesn't work
Till
  • 707
  • 3
  • 14

1 Answers1

4

Reasons

If we take a look at aes source code, we can find that the first and second places are reserved by x and y

> ggplot2::aes
function (x, y, ...) 
{
    exprs <- enquos(x = x, y = y, ..., .ignore_empty = "all")
    aes <- new_aes(exprs, env = parent.frame())
    rename_aes(aes)
}

Let's define a function to see how it will impact the aes():

testaes <- function(aesthetic, var){
    aesthetic <- enquo(aesthetic)
    var <- enquo(var)

    print("with x and y:")
    print(aes(Sepal.Length,Sepal.Width,!!(aesthetic) := !!var))
    print("without x and y:")
    print(aes(!!(aesthetic) := !!var))

}
> testaes(size, Petal.Width)
[1] "with x and y:"
Aesthetic mapping: 
* `x`    -> `Sepal.Length`
* `y`    -> `Sepal.Width`
* `size` -> `Petal.Width`
[1] "without x and y:"
Aesthetic mapping: 
* `x` -> ``:=`(size, Petal.Width)`

As you can see, when use := without x and y, the aesthetic and var are assigned to x instead.

To systematically fix this issue, more knowledge on NSE and source code of ggplot2 is needed.

workarounds

always assign values to first and second place

library(ggplot2)
myfct <- function(aesthetic, var){
  aesthetic <- enquo(aesthetic)
  var <- enquo(var)
  ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width)) + 
    geom_point(aes(x = Sepal.Length, y = Sepal.Width,!! (aesthetic) := !!var))
}
myfct(size, Petal.Width)

Or write a wrapper on aes

library(ggplot2)

myfct <- function(aesthetic, var){
    aesthetic <- enquo(aesthetic)
    var <- enquo(var)

    # wrapper on aes
    myaes <- function(aesthetic, var){
        aes(x = Sepal.Length, y = Sepal.Width,!! (aesthetic) := !!var)
    }

    ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width)) + 
        geom_point(mapping = myaes(aesthetic,var))
}
myfct(size, Petal.Width)

Or Modify Source Code

since x and y are the cause, we can modify the aes() source coding by removing x and y. since geom_*() inherits aes by having the default inherit.aes = TRUE, so you should be able to run it.

aes_custom <- function(...){
    exprs <- enquos(..., .ignore_empty = "all")
    aes <- ggplot2:::new_aes(exprs, env = parent.frame())
    ggplot2:::rename_aes(aes)
}

myfct <- function(aesthetic, var){
    aesthetic <- enquo(aesthetic)
    var <- enquo(var)
    ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width)) + 
        geom_point(aes_custom(!!(aesthetic) := !!var))
}
myfct(size, Petal.Width)

UPDATE

In short, argument order matters. When using NSE, we should always place !!x := !!y in position of unnamed arguments(e.g. ...) and never in position of named argument.

I was able to reproduce the problem outside of ggplot, so root cause is from NSE. It seems like := only works when in the position of unnamed argument(...). It doesn't get evaluated correctly when in the position of a named argument(x in the following example).

library(rlang)
# test funciton
testNSE <- function(x,...){
    exprs <- enquos(x = x, ...)
    print(exprs)
}

# test data
a = quo(size)
b = quo(Sepal.Width)

:= used in place of unnamed argument(...)

It works correctly

> testNSE(x,!!a := !!b)
<list_of<quosure>>

$x
<quosure>
expr: ^x
env:  global

$size
<quosure>
expr: ^Sepal.Width
env:  global

:= used in place of named argument

It doesn't work, since !!a := !!b is used on the first position of testNSE(), and the first position already have the name x. So it tries to assign size := Sepal.Width to x instead of assign Sepal.Width to size.

> testNSE(!!a := !!b)
<list_of<quosure>>

$x
<quosure>
expr: ^^size := ^Sepal.Width
env:  global
yusuzech
  • 5,896
  • 1
  • 18
  • 33
  • Interesting answer. What I do not understand is why in a non-function context `aes()` in `geom_point()` inherits the `x` and `y` argument from the `ggplot` call and why this is not happening in a function context. – TimTeaFan Oct 09 '19 at 17:39
  • Me neither, what I can understand is that it is part of how ggplot2 works. But without deeper understanding in NSE or ggplot2 source code, I can't tell the specific reasons. – yusuzech Oct 09 '19 at 17:41
  • Great answer, very helpful! – Till Oct 09 '19 at 19:10
  • The more I think about it, the less I think `x` and `y` are at the root of the problem. It seems to be more a NSE problem of `:=`. If we only use left hand-side NSE with `size = !! var` everything works fine: `testaes <- function(aesthetic, var){ aesthetic <- enquo(aesthetic) var <- enquo(var) print("with x and y:") print(aes(Sepal.Length,Sepal.Width,!!(aesthetic) := !!var)) print("without x and y and LHS NSE:") print(aes(!!(aesthetic) := !!var)) print("without x and y and only RHS NSE:") print(aes(size = !!var)) } ` – TimTeaFan Oct 09 '19 at 21:55
  • I updated my answer. `x` and `y` are the root of the problem when we try to use `:=`; It seems like `:=` doesn't work when occupying the position of named arguments. In your example, it works because it didn't use `:=` or evaluate the LHS. – yusuzech Oct 09 '19 at 22:10