-
Couldn't load subscription status.
- Fork 118
Add post about ggplot2 migrating to S7 #735
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
teunbrand
wants to merge
5
commits into
tidyverse:main
Choose a base branch
from
teunbrand:ggplot2-4-0-0-S7
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 3 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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] | ||
| --- | ||
|
|
||
| <!-- | ||
| TODO: | ||
| * [x] Look over / edit the post's title in the yaml | ||
| * [x] Edit (or delete) the description; note this appears in the Twitter card | ||
| * [x] Pick category and tags (see existing with `hugodown::tidy_show_meta()`) | ||
| * [x] Find photo & update yaml metadata | ||
| * [x] Create `thumbnail-sq.jpg`; height and width should be equal | ||
| * [x] Create `thumbnail-wd.jpg`; width should be >5x height | ||
| * [x] `hugodown::use_tidy_thumbnails()` | ||
| * [ ] Add intro sentence, e.g. the standard tagline for the package | ||
| * [ ] `usethis::use_tidy_thanks()` | ||
| --> | ||
|
|
||
| The ggplot2 package is on the verge to release version 4.0.0. | ||
| 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 damage. | ||
teunbrand marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| This here is a guide for package authors that might be affected by this change. | ||
teunbrand marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| This guide details some changes in classes and functions that may affect downstream packages, and gives recommendations how broken parts might be repaired. | ||
teunbrand marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 also runs on your package to keep tabs on if your package continues to work. | ||
teunbrand marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| If you are lucky, this will happily report that there are no problems and you can stop reading this guide! | ||
teunbrand marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 niches in the R community, ranging from R Core, to Bioconductor to the tidyverse. | ||
teunbrand marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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. | ||
| For example `is.factor()`; and if such a testing function doesn't exist: use `inherits()`. | ||
teunbrand marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 what the terms 'generic' and 'methods' mean. | ||
teunbrand marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| They are a form of 'polymorphism', where we can use a single function, called the 'generic' function, with different implementations for different classes (where one such implementation is called a 'method'). | ||
teunbrand marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 to render the plot. | ||
teunbrand marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ### 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. | ||
|
|
||
| <input type= "checkbox"> Do I wrap a gglot class that should become an S7 class with extra properties?</input> | ||
|
|
||
| <input type= "checkbox"> Are there cases where `inherits()` is used which should be replaced with test functions or `S7::S7_inherits()`?</input> | ||
|
|
||
| <input type= "checkbox"> Do I edit objects with `$`, `[` or `[[` that were previously lists but are now properties to edit with `@`?</input> | ||
|
|
||
| <input type= "checkbox"> Are there tests that assume S3 classes, that should use `testthat::expect_s7_class()` instead?</input> | ||
|
|
||
| <input type= "checkbox"> Do I implement methods for one of the S3 generics that should become S7 methods?</input> | ||
|
|
||
| <input type= "checkbox"> Do I have a generic that may need to facilitate methods for ggplot2's new S7 classes?</input> | ||
|
|
||
| <input type= "checkbox"> If I assume ggplot2's S7 classes in my code, do I need to bump the required ggplot2 version in the DESCRIPTION file?</input> | ||
|
|
||
| Thank you for reading, we hope that most of it was not necessary! | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.