rggplot2r-s3

How to use ggplot_add inside another package


I'm trying to build a package for data visualisation that relies heavily on ggplot2, but has some custom shortcuts for some of the day to day problems I face.

I am able to use ggplot_add function to extend the functionality of + for custom classes from scripts, however when I add these scripts to a package, ggplot_add no longer works.

Below I paste a minrep, to replicate first one needs to create a package (I'm using RStudio), that I've called SOExa. That project contains the following files:

.Rbuildignore

^.*\.Rproj$
^\.Rproj\.user$

DESCRIPTION

Package: SOExa
Type: Package
Title: An minrep for a problem I'm having
Version: 0.1.0
Author: Col Bates
Maintainer: The package maintainer <yourself@somewhere.net>
Description: I want to use ggplot2's ggplot_add from inside another package, i.e. this one.
    It seems that when I do I get an error.
License: GPLv2
Encoding: UTF-8
Imports:
    dplyr,
    magrittr,
    tidyr,
    glue,
    ggplot2
LazyData: true
RoxygenNote: 7.1.1

My project file SOExa.Rproj

A folder called R, containing the minrep example of use:

R/designed_by.R

#' the function to add a 'designed by' to the plot
#' as a designed_by class
#'@export
designed_by<-function(x){
  return(new_designed_by(x))
}


#' generic constructor.
#' @export
new_designed_by<-function(x){
  x <- list('designed_by' = x)
  class(x) <- 'designed_by'
  return(x)
}


#' generic print for designed_by
#' @export
print.designed_by <- function(x){
  print(paste('Designed by:', format(x)))

}


#' defines the addition of an designed_by object for
#' @export
ggplot_add.designed_by <- function(object, plot, objectname){
  plot$designed_by <-  object$designed_by
  plot
}

ggplot_add <- function(x){
  UseMethod("ggplot_add")
  }

I run the following code to build the Namespace file

devtools::document()

A new file is created:

NAMESPACE

# Generated by roxygen2: do not edit by hand

S3method(ggplot_add,designed_by)
S3method(print,designed_by)
export(designed_by)
export(new_designed_by)

After this I install and load the library:

devtools::install()
library(SOExa)

Then creating an empty plot:

p <- ggplot2::ggplot()

The following will give rise to an error:

p <- p + designed_by('Col Bates')

The error I get is:

# Error: Can't add `designed_by("Col Bates")` to a ggplot object.
# Run `rlang::last_error()` to see where the error occurred.

So following those steps:

rlang::last_error()

Which returns

# <error/rlang_error>
#   Can't add `designed_by("Col Bates")` to a ggplot object.
# Backtrace:
#  1. ggplot2:::`+.gg`(p, designed_by("Col Bates"))
#  2. ggplot2:::add_ggplot(e1, e2, e2name)
#  4. ggplot2:::ggplot_add.default(object, p, objectname)
# Run `rlang::last_trace()` to see the full context.

Running

rlang::last_trace()

I get

<error/rlang_error>
Can't add `designed_by("Col Bates")` to a ggplot object.
Backtrace:
    x
 1. \-ggplot2:::`+.gg`(p, designed_by("Col Bates"))
 2.   \-ggplot2:::add_ggplot(e1, e2, e2name)
 3.     +-ggplot2::ggplot_add(object, p, objectname)
 4.     \-ggplot2:::ggplot_add.default(object, p, objectname)

From this I can deduce that ggplot2::ggplot_add(), which calls UseMethod('ggplot_add') has invoked decided to apply the function ggplot_add.default, and hasn't recognised my class designed_by.

Incidentally, using the print() function does work from the library.

print(designed_by('Col Bates'))

However, if I were to source the script, rather than use the package like the following:

source('./R/designed_by.R')
p <- p + designed_by('Col Bates')

It does indeed work the way I would expect.

Looking deeper into things, I can see that the source of the generic ggplot_add on the class designed_by is my package.

 sloop::s3_methods_generic("ggplot_add")
## A tibble: 1 x 4
#  generic    class       visible source
#  <chr>      <chr>       <lgl>   <chr> 
# 1 ggplot_add designed_by TRUE    SOExa 

Whereas with ggplot classes it is 'registered S3method'

> sloop::s3_methods_generic("ggplot_add")
## A tibble: 14 x 4
#   generic    class      visible source             
#   <chr>      <chr>      <lgl>   <chr>              
# 1 ggplot_add by         FALSE   registered S3method
# 2 ggplot_add Coord      FALSE   registered S3method
# 3 ggplot_add data.frame FALSE   registered S3method
# 4 ggplot_add default    FALSE   registered S3method
# 5 ggplot_add Facet      FALSE   registered S3method
# ...

I looked inside the ggplot2 source code, but couldn't really figure out how this works. I've also been reading https://adv-r.hadley.nz/s3.html but haven't seen anything about using S3methods which apply to classes from another library.

It would be great to figure out if it is possible to package the calls into my custom package, or if I would always need to rely on sourcing.

Thanks.


Solution

  • Your setup should work if you add ggplot2 to Depends rather than Imports in the DESCRIPTION file. For example, if I create a new package with the following files:

    .Rbuildignore

    ^.*\.Rproj$
    ^\.Rproj\.user$
    

    DESCRIPTION

    Package: SOExa
    Type: Package
    Title: An minrep for a problem I'm having
    Version: 0.1.0
    Author: Col Bates
    Maintainer: The package maintainer <yourself@somewhere.net>
    Description: I want to use ggplot2's ggplot_add from inside another package, i.e. this one.
        It seems that when I do I get an error.
    License: GPLv2
    Encoding: UTF-8
    Depends: ggplot2
    Imports:
        dplyr,
        magrittr,
        tidyr,
        glue
    LazyData: true
    RoxygenNote: 7.1.1
    

    designed_by.R

    #' the function to add a 'designed by' to the plot
    #' as a designed_by class
    #'@export
    designed_by<-function(x){
      return(new_designed_by(x))
    }
    
    
    #' generic constructor.
    #' @export
    new_designed_by<-function(x){
      x <- list('designed_by' = x)
      class(x) <- 'designed_by'
      return(x)
    }
    
    
    #' generic print for designed_by
    #' @export
    print.designed_by <- function(x){
      print(paste('Designed by:', format(x)))
    
    }
    
    
    #' defines the addition of an designed_by object for
    #' @export
    ggplot_add.designed_by <- function(object, plot, objectname){
      plot$designed_by <-  object$designed_by
      plot
    }
    

    Then running devtools::document() gives me:

    NAMESPACE

    # Generated by roxygen2: do not edit by hand
    
    S3method(ggplot_add,designed_by)
    S3method(print,designed_by)
    export(designed_by)
    export(new_designed_by)
    

    So after I do devtools::install() I get the following output:

    library(SOExa)
    #> Loading required package: ggplot2
    
    p <- ggplot(data = NULL, aes(x = 1:10, y = 1:10)) + geom_point()
    
    p <- p + designed_by('Col Bates')
    
    p
    

    
    p$designed_by
    #> [1] "Col Bates"