rggplot2methodsoperator-overloadingggproto

S3 Methods: Extending ggplot2 `+.gg` function


I am trying to extend ggplot2 with a new class that we will call foo for this example. The goal is to write a +.foo method that will be used in place of +.gg. However I am running into an issue of "incompatible methods"

The Setup

Currently I am able to write ggplot_add.foo_layer which will make plot into my foo class and then add the corresponding layer as normal.

The idea is that once the plot object inherits foo it will dispatch to +.foo when the next layer added.

The reason I would like to do this is because I want to check if the structure of foo object is still valid/compatible with the incoming layer. This will prevent me from having to write a method for ggplot_build.

Code Definitions

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")

The Error

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 "+" 

From the code above p1 + geom_* executes fine. However p2 + geom_* can not be made due to the above error about Incompatible methods. From what I know about S3 method dispatch I don't understand why this would not work. Could someone explain why this is or how I could remedy this.

Ideally I would not have to write a method ggplot_build.foo because I want other package's ggplot_build to be used if they exist (for example gganimate).


Solution

  • One thing you can do is to overwrite ggplot2:::+gg method to support double dispatch in S3. This isn't really good behaviour if you're writing a package, but it gets the job done. Note that this being naughty behaviour hasn't stopped other packages from overwriting ggplot's functions (looking at you, 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 +
    

    Created on 2021-01-20 by the reprex package (v0.3.0)