如何在ggplot geoms中编写带有整齐求值的函数?

Til*_*ill 7 r function ggplot2 tidyeval

我想编写一个在ggplot中执行美学映射的函数。该函数应该有两个参数:var应该映射到aesthetic。实际上,下面的第一个代码块有效。

但是,我不想在初始ggplot函数中而是在geom_point函数中进行映射。在这里,我收到以下错误消息:

错误::=只能在准引数中使用

1.阻止:工作正常

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)
Run Code Online (Sandbox Code Playgroud)

2.阻止:抛出错误

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)
Run Code Online (Sandbox Code Playgroud)

如果自变量审美作为字符串传递时,也会发生相同的行为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
Run Code Online (Sandbox Code Playgroud)

yif*_*yan 4

原因

如果我们看一下aes源代码,我们可以发现第一和第二位被x和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)
}
Run Code Online (Sandbox Code Playgroud)

让我们定义一个函数来看看它将如何影响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))

}
Run Code Online (Sandbox Code Playgroud)
> 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)`
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,当:=不使用 x 和 y 时, 和aestheticvar被分配给 x 。

为了系统地解决这个问题,需要更多关于 NSE 和 ggplot2 源代码的知识。

解决方法

始终将值分配给第一和第二位

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)
Run Code Online (Sandbox Code Playgroud)

或者在 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)
Run Code Online (Sandbox Code Playgroud)

或者修改源码

由于 x 和 y 是原因,我们可以aes()通过删除 x 和 y 来修改源代码。由于geom_*()通过默认继承了 aes inherit.aes = TRUE,所以您应该能够运行它。

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)
Run Code Online (Sandbox Code Playgroud)

更新

简而言之,论证顺序很重要。使用 NSE 时,我们应该始终放置!!x := !!y在未命名参数的位置(例如...),而不是放置在命名参数的位置。

我能够在 ggplot 之外重现该问题,因此根本原因来自 NSE。似乎:=只有在未命名参数()的位置时才有效...x当位于命名参数的位置时(在下面的示例中),它不会被正确评估。

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

# test data
a = quo(size)
b = quo(Sepal.Width)
Run Code Online (Sandbox Code Playgroud)

:=用于代替未命名参数( ...)

它工作正常

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

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

$size
<quosure>
expr: ^Sepal.Width
env:  global
Run Code Online (Sandbox Code Playgroud)

:=用于代替命名参数

它不起作用,因为!!a := !!b用于 的第一个位置testNSE(),并且第一个位置已经具有名称x。所以它尝试分配给size := Sepal.Widthx不是分配Sepal.Widthsize

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

$x
<quosure>
expr: ^^size := ^Sepal.Width
env:  global
Run Code Online (Sandbox Code Playgroud)