-
-
Notifications
You must be signed in to change notification settings - Fork 226
/
13-chunk-hooks.Rmd
200 lines (140 loc) · 11 KB
/
13-chunk-hooks.Rmd
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
# Chunk Hooks (\*) {#chunk-hooks}
A chunk hook\index{chunk hook} is a function that is triggered by a chunk option when the value of this chunk option is not `NULL`. Chunk hooks provide a way for you to execute additional tasks beyond running the code in a chunk. For example, you may want to post-process plots (e.g., Section \@ref(crop-plot) and Section \@ref(optipng)), or record the time taken by a code chunk (Section \@ref(time-chunk)). Such tasks may not be essential to the computing or analysis in the report, but they can be useful for other purposes (e.g., enhance plots or help you identify the most time-consuming chunks).
You can use chunk hooks purely for their side effects (e.g., only printing out certain information to the console), or for their returned values, which will be written to the output document if the value is a character value.
Like output hooks (see Chapter \@ref(output-hooks)), chunk hooks are also registered via the object `knitr::knit_hooks`\index{knitr!knit\_hooks}. Please note that the names of output hooks are reserved by **knitr**, so you must not use these names for your custom chunk hooks:
```{r}
names(knitr:::.default.hooks)
```
A chunk hook is associated with a chunk option\index{chunk option!chunk hook|see {chunk hook}} of the same name. For example, you can register a chunk hook with the name `greet`:
```{r}
knitr::knit_hooks$set(greet = function(before) {
if (before) "Hello!" else "Bye!"
})
```
We will explain the arguments of the hook function in a moment. Now we set the chunk option `greet = TRUE` for the chunk below:
````md
```{r, greet=TRUE}`r ''`
1 + 1
```
````
And you will see that "Hello!" appears before the chunk, and "Bye!" appears after the chunk in the output below (which is because they are character values):
> ```{r, greet=TRUE}
> 1 + 1
> ```
A chunk hook function can possibly take four arguments: `before`, `options`, `envir`, and `name`. In other words, it can be of this form:
```r
function(before, options, envir, name) {
}
```
All four arguments are optional. You can have four, three, two, one, or even no arguments. In the above example, we used one argument (i.e., `before`). The meanings of these arguments are:
- `before`: Whether the chunk hook is currently being executed before or after the code chunk itself is executed. Note that a chunk hook is executed twice for every code chunk (once before with `hook(before = TRUE)` and once after with `hook(before = FALSE`).
- `options`: The list of chunk options for the current code chunk, e.g., `list(fig.width = 5, echo = FALSE, ...)`.
- `envir`: The environment in which the chunk hook is evaluated.
- `name`: The name of the chunk option that triggered the chunk hook.
As we mentioned in the beginning of this chapter, non-character values returned by chunk hooks are silently ignored, and character values are written to the output document.
## Crop plots {#crop-plot}
The chunk hook `knitr::hook_pdfcrop()`\index{knitr!hook\_pdfcrop()}\index{chunk hook!crop plot} can be used to crop PDF and other types of plot files, i.e., remove the extra margins in plots. To enable it, set this hook via `knit_hooks$set()`\index{knitr!knit\_hooks} in a code chunk, and turn on the corresponding chunk option, e.g.,
```{r}
knitr::knit_hooks$set(crop = knitr::hook_pdfcrop)
```
Then you can use the chunk option `crop = TRUE`\index{chunk option!crop} to crop plots in a code chunk.
The hook `hook_pdfcrop()` calls the external program `pdfcrop` to crop PDF files. This program often comes with a LaTeX distribution (e.g., TeX Live or MiKTeX). You can check if it is available in your system via:
```{r}
# if the returned value is not empty, it is available
Sys.which('pdfcrop')
```
If you are using the LaTeX distribution TinyTeX (see Section \@ref(install-latex)), and `pdfcrop` is not available in your system, you may install it via `tinytex::tlmgr_install('pdfcrop')`\index{tinytex!tlmgr\_install()}.
```{r include = FALSE, eval=!tinytex:::check_installed("pdfcrop")}
tinytex::tlmgr_install('pdfcrop')
```
For non-PDF plot files such as PNG or JPEG files, this hook function calls the R package **magick** [@R-magick]\index{R package!magick} for cropping. You need to make sure this R package has been installed. Figure \@ref(fig:crop-no) shows a plot that is not cropped, and Figure \@ref(fig:crop-yes) shows the same plot but has been cropped.
```{r, crop-no, crop=NULL, echo=FALSE, fig.height=4, fig.cap='A plot that is not cropped.', out.extra=if (knitr::is_latex_output()) '', resize.command='framebox'}
if (!knitr::is_latex_output()) par(bg = 'gray', fg = 'yellow')
plot(cars)
```
```{r, crop-yes, crop=TRUE, echo=FALSE, fig.height=4, fig.cap='A plot that is cropped.', ref.label='crop-no', out.extra=if (knitr::is_latex_output()) '', resize.command='framebox'}
```
## Optimize PNG plots {#optipng}
If you have installed the program OptiPNG (<http://optipng.sourceforge.net>)\index{OptiPNG}, you may use the hook `knitr::hook_optipng()`\index{knitr!hook\_optipng()} to optimize PNG plot files to a smaller size without losing the image quality\index{chunk hook!optimize PNG}\index{figure!optimize PNG}.
```{r, eval=FALSE}
knitr::knit_hooks$set(optipng = knitr::hook_optipng)
```
After you set up this hook, you can use the chunk option `optipng`\index{chunk option!optipng} to pass command-line arguments to OptiPNG, e.g., `optipng = '-o7'`. These command-line arguments are optional, which means you can just use `optipng = ''` to enable the hook for a code chunk. Please see the user manual on the website of OptiPNG to know the possible arguments.
OptiPNG can be easily installed on macOS with Homebrew (https://brew.sh): `brew install optipng`, and on Windows using Scoop (https://scoop.sh/): `scoop install optipng`.
## Report how much time each chunk takes to run {#time-chunk}
By default, **knitr** provides a text-based progress bar to show you the knitting progress. If you want more precise timing information about the chunks, you may register a custom chunk hook to record the time for each chunk. Here is an example hook:
```{r, eval=FALSE}
knitr::knit_hooks$set(time_it = local({
now <- NULL
function(before, options) {
if (before) {
# record the current time before each chunk
now <<- Sys.time()
} else {
# calculate the time difference after a chunk
res <- difftime(Sys.time(), now, units = "secs")
# return a character string to show the time
paste('Time for this code chunk to run:',
round(res, 2), 'seconds')
}
}})
)
```
Then you can time a chunk with the chunk option `time_it`, e.g.,
````
```{r, time_it = TRUE}`r ''`
Sys.sleep(2)
```
````
If you want to time all code chunks, you can certainly set the option globally: `knitr::opts_chunk$set(time_it = TRUE)`.
In the above hook function, you can also output more information from the chunk options (i.e., the `options` argument of the function). For example, you may print out the chunk label in the returned value:
```{r, eval=FALSE}
paste('Time for the chunk', options$label, 'to run:', res)
```
Or you may record the time without printing it out in the hook:
```{r, eval=FALSE}
all_times <- list() # store the time for each chunk
knitr::knit_hooks$set(time_it = local({
now <- NULL
function(before, options) {
if (before) {
now <<- Sys.time()
} else {
res <- difftime(Sys.time(), now)
all_times[[options$label]] <<- res
}
}})
)
```
Then you can access all the time information in the object `all_times`. This object is a named list with the names being chunk labels, and element values being the execution time for each chunk.
Lastly, as a technical note, we want to explain the use of the `local()` function in the previous hooks because some readers may not be familiar with it. This function allows you to run code in a "local" environment. The main benefit is that variables created in the code are local to that environment, so they will not pollute the outer environment (usually the global environment). For example, we created a variable `now` in `local()`, and used it in the `time_it` hook function. In the hook function, we update the value of `now` via the double arrow `<<-` instead of the normal assignment operator `<-`. This is because `<<-` assigns a value to a variable in the parent environment (which is the environment in `local()` in this case), and `<-` can only assign values to variables in the current environment. Before each code chunk is evaluated, the local variable `now` records the current time. After each code chunk is evaluated, we calculate the time difference between the current time and `now`. Note that `local()` returns the last value in the expression passed to it, which is a (hook) function in this case. In short, `local()` can make your workspace cleaner by not exposing variables that are only used locally but unused in the global environment. If you do not mind creating a variable `now` in the global environment, you can choose not to use `local()`.
## Show the chunk header in the output {#show-header}
Sometimes you may want to show the original code chunk header to your readers. For example, when you write an R Markdown tutorial, you may want to show both the chunk output and the chunk options that you used to generate the output, so your readers can learn how to do it by themselves.
The original chunk options are actually stored as a character string in the chunk option `params.src`. After you know this, you may write a chunk hook to add `params.src` to the output. Below is a full example:
`r import_example('chunk-wrapper.Rmd')`
Basically, we restored the chunk header from `options$params.src` by putting this string inside ```` ```{r, }````. Then we wrapped this line in a pair of four backticks, so it can be displayed verbatim in the output. Note that the original code chunk might be indented (e.g., when it is nested in a list item), so we also need to add the proper indentation, which is stored in the chunk option `options$indent`.
The output of the bullet list at the end of the above example will be like this:
> - One bullet.
>
> ````
> ```{r, eval=TRUE}`r ''`
> ````
> ```r
> 2 + 2
> ```
> ```
> ## [1] 4
> ```
> ````
> ```
> ````
>
> - Another bullet.
You can see that the code chunk was evaluated, and the chunk header was also added.
## Embed an interactive 3D plot with rgl {#rgl-3d}
The **rgl** package [@R-rgl]\index{R package!rgl} can be used to generate interactive 3D plots. These plots can still be interactive if they are saved to the WebGL format\index{WebGL}, which can be done through a hook function `rgl::hook_webgl()`\index{chunk hook!WebGL plot}\index{figure!WebGL}. Below is an example that shows you how to set up **rgl** and **knitr** so 3D plots can be saved while preserving the interactivity:
`r import_example('rgl-3d.Rmd')`
You should get an interactive 3D scatterplot like Figure \@ref(fig:rgl-3d) after you compile this example. Note that the interactive plots only work when the output format is HTML.
```{r, rgl-3d, echo=FALSE, fig.cap='A 3D scatterplot generated from the rgl package.', fig.align='center'}
knitr::include_graphics('images/rgl-3d.png', dpi = NA)
```