While functionality of pander and knitr overlap in report generation, we have the feeling that the best way to use all power of R/knitr/pander for report generation is to utilize them together. This short vignette aims to explain how to embed output of pander in reports generated by knitr. If you are not aware of what knitr is, be sure to check out project’s homepage which contains extensive documentation and examples.

One of the most useful feature of knitr is the ability to convert tables to output format on the fly. For example:

head(iris)
#>   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
#> 1          5.1         3.5          1.4         0.2  setosa
#> 2          4.9         3.0          1.4         0.2  setosa
#> 3          4.7         3.2          1.3         0.2  setosa
#> 4          4.6         3.1          1.5         0.2  setosa
#> 5          5.0         3.6          1.4         0.2  setosa
#> 6          5.4         3.9          1.7         0.4  setosa
knitr::kable(head(iris))
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
5.1 3.5 1.4 0.2 setosa
4.9 3.0 1.4 0.2 setosa
4.7 3.2 1.3 0.2 setosa
4.6 3.1 1.5 0.2 setosa
5.0 3.6 1.4 0.2 setosa
5.4 3.9 1.7 0.4 setosa

However, kable table generator is simple by design, and does not capture all the variety of classes that R has to offer. For example, CrossTable and tabular is not supported:

library(descr, quietly = TRUE)
ct <- CrossTable(mtcars$gear, mtcars$cyl)
#> Warning in chisq.test(tab, correct = FALSE, ...): Chi-squared approximation
#> may be incorrect
knitr::kable(ct)
#> Error in as.data.frame.default(x): cannot coerce class ""CrossTable"" to a data.frame
library(tables, quietly = TRUE)
#> 
#> Attaching package: 'Hmisc'
#> 
#> The following objects are masked from 'package:base':
#> 
#>     format.pval, round.POSIXt, trunc.POSIXt, units
tab <- tabular( (Species + 1) ~ (n=1) + Format(digits=2)*
         (Sepal.Length + Sepal.Width)*(mean + sd), data=iris )
knitr::kable(tab)
#> Error in `colnames<-`(`*tmp*`, value = c("term", "term", "term", "term", : length of 'dimnames' [2] not equal to array extent

This is where pander comes handy. pander support rendering for many popular classes:

methods(pander)
#>  [1] pander.anova*           pander.aov*            
#>  [3] pander.aovlist*         pander.Arima*          
#>  [5] pander.call*            pander.cast_df*        
#>  [7] pander.character*       pander.clogit*         
#>  [9] pander.coxph*           pander.cph*            
#> [11] pander.CrossTable*      pander.data.frame*     
#> [13] pander.Date*            pander.default*        
#> [15] pander.density*         pander.describe*       
#> [17] pander.evals*           pander.factor*         
#> [19] pander.formula*         pander.ftable*         
#> [21] pander.function*        pander.glm*            
#> [23] pander.Glm*             pander.gtable*         
#> [25] pander.htest*           pander.image*          
#> [27] pander.irts*            pander.list*           
#> [29] pander.lm*              pander.lme*            
#> [31] pander.logical*         pander.lrm*            
#> [33] pander.manova*          pander.matrix*         
#> [35] pander.microbenchmark*  pander.mtable*         
#> [37] pander.name*            pander.nls*            
#> [39] pander.NULL*            pander.numeric*        
#> [41] pander.ols*             pander.orm*            
#> [43] pander.polr*            pander.POSIXct*        
#> [45] pander.POSIXlt*         pander.prcomp*         
#> [47] pander.randomForest*    pander.rapport*        
#> [49] pander.rlm*             pander.sessionInfo*    
#> [51] pander.smooth.spline*   pander.stat.table*     
#> [53] pander.summary.aov*     pander.summary.aovlist*
#> [55] pander.summary.glm*     pander.summary.lm*     
#> [57] pander.summary.lme*     pander.summary.manova* 
#> [59] pander.summary.nls*     pander.summary.polr*   
#> [61] pander.summary.prcomp*  pander.summary.rms*    
#> [63] pander.summary.survreg* pander.summary.table*  
#> [65] pander.survdiff*        pander.survfit*        
#> [67] pander.survreg*         pander.table*          
#> [69] pander.tabular*         pander.ts*             
#> [71] pander.zoo*            
#> see '?methods' for accessing help and source code

And it’s integrated with knitr by default. pander simply identifies if knitr is running in the backgorund, and if so, it capture.output and return the resulting string as an knit_asis object, so that you do not need to specify the results='asis' option in your knitr chunk:

library(descr, quietly = TRUE)
pander(CrossTable(mtcars$gear, mtcars$cyl))
#> Warning in chisq.test(tab, correct = FALSE, ...): Chi-squared approximation
#> may be incorrect
 
mtcars$gear
mtcars$cyl
4
 
6
 
8
 
Total
3
N
Chi-square
Row(%)
Column(%)
Total(%)
 
1
3.3502
6.6667%
9.0909%
3.125%
 
2
0.5003
13.3333%
28.5714%
6.250%
 
12
4.5054
80.0000%
85.7143%
37.500%
 
15

46.8750%

4
N
Chi-square
Row(%)
Column(%)
Total(%)
 
8
3.6402
66.6667%
72.7273%
25.000%
 
