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
|
---
title: "Custom Print Methods"
author: "Yihui Xie"
date: '`r Sys.Date()`'
output:
html:
options:
toc: true
meta:
css: ["@default"]
vignette: >
%\VignetteEngine{knitr::knitr_notangle}
%\VignetteIndexEntry{Custom Print Methods}
---
Before **knitr** v1.6, printing objects in R code chunks basically emulates the
R console. For example, a data frame is printed like this[^1]:
[^1]: Note R prints an object without an explicit `print()` call when it is
*visible*; see `?invisible`
```{r normal-print, comment=''}
head(mtcars)
```
The text representation of the data frame above may look very familiar with most
R users, but for reporting purposes, it may not be satisfactory -- often times
we want to see a table representation instead. That is the problem that the
chunk option `render` and the S3 generic function `knit_print()` try to solve.
## Customize Printing
After we evaluate each R expression in a code chunk, there is an object
returned. For example, `1 + 1` returns `2`. This object is passed to the chunk
option `render`, which is a function with two arguments, `x` and `options`, or
`x` and `...`. The default value for the `render` option is `knit_print`, an S3
function in **knitr**:
```{r}
library(knitr)
knit_print # an S3 generic function
methods(knit_print)
getS3method('knit_print', 'default') # the default method
normal_print
```
As we can see, `knit_print()` has a `default` method, which is basically
`print()` or `show()`, depending on whether the object is an S4 object. This
means it does nothing special when printing R objects:
```{r collapse=TRUE}
knit_print(1:10)
knit_print(head(mtcars))
```
S3 generic functions are extensible in the sense that we can define custom
methods for them. A method `knit_print.foo()` will be applied to the object that
has the class `foo`. Here is quick example of how we can print data frames as
tables:
```{r}
library(knitr)
# define a method for objects of the class data.frame
knit_print.data.frame = function(x, ...) {
res = paste(c('', '', kable(x)), collapse = '\n')
asis_output(res)
}
# register the method
registerS3method("knit_print", "data.frame", knit_print.data.frame)
```
If you define a method in a code chunk in a **knitr** document, the call to
`registerS3method()` will be necessary for R \>= 3.5.0, because the S3 dispatch
mechanism has changed since R 3.5.0. If you are developing an R package, see the
section [For package authors] below.
We expect the print method to return a character vector, or an object that can
be coerced into a character vector. In the example above, the `kable()` function
returns a character vector, which we pass to the `asis_output()` function so
that later **knitr** knows that this result needs no special treatment (just
write it as is), otherwise it depends on the chunk option `results` (`= 'asis'`
/ `'markup'` / `'hide'`) how a normal character vector should be written. The
function `asis_output()` has the same effect as `results = 'asis'`, but saves us
the effort to provide this chunk option explicitly. Now we check how the
printing behavior is changed. We print a number, a character vector, a list, a
data frame, and write a character value using `cat()` in the chunk below:
```{r knit-print}
1 + 1
head(letters)
list(a = 1, b = 9:4)
head(mtcars)
cat('This is cool.')
```
We see all objects except the data frame were printed "normally"[^2]. The data
frame was printed as a real table. Note you do not have to use `kable()` to
create tables -- there are many other options such as **xtable**. Just make sure
the print method returns a character string.
[^2]: The two hashes `##` were from the chunk option `comment`; you can turn
them off by `comment = ''`.
The [**printr**](https://github.com/yihui/printr) package is a companion to
**knitr** containing printing methods for some common objects like matrices and
data frames. Users only need to load this package to get attractive printed
results. A major factor to consider (which has been considered in **printr**)
when defining a printing method is the output format. For example, the table
syntax can be entirely different when the output is LaTeX vs when it is
Markdown.
It is strongly recommended that your S3 method has a `...` argument, so that
your method can safely ignore arguments that are passed to `knit_print()` but
not defined in your method. At the moment, a `knit_print()` method can have two
optional arguments:
- the `options` argument takes a list of the current chunk options;
- the `inline` argument indicates if the method is called in code chunks or
inline R code;
Depending on your application, you may optionally use these arguments. Here are
some examples:
```{r eval=FALSE, tidy=FALSE}
knit_print.classA = function(x, ...) {
# ignore options and inline
}
knit_print.classB = function(x, options, ...) {
# use the chunk option out.height
asis_output(paste0(
'<iframe src="https://yihui.org" height="', options$out.height, '"></iframe>',
))
}
knit_print.classC = function(x, inline = FALSE, ...) {
# different output according to inline=TRUE/FALSE
if (inline) {
'inline output for classC'
} else {
'chunk output for classC'
}
}
knit_print.classD = function(x, options, inline = FALSE, ...) {
# use both options and inline
}
```
Note that when *using* your (or another) `knit_print()` method *inline* (if it
supports that), you must not call `knit_print()` on the object, but just have it
return. For example, your inline code should read `` `r c("foo")` `` and *not*
`` `r knit_print(c("foo"))` ``. The latter inline code would yield the methods'
result for *in-chunk* (not inline), because, as set up in the above,
`knit_print()` methods default to `inline = FALSE`. This default gets
overwritten depending on the context in which `knit_print()` is called (inline
or in-chunk), only when `knit_print()` is called by **knitr** (not you) via the
`render` option (see below). You can, of course, always manually set the inline
option `` `r knit_print(c("foo"), inline = TRUE)` ``, but that's a lot of
typing.
## A Low-level Explanation
You can skip this section if you do not care about the low-level implementation
details.
### The `render` option
As mentioned before, the chunk option `render` is a function that defaults to
`knit_print()`. We can certainly use other render functions. For example, we
create a dummy function that always says "I do not know what to print" no matter
what objects it receives:
```{r}
dummy_print = function(x, ...) {
cat("I do not know what to print!")
# this function implicitly returns an invisible NULL
}
```
Now we use the chunk option `render = dummy_print`:
```{r knit-print, render=dummy_print, collapse=TRUE}
```
Note the `render` function is only applied to visible objects. There are cases
in which the objects returned are invisible, e.g. those wrapped in
`invisible()`.
```{r}
1 + 1
invisible(1 + 1)
invisible(head(mtcars))
x = 1:10 # invisibly returns 1:10
```
### Metadata
The print function can have a side effect of passing "metadata" about objects to
**knitr**, and **knitr** will collect this information as it prints objects. The
motivation of collecting metadata is to store external dependencies of the
objects to be printed. Normally we print an object only to obtain a text
representation, but there are cases that can be more complicated. For example, a
[**ggvis**](https://ggvis.rstudio.com/) graph requires external JavaScript and
CSS dependencies such as `ggvis.js`. The graph itself is basically a fragment of
JavaScript code, which will not work unless the required libraries are loaded
(in the HTML header). Therefore we need to collect the dependencies of an object
beside printing the object itself.
One way to specify the dependencies is through the `meta` argument of
`asis_output()`. Here is a pseudo example:
```{r eval=FALSE, tidy=FALSE}
# pseudo code
knit_print.ggvis = function(x, ...) {
res = ggvis::print_this_object(x)
knitr::asis_output(res, meta = list(
ggvis = list(
version = '0.1.0',
js = system.file('www', 'js', 'ggvis.js', package = 'ggvis'),
css = system.file('www', 'www', 'ggvis.css', package = 'ggvis')
)
))
}
```
Then when **knitr** prints a **ggvis** object, the `meta` information will be
collected and stored. After knitting is done, we can obtain a list of all the
dependencies via `knit_meta()`. It is very likely that there are duplicate
entries in the list, and it is up to the package authors to clean them up, and
process the metadata list in their own way (e.g. write the dependencies into the
HTML header). We give a few more quick and dirty examples below to see how
`knit_meta()` works.
Now we define a print method for `foo` objects:
```{r tidy=FALSE}
library(knitr)
knit_print.foo = function(x, ...) {
res = paste('> **This is a `foo` object**:', x)
asis_output(res, meta = list(
js = system.file('www', 'shared', 'shiny.js', package = 'shiny'),
css = system.file('www', 'shared', 'shiny.css', package = 'shiny')
))
}
```
See what happens when we print `foo` objects:
```{r}
new_foo = function(x) structure(x, class = 'foo')
new_foo('hello')
```
Check the metadata now:
```{r}
str(knit_meta(clean = FALSE))
```
Another `foo` object:
```{r}
new_foo('world')
```
Similarly for `bar` objects:
```{r}
knit_print.bar = function(x, ...) {
asis_output(x, meta = list(head = '<script>console.log("bar!")</script>'))
}
new_bar = function(x) structure(x, class = 'bar')
new_bar('> **hello** world!')
new_bar('> hello **world**!')
```
The final version of the metadata, and clean it up:
```{r}
str(knit_meta())
str(knit_meta()) # empty now, because clean = TRUE by default
```
### For package authors
If you are implementing a custom print method in your own package, here are two
tips:
1. With R \>= 3.6.0 (2019-04-26), you can declare the S3 method in the package
`NAMESPACE` with `S3method(knitr::knit_print, class)`. If you use
**roxygen2** package, that means a roxygen comment like this:
``` r
#' @exportS3Method knitr::knit_print
knit_print.class <-
```
With this method, you do not need to import **knitr** to your package, i.e.,
**knitr** can be listed in `Suggests` and not necessarily `Imports` in the
package `DESCRIPTION`. The S3 methods will be automatically registered when
**knitr** is actually loaded.
For R \< 3.6.0, you need to import `knit_print` in your package namespace
via `importFrom(knitr, knit_print)` (or roxygen:
`#' @importFrom knitr knit_print`) (see [the **printr**
package](https://github.com/yihui/printr) for an example).
2. `asis_output()` is simply a function that marks an object with the class
`knit_asis`, and you do not have to import this function to your package,
either---just let your print method return
`structure(x, class = 'knit_asis')`, and if there are additional metadata,
just put it in the `knit_meta` attribute; here is the source code of this
function:
```{r}
knitr::asis_output
```
You may put **knitr** in the `Suggests` field in `DESCRIPTION`, and use
`knitr::asis_output()`, so that you can avoid the "hard" dependency on
**knitr**.
```{r clean-up, include=FALSE}
# R compiles all vignettes in the same session, which can be bad
rm(list = ls(all = TRUE))
```
|