Skip to content

Commit b795951

Browse files
committed
rename ggplot2 in packages vignette, polish draft for comments
1 parent 08f1900 commit b795951

File tree

2 files changed

+247
-256
lines changed

2 files changed

+247
-256
lines changed

vignettes/ggplot2-in-packages.Rmd

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
---
2+
title: "Using ggplot2 in packages"
3+
output: rmarkdown::html_vignette
4+
vignette: >
5+
%\VignetteIndexEntry{Using ggplot2 in packages}
6+
%\VignetteEngine{knitr::rmarkdown}
7+
%\VignetteEncoding{UTF-8}
8+
---
9+
10+
```{r, include = FALSE}
11+
knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.show = "hide")
12+
library(ggplot2)
13+
```
14+
15+
This vignette is intended for package developers who use ggplot2 within their package code. As of this writing, this includes over 2,000 packages on CRAN and many more elsewhere! Programming with ggplot2 within a package adds several constraints, particularly if you would like to submit the package to CRAN. In particular, programming within an R package changes the way you refer to functions from ggplot2 and how you use ggplot2's non-standard evaluation within `aes()` and `vars()`.
16+
17+
## Referring to ggplot2 functions
18+
19+
As with any function from another package, you will have to list ggplot2 in your `DESCRIPTION` under `Imports` or `Suggests` and refer to its functions using `::` (e.g., `ggplot2::function_name`).
20+
21+
```{r}
22+
mpg_drv_summary <- function() {
23+
ggplot2::ggplot(ggplot2::mpg) +
24+
ggplot2::geom_bar(ggplot2::aes(x = .data$drv)) +
25+
ggplot2::coord_flip()
26+
}
27+
```
28+
29+
```{r, include=FALSE}
30+
# make sure this function runs!
31+
mpg_drv_summary()
32+
```
33+
34+
If you use ggplot2 functions frequently, you may wish to import one or more functions from ggplot2 into your `NAMESPACE`. If you use [roxygen2](https://cran.r-project.org/package=roxygen2), you can include `#' @importFrom ggplot2 <one or more function names>` in any roxygen comment block (this will not work for datasets like `mpg`). If you do this, you will need to put ggplot2 in your `Imports` rather than your `Suggests`.
35+
36+
```{r}
37+
#' @importFrom ggplot2 ggplot aes geom_bar coord_flip
38+
mpg_drv_summary <- function() {
39+
ggplot(ggplot2::mpg) +
40+
geom_bar(aes(x = drv)) +
41+
coord_flip()
42+
}
43+
```
44+
45+
```{r, include=FALSE}
46+
# make sure this function runs!
47+
mpg_drv_summary()
48+
```
49+
50+
If you use infix operators from ggplot2 like `%+replace%` and you want to keep ggplot2 in `Suggests`, you can assign the operator within the function before it is used.
51+
52+
```{r}
53+
theme_custom <- function(...) {
54+
`%+replace%` <- ggplot2::`%+replace%`
55+
56+
ggplot2::theme_grey(...) %+replace%
57+
ggplot2::theme(panel.background = ggplot2::element_blank())
58+
}
59+
```
60+
61+
```{r, include=FALSE}
62+
# make sure this function runs!
63+
mpg_drv_summary() + theme_custom()
64+
```
65+
66+
If you have ggplot2 in your `Imports` anyway, it is much easier to import the infix into your namespace.
67+
68+
```{r}
69+
#' @importFrom ggplot2 %+replace%
70+
theme_custom <- function(...) {
71+
ggplot2::theme_grey(...) %+replace%
72+
ggplot2::theme(panel.background = ggplot2::element_blank())
73+
}
74+
```
75+
76+
```{r, include=FALSE}
77+
mpg_drv_summary() + theme_custom()
78+
```
79+
80+
Even if you use many ggplot2 functions in your package, it is unwise to use ggplot2 in `Depends` or import the entire package into your `NAMESPACE`. Using ggplot2 in `Depends` will attach ggplot2 when your package is attached, which includes when your package is tested. This makes it difficult to ensure that others can use the functions in your package without attaching it (i.e., using `::`). Similarly, importing all 450 of ggplot2's exported objects into your namespace makes it difficult to separate the responsibility of your package and the responsibility of ggplot2, in addition to making it difficult for readers of your code to figure out where functions are coming from!
81+
82+
## Using `aes()` and `vars()` in a package function
83+
84+
To create any graphic using ggplot2 you will probably need to use `aes()` at least once. If your graphic uses facets, you might be using `vars()` to refer to columns in the plot/layer data. Both of these functions use non-standard evaluation, so if you try to use them in a function within a package they will result in a CMD check note:
85+
86+
```{r}
87+
#' @importFrom ggplot2 ggplot aes geom_bar coord_flip
88+
mpg_drv_summary <- function() {
89+
ggplot(ggplot2::mpg) +
90+
geom_bar(aes(x = drv)) +
91+
coord_flip()
92+
}
93+
```
94+
95+
```
96+
N checking R code for possible problems (2.7s)
97+
mpg_drv_summary: no visible binding for global variable ‘drv’
98+
Undefined global functions or variables:
99+
drv
100+
```
101+
102+
If you already know the mapping in advance (like the above example) you should use the `.data` pronoun from [rlang](https://cran.r-project.org/package=rlang) to make it explicit that you are referring to the `drv` in the data and not some other variable named `drv` (which may or may not exist elsewhere).
103+
104+
```{r}
105+
#' @importFrom rlang .data
106+
mpg_drv_summary <- function() {
107+
ggplot(mpg) +
108+
geom_bar(aes(x = .data$drv)) +
109+
coord_flip()
110+
}
111+
```
112+
113+
If the user specifies a part of the mapping, you can either specify this as a column name (e.g., `col = "drv"`) or using the same kind of non-standard evaluation used by `aes()` and `vars()` (e.g., `col = drv`). In the first case, use `.data[[col]]`:
114+
115+
```{r}
116+
#' @importFrom rlang .data
117+
col_summary <- function(data, col) {
118+
ggplot(mpg) +
119+
geom_bar(aes(x = .data[[col]])) +
120+
coord_flip()
121+
}
122+
123+
col_summary(mpg, "drv")
124+
```
125+
126+
To use the same kind of non-standard evaluation that `aes()` uses, use `{{ col }}` to pass the unevaluated expression the user typed in `col` to `aes()`.
127+
128+
<!-- this uses development rlang, which is not yet released -->
129+
130+
```{r, eval=FALSE}
131+
col_summary <- function(data, col) {
132+
ggplot(mpg) +
133+
geom_bar(aes(x = {{ col }})) +
134+
coord_flip()
135+
}
136+
137+
col_summary(mpg, drv)
138+
```
139+
140+
To summarise, if you know the mapping or facet specification is `col` in advance, use `aes(.data$col)` or `vars(.data$col)`. If `col` is a variable that contains the column name as a character scalar, use `aes(.data[[col]]` or `vars(.data[[col]])`. If you would like the behaviour of `col` to look and feel like `aes()` and `vars()`, use `aes({{ col }})` or `vars({{ col }})`.
141+
142+
You will see a lot of other ways to do this in the wild, but the syntax we use here is the only one we can guarantee will work in the future! In particular, don't use `aes_()` or `aes_string()`, as they are deprecated and may be removed in a future version. Finally, don't skip the step of creating a data frame and a mapping to pass in to `ggplot()` or its layers! You will see other ways of doing this in the wild, but these rely on undocumented behaviour and can fail in unexpected ways.
143+
144+
## Best practices for common tasks
145+
146+
### Using ggplot2 to visualize an object
147+
148+
ggplot2 is commonly used in packages to visualize objects (e.g., in a `plot()`-style function). For example, a package might define an S3 object that represents the probability of various discrete values:
149+
150+
```{r}
151+
mpg_drv_dist <- structure(
152+
c(
153+
"4" = 103 / 234,
154+
"f" = 106 / 234,
155+
"r" = 25 / 234
156+
),
157+
class = "discrete_distr"
158+
)
159+
```
160+
161+
Many S3 objects in R implement a `plot()` method, but it is unrealistic to expect that a single `plot()` method can provide the visualization every one of your users is looking for. It is useful, however, to provide a `plot()` method as a visual summary that users can call to understand the essence of an object. To satisfy all your users, we suggest writing a function that transforms the object into a data frame (or a `list()` of data frames if your object is more complicated). A good example of this approach is [ggdendro](https://cran.r-project.org/package=ggdendro), which creates dendrograms using ggplot2 but also computes the data necessary for users to make their own. For the above example, the function might look like this:
162+
163+
```{r}
164+
discrete_distr_data <- function(x) {
165+
tibble::tibble(
166+
value = names(x),
167+
probability = as.numeric(x)
168+
)
169+
}
170+
171+
discrete_distr_data(mpg_drv_dist)
172+
```
173+
174+
In general, users of `plot()` call it for its side-effects: it results in a graphic being displayed. This is different than the behaviour of a `ggplot()`, which is not rendered unless it is explicitly `print()`ed. Because of this, ggplot2 defines its own generic `autoplot()`, a call to which is expected to return a `ggplot()` (with no side effects).
175+
176+
```{r}
177+
#' @importFrom ggplot2 autoplot
178+
#' @importFrom rlang .data
179+
autoplot.discrete_distr <- function(object, ...) {
180+
plot_data <- discrete_distr_data(object)
181+
ggplot(plot_data, aes(.data$value, .data$probability)) +
182+
geom_col() +
183+
coord_flip() +
184+
labs(x = "Value", y = "Probability")
185+
}
186+
```
187+
188+
Once an `autoplot()` method has been defined, a `plot()` method can then consist of `print()`ing the result of `autoplot()`:
189+
190+
```{r}
191+
#' @importFrom graphics plot
192+
plot.discrete_distr <- function(x, ...) {
193+
print(autoplot(x, ...))
194+
}
195+
```
196+
197+
If you don't use ggplot2 for your visualizations but would like to implement `autoplot()` for users that do, it is possible to register your generics only if ggplot2 is installed using `vctrs::s3_register()`.
198+
199+
```{r}
200+
.onLoad <- function(...) {
201+
if (requireNamespace("ggplot2", quietly = TRUE)) {
202+
vctrs::s3_register("ggplot2::autoplot", "discrete_distr")
203+
}
204+
}
205+
```
206+
207+
It is considered bad practice to implement an S3 generic like `plot()`, or `autoplot()` if you don't have any control over the S3 object, as it makes it hard for the package developer who does have control over the S3 to implement the method themselves. This shouldn't stop you from creating your own functions to visualize these objects!
208+
209+
### Creating a new theme
210+
211+
When creating a new theme, it's always good practice to start with an existing theme (e.g. `theme_grey()`) and then `%+replace%` the elements that should be changed. This is the right strategy even if seemingly all elements are replaced, as not doing so makes it difficult for us to improve themes by adding new elements. There are many excellent examples of themes in the [ggthemes](https://cran.r-project.org/package=ggthemes) package.
212+
213+
```{r}
214+
#' @importFrom ggplot2 %+replace%
215+
theme_custom <- function(...) {
216+
theme_grey(...) %+replace%
217+
theme(
218+
panel.border = element_rect(size = 1, fill = NA),
219+
panel.background = element_blank(),
220+
panel.grid = element_line(colour = "grey80")
221+
)
222+
}
223+
224+
mpg_drv_summary() + theme_custom()
225+
```
226+
227+
It is important that the theme be calculated after the package is loaded. If not, the theme object is stored in the compiled bytecode of the built package, which may or may not align with the installed version of ggplot2! If your package has a default theme for its visualizations, the correct way to load it is to have a function that returns the default theme:
228+
229+
```{r}
230+
default_theme <- function() {
231+
theme_custom()
232+
}
233+
234+
mpg_drv_summary2 <- function() {
235+
mpg_drv_summary() + default_theme()
236+
}
237+
```
238+
239+
### Testing ggplot2 output
240+
241+
We suggest testing the output of ggplot2 in using the [vdiffr](https://cran.r-project.org/package=vdiffr) package, which is a tool to manage visual test cases (this is one of the ways we test ggplot2). If changes in ggplot2 or your code introduce a change in the visual output of a ggplot, tests will fail when you run them locally or on Travis. To use vdiffr, make sure you are using [testthat](https://cran.r-project.org/package=testthat) (you can use `usethis::use_testthat()` to get started) and add vdiffr to `Suggests` in your `DESCRIPTION`. Then, use `expect_doppleganger(<name of plot>, <ggplot object>)` to make a test that fails if there are visual changes in `<ggplot object>`.
242+
243+
```r
244+
test_that("output of ggplot() is stable", {
245+
vdiffr::expect_doppelganger("A blank plot", ggplot())
246+
})
247+
```

0 commit comments

Comments
 (0)