Beforeknitr v1.6, printing objects in R code chunks basically emulates theR console. For example, a data frame is printed like this1:
head(mtcars) mpg cyl disp hp drat wt qsec vs am gear carbMazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2Valiant 18.1 6 225 105 2.76 3.460 20.22 1 0 3 1The text representation of the data frame above may look very familiar with mostR users, but for reporting purposes, it may not be satisfactory – often timeswe want to see a table representation instead. That is the problem that thechunk optionrender and the S3 generic functionknit_print() try to solve.
After we evaluate each R expression in a code chunk, there is an objectreturned. For example,1 + 1 returns2. This object is passed to the chunkoptionrender, which is a function with two arguments,x andoptions, orx and.... The default value for therender option isknit_print, an S3function inknitr:
library(knitr)knit_print # an S3 generic function## function (x, ...) ## {## if (need_screenshot(x, ...)) {## html_screenshot(x)## }## else {## UseMethod("knit_print")## }## }## <bytecode: 0x1280ee4f0>## <environment: namespace:knitr>methods(knit_print)## [1] knit_print.default* knit_print.knit_asis* ## [3] knit_print.knit_asis_url* knit_print.knitr_kable* ## see '?methods' for accessing help and source codegetS3method('knit_print', 'default') # the default method## function (x, ..., inline = FALSE) ## {## if (inline) ## x## else normal_print(x)## }## <bytecode: 0x11f6bbfa8>## <environment: namespace:knitr>normal_print## function (x, ...) ## {## if (isS4(x)) ## methods::show(x)## else print(x)## }## <bytecode: 0x11f645640>## <environment: namespace:knitr>As we can see,knit_print() has adefault method, which is basicallyprint() orshow(), depending on whether the object is an S4 object. Thismeans it does nothing special when printing R objects:
knit_print(1:10)## [1] 1 2 3 4 5 6 7 8 9 10knit_print(head(mtcars))## mpg cyl disp hp drat wt qsec vs am gear carb## Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4## Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4## Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1## Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1## Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2## Valiant 18.1 6 225 105 2.76 3.460 20.22 1 0 3 1S3 generic functions are extensible in the sense that we can define custommethods for them. A methodknit_print.foo() will be applied to the object thathas the classfoo. Here is quick example of how we can print data frames astables:
library(knitr)# define a method for objects of the class data.frameknit_print.data.frame = function(x, ...) { res = paste(c('', '', kable(x)), collapse = '\n') asis_output(res)}# register the methodregisterS3method("knit_print", "data.frame", knit_print.data.frame)If you define a method in a code chunk in aknitr document, the call toregisterS3method() will be necessary for R >= 3.5.0, because the S3 dispatchmechanism has changed since R 3.5.0. If you are developing an R package, see thesection [For package authors] below.
We expect the print method to return a character vector, or an object that canbe coerced into a character vector. In the example above, thekable() functionreturns a character vector, which we pass to theasis_output() function sothat laterknitr knows that this result needs no special treatment (justwrite it as is), otherwise it depends on the chunk optionresults (= 'asis'/'markup' /'hide') how a normal character vector should be written. Thefunctionasis_output() has the same effect asresults = 'asis', but saves usthe effort to provide this chunk option explicitly. Now we check how theprinting behavior is changed. We print a number, a character vector, a list, adata frame, and write a character value usingcat() in the chunk below:
1 + 1## [1] 2head(letters)## [1] "a" "b" "c" "d" "e" "f"list(a = 1, b = 9:4)## $a## [1] 1## ## $b## [1] 9 8 7 6 5 4head(mtcars)| mpg | cyl | disp | hp | drat | wt | qsec | vs | am | gear | carb | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Mazda RX4 | 21.0 | 6 | 160 | 110 | 3.90 | 2.620 | 16.46 | 0 | 1 | 4 | 4 |
| Mazda RX4 Wag | 21.0 | 6 | 160 | 110 | 3.90 | 2.875 | 17.02 | 0 | 1 | 4 | 4 |
| Datsun 710 | 22.8 | 4 | 108 | 93 | 3.85 | 2.320 | 18.61 | 1 | 1 | 4 | 1 |
| Hornet 4 Drive | 21.4 | 6 | 258 | 110 | 3.08 | 3.215 | 19.44 | 1 | 0 | 3 | 1 |
| Hornet Sportabout | 18.7 | 8 | 360 | 175 | 3.15 | 3.440 | 17.02 | 0 | 0 | 3 | 2 |
| Valiant | 18.1 | 6 | 225 | 105 | 2.76 | 3.460 | 20.22 | 1 | 0 | 3 | 1 |
cat('This is cool.')## This is cool.We see all objects except the data frame were printed “normally”2. The dataframe was printed as a real table. Note you do not have to usekable() tocreate tables – there are many other options such asxtable. Just make surethe print method returns a character string.
Theprintr package is a companion toknitr containing printing methods for some common objects like matrices anddata frames. Users only need to load this package to get attractive printedresults. A major factor to consider (which has been considered inprintr)when defining a printing method is the output format. For example, the tablesyntax can be entirely different when the output is LaTeX vs when it isMarkdown.
It is strongly recommended that your S3 method has a... argument, so thatyour method can safely ignore arguments that are passed toknit_print() butnot defined in your method. At the moment, aknit_print() method can have twooptional arguments:
options argument takes a list of the current chunk options;inline argument indicates if the method is called in code chunks orinline R code;Depending on your application, you may optionally use these arguments. Here aresome examples:
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 whenusing your (or another)knit_print() methodinline (if itsupports that), you must not callknit_print() on the object, but just have itreturn. For example, your inline code should readfoo andnot. The latter inline code would yield the methods’result forin-chunk (not inline), because, as set up in the above,knit_print() methods default toinline = FALSE. This default getsoverwritten depending on the context in whichknit_print() is called (inlineor in-chunk), only whenknit_print() is called byknitr (not you) via therender option (see below). You can, of course, always manually set the inlineoptionfoo, but that’s a lot oftyping.
You can skip this section if you do not care about the low-level implementationdetails.
render optionAs mentioned before, the chunk optionrender is a function that defaults toknit_print(). We can certainly use other render functions. For example, wecreate a dummy function that always says “I do not know what to print” no matterwhat objects it receives:
dummy_print = function(x, ...) { cat("I do not know what to print!") # this function implicitly returns an invisible NULL}Now we use the chunk optionrender = dummy_print:
1 + 1## I do not know what to print!head(letters)## I do not know what to print!list(a = 1, b = 9:4)## I do not know what to print!head(mtcars)## I do not know what to print!cat('This is cool.')## This is cool.Note therender function is only applied to visible objects. There are casesin which the objects returned are invisible, e.g. those wrapped ininvisible().
1 + 1## [1] 2invisible(1 + 1)invisible(head(mtcars))x = 1:10 # invisibly returns 1:10The print function can have a side effect of passing “metadata” about objects toknitr, andknitr will collect this information as it prints objects. Themotivation of collecting metadata is to store external dependencies of theobjects to be printed. Normally we print an object only to obtain a textrepresentation, but there are cases that can be more complicated. For example, aggvis graph requires external JavaScript andCSS dependencies such asggvis.js. The graph itself is basically a fragment ofJavaScript code, which will not work unless the required libraries are loaded(in the HTML header). Therefore we need to collect the dependencies of an objectbeside printing the object itself.
One way to specify the dependencies is through themeta argument ofasis_output(). Here is a pseudo example:
# pseudo codeknit_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 whenknitr prints aggvis object, themeta information will becollected and stored. After knitting is done, we can obtain a list of all thedependencies viaknit_meta(). It is very likely that there are duplicateentries in the list, and it is up to the package authors to clean them up, andprocess the metadata list in their own way (e.g. write the dependencies into theHTML header). We give a few more quick and dirty examples below to see howknit_meta() works.
Now we define a print method forfoo objects:
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 printfoo objects:
new_foo = function(x) structure(x, class = 'foo')new_foo('hello')This is a
fooobject: hello
Check the metadata now:
str(knit_meta(clean = FALSE))## List of 2## $ js : chr ""## $ css: chr ""## - attr(*, "knit_meta_id")= chr [1:2] "unnamed-chunk-9" "unnamed-chunk-9"Anotherfoo object:
new_foo('world')This is a
fooobject: world
Similarly forbar objects:
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!')hello world!
new_bar('> hello **world**!')helloworld!
The final version of the metadata, and clean it up:
str(knit_meta())## List of 6## $ js : chr ""## $ css : chr ""## $ js : chr ""## $ css : chr ""## $ head: chr "<script>console.log(\"bar!\")</script>"## $ head: chr "<script>console.log(\"bar!\")</script>"## - attr(*, "knit_meta_id")= chr [1:6] "unnamed-chunk-9" "unnamed-chunk-9" "unnamed-chunk-11" "unnamed-chunk-11" ...str(knit_meta()) # empty now, because clean = TRUE by default## list()If you are implementing a custom print method in your own package, here are twotips:
With R >= 3.6.0 (2019-04-26), you can declare the S3 method in the packageNAMESPACE withS3method(knitr::knit_print, class). If you useroxygen2 package, that means a roxygen comment like this:
#' @exportS3Method knitr::knit_printknit_print.class <-With this method, you do not need to importknitr to your package, i.e.,knitr can be listed inSuggests and not necessarilyImports in thepackageDESCRIPTION. The S3 methods will be automatically registered whenknitr is actually loaded.
For R < 3.6.0, you need to importknit_print in your package namespaceviaimportFrom(knitr, knit_print) (or roxygen:#' @importFrom knitr knit_print) (seetheprintrpackage for an example).
asis_output() is simply a function that marks an object with the classknit_asis, and you do not have to import this function to your package,either—just let your print method returnstructure(x, class = 'knit_asis'), and if there are additional metadata,just put it in theknit_meta attribute; here is the source code of thisfunction:
knitr::asis_output## function (x, meta = NULL, cacheable = NA) ## {## structure(x, class = "knit_asis", knit_meta = meta, knit_cacheable = cacheable)## }## <bytecode: 0x12bf2d4f0>## <environment: namespace:knitr>You may putknitr in theSuggests field inDESCRIPTION, and useknitr::asis_output(), so that you can avoid the “hard” dependency onknitr.