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
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.
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)
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)
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)
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 argumentIt 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