diff --git a/content/blog/ggplot2-4-0-0-s7/figs/unnamed-chunk-11-1.png b/content/blog/ggplot2-4-0-0-s7/figs/unnamed-chunk-11-1.png new file mode 100644 index 000000000..77e7a6fe3 Binary files /dev/null and b/content/blog/ggplot2-4-0-0-s7/figs/unnamed-chunk-11-1.png differ diff --git a/content/blog/ggplot2-4-0-0-s7/figs/unnamed-chunk-12-1.png b/content/blog/ggplot2-4-0-0-s7/figs/unnamed-chunk-12-1.png new file mode 100644 index 000000000..5ee242935 Binary files /dev/null and b/content/blog/ggplot2-4-0-0-s7/figs/unnamed-chunk-12-1.png differ diff --git a/content/blog/ggplot2-4-0-0-s7/figs/unnamed-chunk-3-1.png b/content/blog/ggplot2-4-0-0-s7/figs/unnamed-chunk-3-1.png new file mode 100644 index 000000000..1d411bc35 Binary files /dev/null and b/content/blog/ggplot2-4-0-0-s7/figs/unnamed-chunk-3-1.png differ diff --git a/content/blog/ggplot2-4-0-0-s7/figs/unnamed-chunk-4-1.png b/content/blog/ggplot2-4-0-0-s7/figs/unnamed-chunk-4-1.png new file mode 100644 index 000000000..1d411bc35 Binary files /dev/null and b/content/blog/ggplot2-4-0-0-s7/figs/unnamed-chunk-4-1.png differ diff --git a/content/blog/ggplot2-4-0-0-s7/index.Rmd b/content/blog/ggplot2-4-0-0-s7/index.Rmd new file mode 100644 index 000000000..55ed9bbe1 --- /dev/null +++ b/content/blog/ggplot2-4-0-0-s7/index.Rmd @@ -0,0 +1,339 @@ +--- +output: hugodown::hugo_document + +slug: ggplot2-4-0-0-s7 +title: ggplot2 migrates to S7 +date: 2025-05-26 +author: Teun van den Brand +description: > + The ggplot2 package is migrating to S7 and we'd like to minimise any problems. + This guide details the classes and functions that ggplot2 has migrated and + how these might affect downstream packages. + +photo: + url: https://unsplash.com/photos/silhouette-of-person-standing-on-grass-field-during-sunset-Fr33DHTpLZk + author: Inhyeok Park + +# one of: "deep-dive", "learn", "package", "programming", "roundup", or "other" +categories: [package] +tags: [ggplot2, s7, package maintenance] +--- + + + +We are on the verge of releasing version 4.0.0 of the ggplot2 package. +That is right: a new major version release! +We only tend to do these when something fundamental changes in ggplot2. +For example: ggplot2 2.0.0 brought the ggproto extension system and 3.0.0 switched to tidy evaluation. +This time around, we're swapping out the S3 object oriented programming system for the newer S7 system. +Because of this major change, we expect that some packages might break, despite our best efforts to minimise the implications of the switch. +This here is a guide for package authors that might be affected by the changes. +It details some changes in classes and functions that may affect downstream packages, and gives recommendations how broken parts might be repaired. +If you don't maintain a package that depends on ggplot2, you can skip reading this guide and simply take away that there will be a release soon. + +## Testing compatibility + +If you are a package author that depends on ggplot2 and you want to know how your package might be affected, you can try the current development version from GitHub using the code below. + +```r +pak::pak("tidyverse/ggplot2") +``` + +It should also automatically install scales 1.4.0, which is needed for this release. +One of the things to inspect first is the result of R CMD check on your package, with the development version of ggplot2 installed. +It can be invoked by `devtools::check()`. +This is also the check CRAN runs on your package to keep tabs on whether your package continues to work. +If you are lucky, it will happily report that there are no problems and you can stop reading this guide! +If you are unlucky, it will list errors and warnings associated with running your package. +It might be that your examples no longer work, test assumptions are no longer met or vignettes run amock. +If you use visual snapshots from the vdiffr package, you may certainly expect (mostly harmless) imperceptible changes. + +As you're still reading, I'm assuming there are problems to solve. +The next step is determining who should fix these problems. +We have tried to facilitate some backwards compatibility, but we also cannot anticipate every contingency. +If something is broken with classes, generics, methods or object oriented programming in general, this guide describes problems and remedies. +Because ggplot2 does not go back to S3, we hope that you will facilitate the migration to S7 in your code where appropriate. +If there are other issues that pop up that you think might be best repaired in ggplot2, you can post an issue in the [issue tracker](https://github.com/tidyverse/ggplot2/issues). + +That said, let's go through S7 a bit. [S7](https://rconsortium.github.io/S7/) is a newer object oriented programming system that is built on top of the older S3 system. +It was build by a collaboration of developers from different parts in the R community, ranging from R Core, to Bioconductor to the tidyverse. +It aims to succeed the simpler S3 and more complex S4 systems. +Aside from simply modernising ggplot2, the migration to S7 also enables features that are hard to implement in S3, such as double dispatch. +For years now, people have been asking for more control over how plots are declared at both sides of the `+` operator, which S7 will facilitate. + +## Classes + +The ggplot2 package uses a mixture of object oriented programming systems. +One of these systems is the ggproto system that powers the extension mechanism and remains unchanged. +The other system is S3 which has been supplanted by S7 in the recent ggplot2 update. +You might notice this from the new S7 class objects that ggplot2 defines, like `class_ggplot` or `class_theme`. + +```{r} +library(ggplot2) +class_ggplot +``` + +### Properties + +In prior incarnations, ggplot2 defined the ggplot class as a named list with the `"ggplot"` class attribute. +Classes in S7 are more formal than in S3 and have properties which can have restricted classes. +For example, in the ggplot class, the `data` property can be anything (because it will go through `fortify()` to become a data frame), the `facet` property must be the `Facet` ggproto class, and the `theme` property must be an S7 theme object. + +In contrast to S3, we cannot simply add new items to `ggplot` object ^[This still 'works' for backwards compatibility reasons, but it will be phased out in the future, so it should be avoided.]. +The way to add additional information to classes in S7 is to make a subclass with additional properties. +For example, if we want to add colour information to a new plot, we can do the following: + +```{r} +inked_ggplot <- S7::new_class( + name = "inked_ggplot", + parent = class_ggplot, + properties = list(ink = S7::class_character) +) + +inked_ggplot +``` + +When you define a new class, the object you've assigned it to automatically becomes the class definition which comes with a free, standard constructor. +This means that we can start building new plots with our subclass right away. +Note that we haven't implemented any behaviour around the `ink` property (yet), so it will just print like a normal plot. + +```{r} +my_plot <- inked_ggplot(data = mpg, ink = "red") + + geom_point(aes(displ, hwy)) +my_plot +``` + +In contrast to S3, where you would change list-items by using `$`, in S7 you can use `@` to read and write properties. +So if we want to change the stored `ink` colour, we can use: + +```{r} +my_plot@ink <- "blue" +``` + +### Testing + +In S3, the recommended way to test for the class of an object is to use a testing function. +An example is `is.factor()` but it may be that such a testing function doesn't exist. In that case you can use `inherits()`. +In S7, it is still recommended to use dedicating testing functions. +However, if these are absent, you can use `S7::S7_inherits()`. +If we wanted to write a testing function for our new class, we can do that as follows: + +```{r} +is_inked_ggplot <- function(x) S7::S7_inherits(x, inked_ggplot) + +# Is not our class +is_inked_ggplot(ggplot()) + +# Is our class +is_inked_ggplot(my_plot) +``` + +### Overview + +To give an overview of ggplot2's S7 classes, we include the table below. +The table also lists the recommended way to test for the class. + +```{r, echo=FALSE} +cls <- tibble::tribble( + ~`Old S3 Class`, ~`New S7 Class`, ~`Testing functions`, + '"ggplot2"', "class_ggplot", "`is_ggplot(x)`", + '"ggplot_built"', "class_ggplot_built", "`S7::S7_inherits(x, class_ggplot_built)`", + '"labs"', "class_labels", "`S7::S7_inherits(x, class_labels)`", + '"uneval"', "class_mapping", "`is_mapping(x)`", + '"theme"', "class_theme", "`is_theme(x)`", + '"element_blank"', "element_blank", '`is_theme_element(x, "blank")`', + '"element_line"', "element_line", '`is_theme_element(x, "line")`', + '"element_rect"', "element_rect", '`is_theme_element(x, "rect")`', + '"element_text"', "element_text", '`is_theme_element(x, "text")`', + NA, "element_polygon", '`is_theme_element(x, "polygon")`', + NA, "element_point", '`is_theme_element(x, "point")`', + NA, "element_geom", '`is_theme_element(x, "geom")`' +) +knitr::kable(cls) +``` + +### Testing + +It should be noted that the `is_*()` testing functions in ggplot2 already know about the S7-ness of the new classes. +This is handy when it comes to test expectations, because the testing function can be used instead of the S3/S7 class expectations. +Previously, you might have used `testthat::expect_s3_class()`, but it is better now to test with `testthat::expect_s7_class()` or use an `is_*()` function instead. + +```{r} +testthat::test_that( + "the plot object has the ggplot class", + { + plot <- ggplot() + + # Works regardless of S3 or S7 + testthat::expect_true(is_ggplot(plot)) + + # This will become dysfunctional in the future. + # Do not use this! + testthat::expect_s3_class(plot, "ggplot") + + # This will work in the new version + testthat::expect_s7_class(plot, class_ggplot) + } +) +``` + +The ggplot2 package manually appends the `"ggplot"` class for backwards compatibility reasons (likewise for `"theme"`). +However, once this phases out, the `testthat::expect_s3_class()` expectation will become untenable. +It is also currently flawed, as it does not work for subclasses! + +```{r, error=TRUE} +testthat::test_that( + "the inked plot has the ggplot class", + { + plot <- inked_ggplot() + testthat::expect_s3_class(plot, "ggplot") + } +) +``` + +The advice herein is thus to use `is_ggplot()`. + +## Generics and methods + +If you are new to object oriented programming in R, you might be unfamiliar with what the terms 'generic' and 'methods' mean. +They are a form of 'polymorphism', that allow us to use a single function, called the 'generic' function, with different implementations for different classes (where one such implementation is called a 'method'). +A well known generic is `print()`, which does different things for different classes. +For example `print(1:10)` prints the numeric vector to the console, but `print(my_plot)` opens a graphics device and renders the plot. + +### Your methods for ggplot's generics + +The ggplot2 package also declares some generic functions and contains methods for these, most of which revolve around plot construction. +The migration to S7 means that the generics and methods defined by ggplot2 also migrate. + +It is also good to mention that when your package registers a method for one of ggplot2's generics, ggplot2's generic is called an 'external generic' from the point of view of your package. With S7, you should include `S7::methods_register()` in your package's `.onLoad()` call. + +While it is possible to define S7 methods for S3 generics, it is not possible to define S3 methods for S7 generics. + +```{r, error=TRUE} +# Declare an S7 generic +apply_ink <- S7::new_generic("apply_ink", "plot") + +# Attempt to implement an S3 method +apply_ink.inked_ggplot <- function(plot, ...) { + # Edit plot to our liking + plot@theme <- theme_gray(ink = plot@ink) + plot@theme + plot +} + +# Burn your fingers +apply_ink(my_plot) +``` + +To allow for a smoother transition from S3 to S7, we plan to keep S3 generics around for another release cycle but will permanently disable them in the future in favour of the S7 generics. +Here is an overview of which S7 generics supplant which S3 generics: + +```{r, echo=FALSE} +generics <- tibble::tribble( + ~`Old S3 Generic`, ~`New S7 Generic`, ~`Description`, + "`ggplot_add()`", "`update_ggplot()`", "Determines what happens when you `+` an object to a plot.", + "`ggplot_build()`", "`build_ggplot()`", "Processes data for display in a plot.", + "`ggplot_gtable()`", "`gtable_ggplot()`", "Renders a processed plot to a gtable object.", + "`element_grob()`", "`draw_element()`", "Renders a theme element." +) +knitr::kable(generics) +``` + +If your package implements methods for one of the old S3 generics, we recommend to replace these with S7 in a timely manner. +An important difference between S3 and S7 is that S7 does not use `NextMethod()` to magically invoke parental methods on children. +Instead, you can use `S7::super()` to explicitly convert the subclass to a parent before invoking the generic again. + +```{r} +S7::method(build_ggplot, inked_ggplot) <- function(plot, ...) { + # Edit plot to our liking + plot@theme <- theme_gray(ink = plot@ink) + plot@theme + + # Invoke next method + build_ggplot(S7::super(plot, to = class_ggplot), ...) +} + +my_plot +``` + +Just to show that the new property in our subclass works as expected: + +```{r} +my_plot@ink <- "red" +my_plot +``` + +### Your generics with methods for ggplot2's classes + +Alternatively, it might be that you package has generic functions and methods that handle some of ggplot2's classes. +The S7 system has its own way of handling class names, which means that S3 function name patterns of the form `{generic_name}.{class_name}` no longer invoke the correct method for S7 classes. + +```{r, error=TRUE} +# Declare S3 generic +foo <- function(x, ...) { + UseMethod("foo") +} + +# Implement S3 method +foo.labels <- function(x, ...) { + x[] <- lapply(x, toupper) + x +} + +# Burn your fingers +foo(labs(colour = "my lowercase title")) +``` + +Please note that the `ggplot()` and `theme()` still produces objects with the `"ggplot"` and `"theme"` class for backwards compatibility, but this is scheduled to be removed in the future. +The best remedy the dilemma with S3 would be to use `S7::method()`, which also works for S3 generics. + +```{r} +# Note that `foo()` is still an S3 generic +S7::method(foo, class_labels) <- function(x, ...) { + x[] <- lapply(x, toupper) + x +} + +# Note text has updated +foo(labs(colour = "my lowercase title")) +``` + +If that is not an option, because you may not want to depend on S7, you can *currently* use a little hack. +The hack is to prepend the S7 class prefix in the class name of the S3 method. +This prefix is the name of the package that defines the class, followed by `::`. + +```{r} +`foo.ggplot2::labels` <- function(x, ...) { + x[] <- lapply(x, toupper) + x +} +``` + +## Checklist + +Because all of the above might be hard to parse in its entirety, here is a dainty checklist of common migration issues. + + Do I wrap a gglot class that should become an S7 class with extra properties? + + Are there cases where `inherits()` is used which should be replaced with test functions or `S7::S7_inherits()`? + + Do I edit objects with `$`, `[` or `[[` that were previously lists but are now properties to edit with `@`? + + Are there tests that assume S3 classes, that should use `testthat::expect_s7_class()` instead? + + Do I implement methods for one of the S3 generics that should become S7 methods? + + Do I have a generic that may need to facilitate methods for ggplot2's new S7 classes? + + If I assume ggplot2's S7 classes in my code, do I need to bump the required ggplot2 version in the DESCRIPTION file? + +Thank you for reading, we hope that most of it was not necessary! diff --git a/content/blog/ggplot2-4-0-0-s7/index.md b/content/blog/ggplot2-4-0-0-s7/index.md new file mode 100644 index 000000000..005406269 --- /dev/null +++ b/content/blog/ggplot2-4-0-0-s7/index.md @@ -0,0 +1,376 @@ +--- +output: hugodown::hugo_document + +slug: ggplot2-4-0-0-s7 +title: ggplot2 migrates to S7 +date: 2025-05-26 +author: Teun van den Brand +description: > + The ggplot2 package is migrating to S7 and we'd like to minimise any problems. + This guide details the classes and functions that ggplot2 has migrated and + how these might affect downstream packages. + +photo: + url: https://unsplash.com/photos/silhouette-of-person-standing-on-grass-field-during-sunset-Fr33DHTpLZk + author: Inhyeok Park + +# one of: "deep-dive", "learn", "package", "programming", "roundup", or "other" +categories: [package] +tags: [ggplot2, s7, package maintenance] +rmd_hash: 9d742fcb8a1f110f + +--- + + + +We are on the verge of releasing version 4.0.0 of the ggplot2 package. That is right: a new major version release! We only tend to do these when something fundamental changes in ggplot2. For example: ggplot2 2.0.0 brought the ggproto extension system and 3.0.0 switched to tidy evaluation. This time around, we're swapping out the S3 object oriented programming system for the newer S7 system. Because of this major change, we expect that some packages might break, despite our best efforts to minimise the implications of the switch. This here is a guide for package authors that might be affected by the changes. It details some changes in classes and functions that may affect downstream packages, and gives recommendations how broken parts might be repaired. If you don't maintain a package that depends on ggplot2, you can skip reading this guide and simply take away that there will be a release soon. + +## Testing compatibility + +If you are a package author that depends on ggplot2 and you want to know how your package might be affected, you can try the current development version from GitHub using the code below. + +``` r +pak::pak("tidyverse/ggplot2") +``` + +It should also automatically install scales 1.4.0, which is needed for this release. One of the things to inspect first is the result of R CMD check on your package, with the development version of ggplot2 installed. It can be invoked by [`devtools::check()`](https://devtools.r-lib.org/reference/check.html). This is also the check CRAN runs on your package to keep tabs on whether your package continues to work. If you are lucky, it will happily report that there are no problems and you can stop reading this guide! If you are unlucky, it will list errors and warnings associated with running your package. It might be that your examples no longer work, test assumptions are no longer met or vignettes run amock. If you use visual snapshots from the vdiffr package, you may certainly expect (mostly harmless) imperceptible changes. + +As you're still reading, I'm assuming there are problems to solve. The next step is determining who should fix these problems. We have tried to facilitate some backwards compatibility, but we also cannot anticipate every contingency. If something is broken with classes, generics, methods or object oriented programming in general, this guide describes problems and remedies. Because ggplot2 does not go back to S3, we hope that you will facilitate the migration to S7 in your code where appropriate. If there are other issues that pop up that you think might be best repaired in ggplot2, you can post an issue in the [issue tracker](https://github.com/tidyverse/ggplot2/issues). + +That said, let's go through S7 a bit. [S7](https://rconsortium.github.io/S7/) is a newer object oriented programming system that is built on top of the older S3 system. It was build by a collaboration of developers from different parts in the R community, ranging from R Core, to Bioconductor to the tidyverse. It aims to succeed the simpler S3 and more complex S4 systems. Aside from simply modernising ggplot2, the migration to S7 also enables features that are hard to implement in S3, such as double dispatch. For years now, people have been asking for more control over how plots are declared at both sides of the `+` operator, which S7 will facilitate. + +## Classes + +The ggplot2 package uses a mixture of object oriented programming systems. One of these systems is the ggproto system that powers the extension mechanism and remains unchanged. The other system is S3 which has been supplanted by S7 in the recent ggplot2 update. You might notice this from the new S7 class objects that ggplot2 defines, like `class_ggplot` or `class_theme`. + +
+ +
library(ggplot2)
+class_ggplot
+#> <ggplot2::ggplot> class
+#> @ parent     : <ggplot2::gg>
+#> @ constructor: function(data, layers, scales, guides, mapping, theme, coordinates, facet, layout, labels, meta, plot_env) {...}
+#> @ validator  : <NULL>
+#> @ properties :
+#>  $ data       : <ANY>             
+#>  $ layers     : <list>            
+#>  $ scales     : S3<ScalesList>    
+#>  $ guides     : S3<Guides>        
+#>  $ mapping    : <ggplot2::mapping>
+#>  $ theme      : <ggplot2::theme>  
+#>  $ coordinates: S3<Coord>         
+#>  $ facet      : S3<Facet>         
+#>  $ layout     : S3<Layout>        
+#>  $ labels     : <ggplot2::labels> 
+#>  $ meta       : <list>            
+#>  $ plot_env   : <environment>
+
+ +
+ +### Properties + +In prior incarnations, ggplot2 defined the ggplot class as a named list with the `"ggplot"` class attribute. Classes in S7 are more formal than in S3 and have properties which can have restricted classes. For example, in the ggplot class, the `data` property can be anything (because it will go through [`fortify()`](https://ggplot2.tidyverse.org/reference/fortify.html) to become a data frame), the `facet` property must be the `Facet` ggproto class, and the `theme` property must be an S7 theme object. + +In contrast to S3, we cannot simply add new items to `ggplot` object [^1]. The way to add additional information to classes in S7 is to make a subclass with additional properties. For example, if we want to add colour information to a new plot, we can do the following: + +
+ +
inked_ggplot <- S7::new_class(
+  name = "inked_ggplot",
+  parent = class_ggplot, 
+  properties = list(ink = S7::class_character)
+)
+
+inked_ggplot
+#> <inked_ggplot> class
+#> @ parent     : <ggplot2::ggplot>
+#> @ constructor: function(data, layers, scales, guides, mapping, theme, coordinates, facet, layout, labels, meta, plot_env, ink) {...}
+#> @ validator  : <NULL>
+#> @ properties :
+#>  $ data       : <ANY>             
+#>  $ layers     : <list>            
+#>  $ scales     : S3<ScalesList>    
+#>  $ guides     : S3<Guides>        
+#>  $ mapping    : <ggplot2::mapping>
+#>  $ theme      : <ggplot2::theme>  
+#>  $ coordinates: S3<Coord>         
+#>  $ facet      : S3<Facet>         
+#>  $ layout     : S3<Layout>        
+#>  $ labels     : <ggplot2::labels> 
+#>  $ meta       : <list>            
+#>  $ plot_env   : <environment>     
+#>  $ ink        : <character>
+
+ +
+ +When you define a new class, the object you've assigned it to automatically becomes the class definition which comes with a free, standard constructor. This means that we can start building new plots with our subclass right away. Note that we haven't implemented any behaviour around the `ink` property (yet), so it will just print like a normal plot. + +
+ +
my_plot <- inked_ggplot(data = mpg, ink = "red") +
+  geom_point(aes(displ, hwy))
+my_plot
+
+ + +
+ +In contrast to S3, where you would change list-items by using `$`, in S7 you can use `@` to read and write properties. So if we want to change the stored `ink` colour, we can use: + +
+ +
my_plot@ink <- "blue"
+ +
+ +### Testing + +In S3, the recommended way to test for the class of an object is to use a testing function. An example is [`is.factor()`](https://rdrr.io/r/base/factor.html) but it may be that such a testing function doesn't exist. In that case you can use [`inherits()`](https://rdrr.io/r/base/class.html). In S7, it is still recommended to use dedicating testing functions. However, if these are absent, you can use [`S7::S7_inherits()`](https://rconsortium.github.io/S7/reference/S7_inherits.html). If we wanted to write a testing function for our new class, we can do that as follows: + +
+ +
is_inked_ggplot <- function(x) S7::S7_inherits(x, inked_ggplot)
+
+# Is not our class
+is_inked_ggplot(ggplot())
+#> [1] FALSE
+
+# Is our class
+is_inked_ggplot(my_plot)
+#> [1] TRUE
+
+ +
+ +### Overview + +To give an overview of ggplot2's S7 classes, we include the table below. The table also lists the recommended way to test for the class. + +
+ +| Old S3 Class | New S7 Class | Testing functions | +|:---------------|:------------------|:-------------------------------------| +| "ggplot2" | class_ggplot | `is_ggplot(x)` | +| "ggplot_built" | class_ggplot_built | `S7::S7_inherits(x, class_ggplot_built)` | +| "labs" | class_labels | `S7::S7_inherits(x, class_labels)` | +| "uneval" | class_mapping | `is_mapping(x)` | +| "theme" | class_theme | `is_theme(x)` | +| "element_blank" | element_blank | `is_theme_element(x, "blank")` | +| "element_line" | element_line | `is_theme_element(x, "line")` | +| "element_rect" | element_rect | `is_theme_element(x, "rect")` | +| "element_text" | element_text | `is_theme_element(x, "text")` | +| NA | element_polygon | `is_theme_element(x, "polygon")` | +| NA | element_point | `is_theme_element(x, "point")` | +| NA | element_geom | `is_theme_element(x, "geom")` | + +
+ +### Testing + +It should be noted that the `is_*()` testing functions in ggplot2 already know about the S7-ness of the new classes. This is handy when it comes to test expectations, because the testing function can be used instead of the S3/S7 class expectations. Previously, you might have used [`testthat::expect_s3_class()`](https://testthat.r-lib.org/reference/inheritance-expectations.html), but it is better now to test with [`testthat::expect_s7_class()`](https://testthat.r-lib.org/reference/inheritance-expectations.html) or use an `is_*()` function instead. + +
+ +
testthat::test_that(
+  "the plot object has the ggplot class",
+  {
+    plot <- ggplot()
+    
+    # Works regardless of S3 or S7
+    testthat::expect_true(is_ggplot(plot))
+    
+    # This will become dysfunctional in the future.
+    # Do not use this!
+    testthat::expect_s3_class(plot, "ggplot")
+    
+    # This will work in the new version
+    testthat::expect_s7_class(plot, class_ggplot)
+  }
+)
+#> Test passed 🎉
+
+ +
+ +The ggplot2 package manually appends the `"ggplot"` class for backwards compatibility reasons (likewise for `"theme"`). However, once this phases out, the [`testthat::expect_s3_class()`](https://testthat.r-lib.org/reference/inheritance-expectations.html) expectation will become untenable. It is also currently flawed, as it does not work for subclasses! + +
+ +
testthat::test_that(
+  "the inked plot has the ggplot class",
+  {
+    plot <- inked_ggplot()
+    testthat::expect_s3_class(plot, "ggplot")
+  }
+)
+#> ── Failure: the inked plot has the ggplot class ────────────────────────────────
+#> `plot` inherits from 'inked_ggplot'/'ggplot2::ggplot'/'ggplot2::gg'/'S7_object' not 'ggplot'.
+#> Error:
+#> ! Test failed
+
+ +
+ +The advice herein is thus to use [`is_ggplot()`](https://ggplot2.tidyverse.org/reference/is_tests.html). + +## Generics and methods + +If you are new to object oriented programming in R, you might be unfamiliar with what the terms 'generic' and 'methods' mean. They are a form of 'polymorphism', that allow us to use a single function, called the 'generic' function, with different implementations for different classes (where one such implementation is called a 'method'). A well known generic is [`print()`](https://rdrr.io/r/base/print.html), which does different things for different classes. For example `print(1:10)` prints the numeric vector to the console, but `print(my_plot)` opens a graphics device and renders the plot. + +### Your methods for ggplot's generics + +The ggplot2 package also declares some generic functions and contains methods for these, most of which revolve around plot construction. The migration to S7 means that the generics and methods defined by ggplot2 also migrate. + +It is also good to mention that when your package registers a method for one of ggplot2's generics, ggplot2's generic is called an 'external generic' from the point of view of your package. With S7, you should include [`S7::methods_register()`](https://rconsortium.github.io/S7/reference/methods_register.html) in your package's `.onLoad()` call. + +While it is possible to define S7 methods for S3 generics, it is not possible to define S3 methods for S7 generics. + +
+ +
# Declare an S7 generic
+apply_ink <- S7::new_generic("apply_ink", "plot")
+
+# Attempt to implement an S3 method
+apply_ink.inked_ggplot <- function(plot, ...) {
+  # Edit plot to our liking
+  plot@theme <- theme_gray(ink = plot@ink) + plot@theme
+  plot
+}
+
+# Burn your fingers
+apply_ink(my_plot)
+#> Error: Can't find method for `apply_ink(<inked_ggplot>)`.
+
+ +
+ +To allow for a smoother transition from S3 to S7, we plan to keep S3 generics around for another release cycle but will permanently disable them in the future in favour of the S7 generics. Here is an overview of which S7 generics supplant which S3 generics: + +
+ +| Old S3 Generic | New S7 Generic | Description | +|:--------------|:--------------|:------------------------------------------| +| [`ggplot_add()`](https://ggplot2.tidyverse.org/reference/update_ggplot.html) | [`update_ggplot()`](https://ggplot2.tidyverse.org/reference/update_ggplot.html) | Determines what happens when you `+` an object to a plot. | +| [`ggplot_build()`](https://ggplot2.tidyverse.org/reference/build_ggplot.html) | [`build_ggplot()`](https://ggplot2.tidyverse.org/reference/build_ggplot.html) | Processes data for display in a plot. | +| [`ggplot_gtable()`](https://ggplot2.tidyverse.org/reference/gtable_ggplot.html) | [`gtable_ggplot()`](https://ggplot2.tidyverse.org/reference/gtable_ggplot.html) | Renders a processed plot to a gtable object. | +| [`element_grob()`](https://ggplot2.tidyverse.org/reference/draw_element.html) | [`draw_element()`](https://ggplot2.tidyverse.org/reference/draw_element.html) | Renders a theme element. | + +
+ +If your package implements methods for one of the old S3 generics, we recommend to replace these with S7 in a timely manner. An important difference between S3 and S7 is that S7 does not use [`NextMethod()`](https://rdrr.io/r/base/UseMethod.html) to magically invoke parental methods on children. Instead, you can use [`S7::super()`](https://rconsortium.github.io/S7/reference/super.html) to explicitly convert the subclass to a parent before invoking the generic again. + +
+ +
S7::method(build_ggplot, inked_ggplot) <- function(plot, ...) {
+  # Edit plot to our liking
+  plot@theme <- theme_gray(ink = plot@ink) + plot@theme
+  
+  # Invoke next method
+  build_ggplot(S7::super(plot, to = class_ggplot), ...)
+}
+
+my_plot
+
+ + +
+ +Just to show that the new property in our subclass works as expected: + +
+ +
my_plot@ink <- "red"
+my_plot
+
+ + +
+ +### Your generics with methods for ggplot2's classes + +Alternatively, it might be that you package has generic functions and methods that handle some of ggplot2's classes. The S7 system has its own way of handling class names, which means that S3 function name patterns of the form `{generic_name}.{class_name}` no longer invoke the correct method for S7 classes. + +
+ +
# Declare S3 generic
+foo <- function(x, ...) {
+  UseMethod("foo")
+}
+
+# Implement S3 method
+foo.labels <- function(x, ...) {
+  x[] <- lapply(x, toupper)
+  x
+}
+
+# Burn your fingers
+foo(labs(colour = "my lowercase title"))
+#> Error in UseMethod("foo"): no applicable method for 'foo' applied to an object of class "c('ggplot2::labels', 'gg', 'S7_object')"
+
+ +
+ +Please note that the [`ggplot()`](https://ggplot2.tidyverse.org/reference/ggplot.html) and [`theme()`](https://ggplot2.tidyverse.org/reference/theme.html) still produces objects with the `"ggplot"` and `"theme"` class for backwards compatibility, but this is scheduled to be removed in the future. The best remedy the dilemma with S3 would be to use [`S7::method()`](https://rconsortium.github.io/S7/reference/method.html), which also works for S3 generics. + +
+ +
# Note that `foo()` is still an S3 generic
+S7::method(foo, class_labels) <- function(x, ...) {
+  x[] <- lapply(x, toupper)
+  x
+}
+
+# Note text has updated
+foo(labs(colour = "my lowercase title"))
+#> <ggplot2::labels> List of 1
+#>  $ colour: chr "MY LOWERCASE TITLE"
+
+ +
+ +If that is not an option, because you may not want to depend on S7, you can *currently* use a little hack. The hack is to prepend the S7 class prefix in the class name of the S3 method. This prefix is the name of the package that defines the class, followed by `::`. + +
+ +
`foo.ggplot2::labels` <- function(x, ...) {
+  x[] <- lapply(x, toupper)
+  x
+}
+ +
+ +## Checklist + +Because all of the above might be hard to parse in its entirety, here is a dainty checklist of common migration issues. + + Do I wrap a gglot class that should become an S7 class with extra properties? + + Are there cases where [`inherits()`](https://rdrr.io/r/base/class.html) is used which should be replaced with test functions or [`S7::S7_inherits()`](https://rconsortium.github.io/S7/reference/S7_inherits.html)? + + Do I edit objects with `$`, `[` or `[[` that were previously lists but are now properties to edit with `@`? + + Are there tests that assume S3 classes, that should use [`testthat::expect_s7_class()`](https://testthat.r-lib.org/reference/inheritance-expectations.html) instead? + + Do I implement methods for one of the S3 generics that should become S7 methods? + + Do I have a generic that may need to facilitate methods for ggplot2's new S7 classes? + + If I assume ggplot2's S7 classes in my code, do I need to bump the required ggplot2 version in the DESCRIPTION file? + +Thank you for reading, we hope that most of it was not necessary! + +[^1]: This still 'works' for backwards compatibility reasons, but it will be phased out in the future, so it should be avoided. + diff --git a/content/blog/ggplot2-4-0-0-s7/thumbnail-sq.jpg b/content/blog/ggplot2-4-0-0-s7/thumbnail-sq.jpg new file mode 100644 index 000000000..f8b056155 Binary files /dev/null and b/content/blog/ggplot2-4-0-0-s7/thumbnail-sq.jpg differ diff --git a/content/blog/ggplot2-4-0-0-s7/thumbnail-wd.jpg b/content/blog/ggplot2-4-0-0-s7/thumbnail-wd.jpg new file mode 100644 index 000000000..5cd5468e2 Binary files /dev/null and b/content/blog/ggplot2-4-0-0-s7/thumbnail-wd.jpg differ