Skip to content

Commit 08f1900

Browse files
committed
improve ggplot2 in packages vignette
1 parent adb62b1 commit 08f1900

File tree

1 file changed

+74
-70
lines changed

1 file changed

+74
-70
lines changed

vignettes/programming-with-ggplot2.Rmd

Lines changed: 74 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -19,31 +19,32 @@ This vignette is intended for package developers who use __ggplot2__ functions w
1919
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`).
2020

2121
```{r}
22-
plot_mpg_classes <- function() {
22+
mpg_drv_summary <- function() {
2323
ggplot2::ggplot(ggplot2::mpg) +
24-
ggplot2::geom_bar(ggplot2::aes(x = .data$class)) +
24+
ggplot2::geom_bar(ggplot2::aes(x = .data$drv)) +
2525
ggplot2::coord_flip()
2626
}
2727
```
2828

2929
```{r, include=FALSE}
30-
plot_mpg_classes()
30+
# make sure this function runs!
31+
mpg_drv_summary()
3132
```
3233

33-
3434
If you use __ggplot2__ functions frequently, you may wish to import one or more functions from __ggplot2__ into your `NAMESPACE`. If you use __roxygen2__, you can include `#' @importFrom ggplot2 <one or more function names>`. in any roxygen comment block (this will not work for datasets). If you do this, you will need to put __ggplot2__ in your `Imports` rather than your `Suggests`.
3535

3636
```{r}
3737
#' @importFrom ggplot2 ggplot aes geom_bar coord_flip
38-
plot_mpg_classes <- function() {
38+
mpg_drv_summary <- function() {
3939
ggplot(ggplot2::mpg) +
40-
geom_bar(aes(x = .data$class)) +
40+
geom_bar(aes(x = drv)) +
4141
coord_flip()
4242
}
4343
```
4444

4545
```{r, include=FALSE}
46-
plot_mpg_classes()
46+
# make sure this function runs!
47+
mpg_drv_summary()
4748
```
4849

4950
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.
@@ -57,7 +58,8 @@ theme_custom <- function(...) {
5758
```
5859

5960
```{r, include=FALSE}
60-
plot_mpg_classes() + theme_custom()
61+
# make sure this function runs!
62+
mpg_drv_summary() + theme_custom()
6163
```
6264

6365
If you have __ggplot2__ in your `Imports` anyway, it is much easier to import the infix into your namespace.
@@ -71,89 +73,73 @@ theme_custom <- function(...) {
7173
```
7274

7375
```{r, include=FALSE}
74-
plot_mpg_classes() + theme_custom()
76+
mpg_drv_summary() + theme_custom()
7577
```
7678

77-
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 the functions are coming from!
79+
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!
7880

7981
## Using aes() and vars() in a package function
8082

81-
Examples: __ggdag__ uses `utils::globalVariables()`: https://github.com/malcolmbarrett/ggdag/blob/424bcb35126a7b7c0ec76429c0306dcaba38b764/R/utils.R#L2-L51
82-
83-
To create any graphic using __ggplot2__ you will probably need to use `aes()` at least once. If your graphic uses facets, you will need to use `vars()`. Both of these functions are intended to refer to variables that exist in the data, so the CMD check will give you a warning if you try to use either inside a function.
84-
85-
There are two situations in which you might use these. The first is when you already know the mapping in advance. In this case, you should use the `.data` pronoun from __rlang__:
83+
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()` or formula notation to refer to columns in the 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:
8684

8785
```{r}
88-
#' @importFrom rlang .data
89-
residual_histogram <- function(fit) {
90-
plot_data <- data.frame(residual = stats::resid(fit))
91-
ggplot(plot_data, aes(x = .data$residual)) + geom_histogram()
86+
mpg_drv_summary <- function() {
87+
ggplot(mpg) +
88+
geom_bar(aes(x = drv)) +
89+
coord_flip()
9290
}
91+
```
9392

94-
test_fit <- lm(hwy ~ cty, data = mpg)
95-
residual_histogram(test_fit)
93+
```
94+
N checking R code for possible problems (2.7s)
95+
mpg_drv_summary: no visible binding for global variable ‘drv’
96+
Undefined global functions or variables:
97+
drv
9698
```
9799

98-
If the user specifies a specific part of the mapping (like the variable from a data frame), you can either have this specified as a column name (e.g., `column = "class"`) or using the same kind of non-standard evaluation used by `aes()` and `vars()` (e.g., `column = class`). In the first case, use `rlang::sym()` to create an unevaluated expression with the name, then use `!!` to evaluate it within `aes()`.
100+
If you already know the mapping in advance (like the above example) you should use the `.data` pronoun from __rlang__:
99101

100102
```{r}
101-
#' @importFrom rlang !! sym
102-
col_summary <- function(data, col) {
103+
#' @importFrom rlang .data
104+
mpg_drv_summary <- function() {
103105
ggplot(mpg) +
104-
geom_bar(aes(x = !!sym(col))) +
106+
geom_bar(aes(x = .data$drv)) +
105107
coord_flip()
106108
}
107-
108-
col_summary(mpg, "class")
109109
```
110110

111-
112-
To use the same kind of non-standard evaluation that `aes()` uses, use `enquo()` to capture the user input, and `!!` pass the (unevaluated) user input in to `aes()`:
111+
If the user specifies a part of the mapping, you can either have this specified 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]]`:
113112

114113
```{r}
115-
#' @importFrom rlang !! enquo
114+
#' @importFrom rlang .data
116115
col_summary <- function(data, col) {
117116
ggplot(mpg) +
118-
geom_bar(aes(x = !!enquo(col))) +
117+
geom_bar(aes(x = .data[[col]])) +
119118
coord_flip()
120119
}
121120
122-
col_summary(mpg, class)
121+
col_summary(mpg, "drv")
123122
```
124123

125-
https://www.tidyverse.org/articles/2018/07/ggplot2-tidy-evaluation/
126-
127-
Please don't:
128-
129-
- Use `annotate()` to avoid using `aes()`
130-
- Use `aes_()` or `aes_string()`: These functions are deprecated and at some point will be removed
124+
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()`.
131125

132-
## Common tasks + best practices
126+
<!-- this uses development rlang, which is not yet released -->
133127

134-
### Creating a theme function
135-
136-
On themes: 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.
137-
138-
Call it `theme_*()`.
139-
140-
Examples: __cowplot__, __ggthemes__
141-
142-
```{r}
143-
theme_col_summary <- function(...) {
144-
theme_grey(...) %+replace%
145-
theme(
146-
panel.border = element_rect(size = 1, fill = NA),
147-
panel.background = element_blank(),
148-
panel.grid = element_line(colour = "grey80")
149-
)
128+
```{r, eval=FALSE}
129+
col_summary <- function(data, col) {
130+
ggplot(mpg) +
131+
geom_bar(aes(x = {{ col }})) +
132+
coord_flip()
150133
}
151134
152-
col_summary(mpg, class) +
153-
theme_col_summary()
135+
col_summary(mpg, drv)
154136
```
155137

156-
- Don't calculate anything during the build phase (like theme defaults). Instead, make a getter and a setter
138+
To summarise, if you know the mapping or facet specification in advance, use `aes(.data$col)` or `vars(.data$col)`. If you have the column name as a character scalar, use `aes(.data[[col]]` or `vars(.data[[col]])`. If you would like your function to look and feel like `aes()` and `vars()`, use `aes({{ col }})` or `vars({{ col }})`.
139+
140+
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 fail in unexpected ways.
141+
142+
## Best practices for common tasks
157143

158144
### Using ggplot2 to visualize an object
159145

@@ -223,6 +209,36 @@ If you don't use __ggplot2__ for your visualizations but would like to implement
223209

224210
Don't implement an S3 generic like `plot()`, or `autoplot()` if you don't have any control over the S3 object (it makes it hard for the package developer who does have control over the S3 to change the underlying data structure).
225211

212+
### Creating a new theme
213+
214+
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__ package.
215+
216+
```{r}
217+
#' @importFrom ggplot2 %+replace%
218+
theme_custom <- function(...) {
219+
theme_grey(...) %+replace%
220+
theme(
221+
panel.border = element_rect(size = 1, fill = NA),
222+
panel.background = element_blank(),
223+
panel.grid = element_line(colour = "grey80")
224+
)
225+
}
226+
227+
mpg_drv_summary() + theme_custom()
228+
```
229+
230+
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 you 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:
231+
232+
```{r}
233+
default_theme <- function() {
234+
theme_custom()
235+
}
236+
237+
mpg_drv_summary2 <- function() {
238+
mpg_drv_summary() + default_theme()
239+
}
240+
```
241+
226242
### Testing ggplot2 output
227243

228244
We suggest testing the output of ggplot2 in using the __vdiffr__ package, which is a tool to manage visual test cases (this is how 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. These tests do not run on CRAN
@@ -238,15 +254,3 @@ test_that("output of ggplot() is stable", {
238254
```
239255

240256
Don't use `expect_reference()`, or test the internal representation of a ggplot object (unless your package created some part of the internal representation).
241-
242-
## Imports or Suggests
243-
244-
In documentation (examples, vignettes) make sure you put __ggplot2__ in `Suggests`.
245-
246-
If your package doesn't really use ggplot2 but would like to provide compatibility for other ggplot2 users, use Suggests (you may have to use some `.onLoad()` tricks to register S3). Can't find a good example with ggplot2, but __sf__ does this for __dplyr__.
247-
248-
If your package uses ggplot2 to visualize the data structures used by your package (e.g., __rstan__)
249-
250-
- Please don't include __ggplot2__ in the Imports when you only use it in your package documentation (i.e., examples and/or vignettes).
251-
252-
- If you include __ggplot2__ in Suggests, you may have to include some other packages, since the dependencies of

0 commit comments

Comments
 (0)