4
0.7202
33.3333%
57.1429%
12.500%
 
0
5.2500
0.0000%
0.0000%
0.000%
 
12

37.5000%

5
N
Chi-square
Row(%)
Column(%)
Total(%)
 
2
0.0460
40.0000%
18.1818%
6.250%
 
1
0.0080
20.0000%
14.2857%
3.125%
 
2
0.0161
40.0000%
14.2857%
6.250%
 
5

15.6250%

Total
11
34.375%
7
21.875%
14
43.75%
32
library(tables, quietly = TRUE)
tab <- tabular( (Species + 1) ~ (n=1) + Format(digits=2)*
         (Sepal.Length + Sepal.Width)*(mean + sd), data=iris )
pander(tab)

Species

n
Sepal.Length
mean

sd
Sepal.Width
mean

sd
setosa 50 5.01 0.35 3.43 0.38
versicolor 50 5.94 0.52 2.77 0.31
virginica 50 6.59 0.64 2.97 0.32
All 150 5.84 0.83 3.06 0.44

In a nutshell, this is achieved by modification that whenever you call pander inside of a knitr document, instead of returning the markdown text to the standard output (as it used to happen), pander returns a knit_asis class object, which renders fine in the resulting document — without the double comment chars, so rendering the tables in HTML, pdf or other document formats just fine.

If by any chance you actually want results of pander not to be converted automatically, just specify knitr.auto.asis to FALSE either using panderOptions:

panderOptions('knitr.auto.asis', FALSE)
pander(head(iris))
#> 
#> -------------------------------------------------------------------
#>  Sepal.Length   Sepal.Width   Petal.Length   Petal.Width   Species 
#> -------------- ------------- -------------- ------------- ---------
#>      5.1            3.5           1.4            0.2       setosa  
#> 
#>      4.9             3            1.4            0.2       setosa  
#> 
#>      4.7            3.2           1.3            0.2       setosa  
#> 
#>      4.6            3.1           1.5            0.2       setosa  
#> 
#>       5             3.6           1.4            0.2       setosa  
#> 
#>      5.4            3.9           1.7            0.4       setosa  
#> -------------------------------------------------------------------
panderOptions('knitr.auto.asis', TRUE)

Rendering markdown inside loop/vectorized function

One question that is being asked a lot is how to use pander with knitr in a loop or with vectorized function. For example we have 3 tables that we want to render and we want to do it using lapply:

dfs <- list(mtcars[1:3, 1:4], mtcars[4:6, 1:4], mtcars[7:9, 1:4])
lapply(dfs, pander)
#> [[1]]
#> [1] "\n-------------------------------------------\n      &nbsp;         mpg   cyl   disp   hp \n------------------- ----- ----- ------ ----\n   **Mazda RX4**     21     6    160   110 \n\n **Mazda RX4 Wag**   21     6    160   110 \n\n  **Datsun 710**    22.8    4    108    93 \n-------------------------------------------\n\n"
#> attr(,"class")
#> [1] "knit_asis"
#> attr(,"knit_cacheable")
#> [1] TRUE
#> 
#> [[2]]
#> [1] "\n-----------------------------------------------\n        &nbsp;           mpg   cyl   disp   hp \n----------------------- ----- ----- ------ ----\n  **Hornet 4 Drive**    21.4    6    258   110 \n\n **Hornet Sportabout**  18.7    8    360   175 \n\n      **Valiant**       18.1    6    225   105 \n-----------------------------------------------\n\n"
#> attr(,"class")
#> [1] "knit_asis"
#> attr(,"knit_cacheable")
#> [1] TRUE
#> 
#> [[3]]
#> [1] "\n----------------------------------------\n     &nbsp;       mpg   cyl   disp   hp \n---------------- ----- ----- ------ ----\n **Duster 360**  14.3    8    360   245 \n\n **Merc 240D**   24.4    4   146.7   62 \n\n  **Merc 230**   22.8    4   140.8   95 \n----------------------------------------\n\n"
#> attr(,"class")
#> [1] "knit_asis"
#> attr(,"knit_cacheable")
#> [1] TRUE

As you can see, this doesn’t work correctly, due to fact that when run inside knitr, pander tries to return knit_asis class object, but for loops/vectorized functions this results in incorrect output.

Recommended way to solve this is to disable pander trying to return knit_asis class object by setting knitr.auto.asis to FALSE using panderOptions. However, in that case to we also need to tell knitr to convert table on the fly by specifying results='asis' for knitr chunk:

panderOptions('knitr.auto.asis', FALSE)
dfs <- list(mtcars[1:3, 1:4], mtcars[4:6, 1:4], mtcars[7:9, 1:4])
invisible(lapply(dfs, pander))
  mpg cyl disp hp
Mazda RX4 21 6 160 110
Mazda RX4 Wag 21 6 160 110
Datsun 710 22.8 4 108 93
  mpg cyl disp hp
Hornet 4 Drive 21.4 6 258 110
Hornet Sportabout 18.7 8 360 175
Valiant 18.1 6 225 105
  mpg cyl disp hp
Duster 360 14.3 8 360 245
Merc 240D 24.4 4 146.7 62
Merc 230 22.8 4 140.8 95