forked from mmistakes/minimal-mistakes
-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy path2022-03-10-self-titled-ggplot2-plots.Rmd
More file actions
338 lines (274 loc) · 10.5 KB
/
Copy path2022-03-10-self-titled-ggplot2-plots.Rmd
File metadata and controls
338 lines (274 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
---
title: Self-documenting plots in ggplot2
excerpt: Including plotting code as an annotation on a plot
tags:
- r
- rlang
- ggplot2
- nonstandard evaluation
share: true
header:
overlay_image: "assets/images/2022-03-neon.jpg"
image_description: "A neon sign that says 'neon'"
overlay_filter: rgba(10, 10, 10, 0.5)
caption: "Photo credit: [**Slava Kuzminsk**](https://unsplash.com/photos/qnHOFl4VuFc)"
---
```{r setup, include = FALSE}
knitr::opts_chunk$set(
echo = TRUE,
fig.asp = NULL
)
a_element_text <- downlit::autolink_url("ggplot2::element_text")
a_wrap_elements <- downlit::autolink_url("patchwork::wrap_elements")
a_parse <- downlit::autolink_url("base::parse")
a_eval <- downlit::autolink_url("base::eval")
a_expr <- downlit::autolink_url("rlang::expr")
a_enexpr <- downlit::autolink_url("rlang::enexpr")
extrafont::loadfonts(device = "win")
self_document <- function(expr) {
monofont <- ifelse(
extrafont::choose_font("Consolas") == "",
"mono",
"Consolas"
)
p <- rlang::enexpr(expr)
title <- rlang::expr_text(p) |>
grkstyle::grk_style_text() |>
paste0(collapse = "\n")
patchwork::wrap_elements(eval(p)) +
patchwork::plot_annotation(
title = title,
theme = theme(
plot.title = element_text(
family = monofont, hjust = 0, size = rel(.9),
margin = margin(0, 0, 5.5, 0, unit = "pt")
)
)
)
}
```
When I am showing off a plotting technique in
[ggplot2](https://ggplot2.tidyverse.org/), I sometimes like to include
the R code that produced the plot *as part of the plot*. Here is an
example I made to demonstrate the `debug` parameter in
[`element_text()`](`r a_element_text`):
```{r basic-example, fig.width = 4, fig.height = 4, fig.cap = "A ggplot2 plot of a histogram with the plotting code above the image. The plot theme includes yellow shading and points in the x and y axis titles."}
library(ggplot2)
self_document(
ggplot(mtcars, aes(x = mpg)) +
geom_histogram(bins = 20, color = "white") +
labs(title = "A basic histogram") +
theme(axis.title = element_text(debug = TRUE))
)
```
Let's call these "self-documenting plots". If we're feeling nerdy, we
might also call them "qquines", although they are not true
[quines](https://en.wikipedia.org/wiki/Quine_%28computing%29).
In this post, we will build up a `self_document()` function from scratch. Here are
the problems we need to sort out:
- how to put plotting code above a title
- how to capture plotting code and convert it into text
## Creating the code annotation
As a first step, let's just treat our plotting code as a string that
is ready to use for annotation.
```{r}
p_text <- 'ggplot(mtcars, aes(x = mpg)) +
geom_histogram(bins = 20, color = "white") +
labs(title = "A basic histogram")'
p_plot <- ggplot(mtcars, aes(x = mpg)) +
geom_histogram(bins = 20, color = "white") +
labs(title = "A basic histogram")
```
In order to have a titled plot along with this annotation, we need some
way to combine these two graphical objects together (the code and the
plot produced by ggplot2). I like the
[patchwork](https://patchwork.data-imaginist.com/articles/patchwork.html)
package for this job. Here we use
[`wrap_elements()`](`r a_wrap_elements`) to capture the plot into a
"patch" that patchwork can annotate.
```{r, include = FALSE}
knitr::opts_chunk$set(out.width = "50%", fig.width = 4, fig.height = 4)
```
```{r from-strings, fig.cap = "A ggplot2 plot of a histogram with the plotting code above the image. Here the title is in the default font."}
library(patchwork)
wrap_elements(p_plot) +
plot_annotation(title = p_text)
```
Let's style this title to use a monospaced font. I use Windows and like
Consolas, so I will use that font.
```{r from-strings-consolas, fig.cap = "A ggplot2 plot of a histogram with the plotting code above the image. Here the title is in Consolas."}
# Use default mono font if "Consolas" is not available
extrafont::loadfonts(device = "win", quiet = TRUE)
monofont <- ifelse(
extrafont::choose_font("Consolas") == "",
"mono",
"Consolas"
)
title_theme <- theme(
plot.title = element_text(
family = monofont, hjust = 0, size = rel(.9),
margin = margin(0, 0, 5.5, 0, unit = "pt")
)
)
wrap_elements(p_plot) +
plot_annotation(title = p_text, theme = title_theme)
```
One problem with this setup is that the plotting code has to be edited
in two places: the plot `p_plot` and the title `p_text`. As a result,
it's easy for these two pieces of code to fall out of sync with each
other, turning our self-documenting plot into a lying liar plot.
The solution is pretty easy: Tell R that `p_text` is code with
[`parse()`](`r a_parse`) and evaluate the code with
[`eval()`](`r a_eval`):
```{r from-strings-consolas-eval, fig.cap = "A ggplot2 plot of a histogram with the plotting code above the image."}
wrap_elements(eval(parse(text = p_text))) +
plot_annotation(title = p_text, theme = title_theme)
```
This *works*. It gets the job done. But we find ourselves in a clumsy
workflow, either having to edit R code inside of quotes or editing the
plot interactively and then having to wrap it in quotes. Let's do better.
## Capturing plotting code as a string
Time for some *nonstandard evaluation*. I will use the
[rlang](https://rlang.r-lib.org/) package, although in principle we
could use functions in base R to accomplish these goals.
First, we are going to use [`rlang::expr()`](`r a_expr`) to
capture/quote/[defuse](https://rlang.r-lib.org/reference/topic-defuse.html)
the R code as an expression. We can print the code as code, print it as
text, and use `eval()` to show the plot.
```{r from-code, fig.cap = "A ggplot2 plot of a histogram with the plotting code above the image."}
p_code <- rlang::expr(
ggplot(mtcars, aes(x = mpg)) +
geom_histogram(bins = 20, color = "white") +
labs(title = "A basic histogram")
)
# print the expressions
p_code
# expression => text
rlang::expr_text(p_code)
eval(p_code)
```
Then, it should be straightforward to make the self-documenting plot, right?
```{r from-code-eval, fig.cap = "A ggplot2 plot of a histogram with the plotting code above the image. In this case, the title is mostly on one line and some text is cut off from the image."}
p_code <- rlang::expr(
ggplot(mtcars, aes(x = mpg)) +
geom_histogram(bins = 20, color = "white") +
labs(title = "A basic histogram")
)
wrap_elements(eval(p_code)) +
plot_annotation(title = rlang::expr_text(p_code), theme = title_theme)
```
Hey, it reformatted the title! Indeed, in the process of capturing the
code, the code formatting was lost. To get something closer to the
source code we provided, we have to reformat the captured code before we
print it.
The [styler](https://styler.r-lib.org/) package provides a suite of
functions for reformatting code. We can define our own coding
styles/formatting rules to customize how styler works. I like the styler
rules used by Garrick Aden-Buie in his
[grkstyle](https://github.com/gadenbuie/grkstyle) package, so I will use
`grkstyle::grk_style_text()` to reformat the code.
```{r from-code-eval-style, fig.cap = "A ggplot2 plot of a histogram with the plotting code above the image."}
p_code <- rlang::expr(
ggplot(mtcars, aes(x = mpg)) +
geom_histogram(bins = 20, color = "white") +
labs(title = "A basic histogram")
)
wrap_elements(eval(p_code)) +
plot_annotation(
title = rlang::expr_text(p_code) |>
grkstyle::grk_style_text() |>
# reformatting returns a vector of lines,
# so we have to combine them
paste0(collapse = "\n"),
theme = title_theme
)
```
## Putting it all together
When we write our `self_document()` function, the only change we have to
make is using [`rlang::enexpr()`](`r a_enexpr`) instead `rlang::expr()`. The
en-variant is used when we want to *en*-quote exactly what the user
provided. Aside from that change, our `self_document()` function just bundles together all of the code we developed above:
```{r}
self_document <- function(expr) {
monofont <- ifelse(
extrafont::choose_font("Consolas") == "",
"mono",
"Consolas"
)
p <- rlang::enexpr(expr)
title <- rlang::expr_text(p) |>
grkstyle::grk_style_text() |>
paste0(collapse = "\n")
patchwork::wrap_elements(eval(p)) +
patchwork::plot_annotation(
title = title,
theme = theme(
plot.title = element_text(
family = monofont, hjust = 0, size = rel(.9),
margin = margin(0, 0, 5.5, 0, unit = "pt")
)
)
)
}
```
And let's confirm that it works.
```{r final-demo, fig.cap = "A ggplot2 plot of a histogram with the plotting code above the image."}
library(ggplot2)
self_document(
ggplot(mtcars, aes(x = mpg)) +
geom_histogram(bins = 20, color = "white") +
labs(title = "A basic histogram")
)
```
Because we developed this function on top of rlang, we can do some tricks like
injecting a variable's value when capturing the code. For example, here I
use `!! color` to replace the `color` variable with the actual value.
```{r final-demo-inject, fig.cap = "A ggplot2 plot of a histogram with the plotting code above the image."}
color <- "white"
self_document(
ggplot(mtcars, aes(x = mpg)) +
geom_histogram(bins = 20, color = !! color) +
labs(title = "A basic histogram")
)
```
And if you are wondering, yes, we can `self_document()` a
`self_document()` plot.
```{r final-demo-self-document, fig.cap = "A self_document() plot of a plot of a histogram with the plotting code above the image. There are two sets of code on top of each other."}
self_document(
self_document(
ggplot(mtcars, aes(x = mpg)) +
geom_histogram(bins = 20, color = "white") +
labs(title = "A basic histogram")
)
)
```
## Alas, comments are lost
One downside of this approach is that helpful comments are lost.
```{r final-demo-no-comments, fig.cap = "A ggplot2 plot of a histogram with the plotting code above the image."}
self_document(
ggplot(mtcars, aes(x = mpg)) +
geom_histogram(bins = 20, color = !! color) +
# get rid of that grey
theme_minimal() +
labs(title = "A basic histogram")
)
```
I am not sure how to include comments. One place where comments are stored
and printed is in function bodies:
```{r}
f <- function() {
ggplot(mtcars, aes(x = mpg)) +
geom_histogram(bins = 20, color = !! color) +
# get rid of that grey
theme_minimal() +
labs(title = "A basic histogram")
}
print(f, useSource = TRUE)
```
I have no idea how to go about exploiting this feature for
self-documenting plots, however.
```{r, include = FALSE}
.parent_doc <- knitr::current_input()
```
```{r, child = "_R/_footer.Rmd"}
```