S3 方法:扩展 ggplot2 `+.gg` 函数

S3 Methods: Extending ggplot2 `+.gg` function

我正在尝试用一个新的 class 来扩展 ggplot2,在这个例子中我们将其称为 foo。目标是编写一个 +.foo 方法来代替 +.gg。但是我 运行 遇到了“不兼容方法”的问题

设置

目前我可以编写ggplot_add.foo_layer,这将使plot进入我的foo class,然后正常添加相应的层。

想法是,一旦 plot 对象继承 foo,它将在添加下一层时分派到 +.foo

我想这样做的原因是因为我想检查 foo 对象的结构是否仍然 valid/compatible 与传入层。这将使我不必为 ggplot_build.

编写方法

代码定义

library(ggplot2)

`+.foo` <- function(e1, e2){
  cat("Using foo ggplot +") # for Debugging
  NextMethod() #ideally just dispatches to `+.gg`
}

ggplot_add.foo_layer <- function(object, plot, object_name) {
  plot <- as_foo(plot)
  ggplot2:::add_ggplot(plot, object$layer, object_name) 
}

as_foo <- function(x){
  if(!is_foo(x)){
    class(x) <- c("foo", class(x))
  }
  x
}

is_foo <- function(x) inherits(x, "foo")

foo_layer <- function(x) structure(list(layer = x), class = "foo_layer")

错误

p1 <- ggplot(iris, aes(Sepal.Width, Sepal.Length, color = Species)) +
  geom_point()
class(p1)
#[1] "gg"     "ggplot"
p1 + geom_density(aes(y = after_stat(density)))


p2 <- ggplot(iris, aes(Sepal.Width, Sepal.Length, color = Species)) +
  foo_layer(geom_point()) 

class(p2)
#[1] "foo"    "gg"     "ggplot"
p2 + geom_density(aes(y = after_stat(density)))
#Error in p2 + geom_density(aes(y = after_stat(density))) : 
#  non-numeric argument to binary operator
#In addition: Warning message:
#Incompatible methods ("+.foo", "+.gg") for "+" 

上面的代码 p1 + geom_* 执行得很好。但是,由于上述有关不兼容方法的错误,无法进行p2 + geom_*。根据我对 S3 方法调度的了解,我不明白为什么这行不通。谁能解释这是为什么,或者我该如何解决这个问题。

理想情况下,我不必编写方法 ggplot_build.foo,因为我希望使用其他包的 ggplot_build(如果它们存在)(例如 gganimate)。

您可以做的一件事是覆盖 ggplot2:::+gg 方法以支持 S3 中的双重分派。如果您正在编写一个程序包,这并不是很好的行为,但它可以完成工作。请注意,这种顽皮的行为并没有阻止其他包覆盖 ggplot 的函数(看着你,ggtern)。

library(ggplot2)

`+.gg` <- function(e1, e2) {
  UseMethod("+.gg")
}

`+.gg.default` <- ggplot2:::`+.gg`

`+.gg.foo` <- function(e1, e2) {
  cat("Using foo ggplot +")
  NextMethod()
}

ggplot_add.foo_layer <- function(object, plot, object_name) {
  plot <- as_foo(plot)
  ggplot2:::add_ggplot(plot, object$layer, object_name) 
}

as_foo <- function(x){
  if(!is_foo(x)){
    class(x) <- c("foo", class(x))
  }
  x
}

is_foo <- function(x) inherits(x, "foo")

foo_layer <- function(x) structure(list(layer = x), class = "foo_layer")

p1 <- ggplot(iris, aes(Sepal.Width, Sepal.Length, color = Species)) +
  geom_point()

p2 <- ggplot(iris, aes(Sepal.Width, Sepal.Length, color = Species)) +
  foo_layer(geom_point()) 


p2 + geom_density(aes(y = after_stat(density)))
#> Using foo ggplot +

reprex package (v0.3.0)

于 2021 年 1 月 20 日创建

感谢 resource provided by @teunbrand,我们可以使用 S4 安全地调度 +

编辑 ----

似乎在会话之间这段代码实际上并没有在我定义的 S4 方法上调度 +。由于某种原因,它仍在调度到 +.gg。我会做一些关于为什么会这样的调度的研究,但暂时,下面的代码不起作用。

S4 class 定义

setOldClass(c("gg", "ggplot")) #required to make it inherit from gg
setClass("Foo", contains = c("gg","ggplot", "list"))
setMethod("initialize", "Foo",
          function(.Object, plot){
            .Object[names(plot)] <- plot
            .Object 
          } )


setMethod("+", signature(e1 = "Foo",e2 = "gg"),
          function(e1, e2){
            cat("Using S4 Method")
            gg <- ggplot2:::`+.gg`(e1, e2)
            as_foo(gg) #ensure that new layers (from other packages) dont return S3
          })
setMethod("show", signature("Foo"),
          function(object){
          ggplot2:::print.ggplot(object)})

ggplot_add.foo_layer <- function(object, plot, object_name) {
  plot <- as_foo(plot)
  ggplot2:::add_ggplot(plot, object$layer, object_name)
}

as_foo <- function(x){
  if(!is_foo(x)){
    x <- new("Foo", x)
  }
  x
}

is_foo <- function(x) inherits(x, "Foo")

foo_layer <- function(x) structure(list(layer = x), class = "foo_layer")

对于所有意图和目的,新的 foo 对象 'behaves' 就像一个标准的 ggplot 对象。

p2 <- ggplot(iris, aes(Sepal.Width, Sepal.Length, color = Species)) +
  foo_layer(geom_point())
p2 + geom_density(aes(y = after_stat(density)))

此外,其他包如 gganimate 仍将 return 一个 Foo 对象,但会调用它们自己的 ggplot_build S3 方法

library(gganimate)
anim <- p2 + transition_states(Species)
is_foo(anim)
inherits(anim, "gganim")
anim