widgetframe and knitr

Bhaskar V. Karambelkar

2017-12-19

knitr

The knitr package, along with the rmarkdown package converts a R markdown (Rmd) document into a target document, which can be an HTML Document, a HTML Website, a Dashboard, or a PDF/Word Document etc.

widgetframe is designed to be used in Rmd documents which will eventually be knitted to an HTML or a derived format. There is little or no benefit in using widgetframe::frameWidget() when the output format is not HTML based such as PDF, Microsoft Word etc. In fact widgetframe will most definitely not work for such output formats.

Regular htmlwidgets and knitr

By default when knitting htmlwidgets the htmlwidgets:::knit_print.htmlwidgets() function (an internal function exposed as an S3 method) is called. The output of this call is a list containing the HTML code + a list of HTML dependencies (JS/CSS) required to render the widget. How this is then inserted into the final document depends on the output format. For this discussion we are going to limit to HTML based output formats because as noted above widgetframe is designed to work only for HTML based output formats.

Let’s start with the simplest of HTML output format rmarkdown::html_document(). The self_contained (default = TRUE), and the lib_dir (default = name of document + _files) arguments of this output format dictate how htmlwidgets instances appear in the final document. If self_contained is TRUE then the HTML dependencies (JS/CSS) are all inlined and the result is just one single HTML document. Naturally this document is quite large in size due to the dependencies being inlined. If self_contained is FALSE then the HTML dependencies are kept in a separate directory determined by the lib_dir argument. Other HTML based formats like bookdown, flexdashboard etc. too have similar arguments. This is not surprising as they tend to extend rmarkdown::html_document.

Potential Issues

Unless you are working on small one-off Rmd document, it is never a good idea to have self_contained set to its default value TRUE. It results in large HTML documents which are slow to load in the browser. Secondly by inlining dependencies you lose the ability to share common dependencies across different HTMLs. Lastly you also limit the browser’s ability to cache HTML dependencies across sessions.

Even if you were to externalize the dependencies by setting sefl_contained=FALSE, there are still two potential issues…

These problems typically don’t occur when knitting one-off HTML documents using the rmarkdown::html_document format. But they do occur once you start outputting documents in the bookdown, blogdown, rmarkdown websites, xaringan and other HTML based output formats. And this is where widgetframe comes in.

widgetframe

widgetframe is itself a htmlwidgets instance, but of a different kind. Instead of wrapping a Javascript based dataviz object, it wraps another htmlwidgets instance like leaflet, or DT, or dygraphs etc. It does so by embedding the target htmlwidgets instance in a HTML iframe. It also uses NPR’s pym.js Javascript library to make the iframes responsive. This allows you to easily embed htmlwidgets inside complex HTML documents and avoid the issues mentioned in the section above.

widgetframe and knitr

To understand how the knitting of widgetframes differ from regular htmlwidgets, let’s examine the following code. It shows you how to use widgetframe::fameWidget() function in a RMarkdown document and the three knitr chunk options it supports.

```{r, include=FALSE}

# widgetframe supports 3 custom knitr chunk options...

# For all practicle purposes this should always be FALSE 
knitr::opts_chunk$set(widgetframe_self_contained = FALSE) # default = FALSE

# For all practicle purposes this should always be TRUE 
knitr::opts_chunk$set(widgetframe_isolate_widgets = TRUE) # default = TRUE

# Only needed in bookdown format/s such as bookdown::gitbook. Otherwise don't set.
# knitr::opts_chunk$set(widgetframe_widgets_dir = 'widgets' ) 
```

```{r leaflet-01}
library(leaflet)
library(widgetframe)
l <- leaflet() %>% addTiles() %>% setView(0,0,1)
frameWidget(l, width='90%')
```

While using wigdetframe is as simple as widgetframe::frameWidget(some_other_widget), how the widget is knitted depends on three custom knitr chunk options explained below. When knitr comes across a widgetframe::frameWidget() call in a R Markdown document, it calls widgetframe:::knit_print.widgetframe() function (internal function exposed as a S3 method) to render the widget. This function is a wrapper around the htmlwidgets:::knit_print.htmlwidgets() function of the taget htmlwidgets instance. It is important to know that both knitr_print.widgetframe() and knitr_print.htmlwidgets() functions have no way of knowing what the target output format is and what arguments were passed to the target output format. So in order to knit the target widget in a child HTML document, which is displayed inside an iframe of the parent HTML document, widgetframe needs to depend of the three chunk options shown in the code sample above. We now discuss these 3 options and what their values should be for different output formats.

widgetfram_self_contained

This option is similar to the self_contained argument of the rmarkdown::html_document() output format. The reason we need a separate chucnk option is because …

  1. As explained above knitr_print.widgetframe() has no way of knowing what the output format is and what arguments were passed to it.

  2. Unlike self_contained which defaults to TRUE, widgetframe_self_contained defaults to FALSE. That is to say that by default when using widgetframe the dependencies of the target htmlwidgets instance are kept in a separate directory.

There is very little reason to set this option to TRUE. If however you do set it to TRUE the HTML file for the wrapped widget will have its dependencies inlined, resulting in a single large HTML file. Note that this is not the same as your final output HTML. This is just the HTML for the widget which is displayed inside an iframe in the final HTML document. Wheter the final HTML document has inlined dependencies depends on the self_contaied argument of your output format e.g. rmarkdown::html_document().

widgetframe_isolate_widgets

Defaults to TRUE, and when TRUE, isolates HTML dependencies (CSS/JS) of htmlwidgets of different types. This avoids compatibility issues when htmlwidgets of different types depend on the same dependency(ies) but on different versions. For example if you are using some leaflet, and some DT htmlwdigets in your document (doesn’t matter how many), then the dependencies of leaflet widgets will be stored separately from dependencies of DT widgets and any other widget other than leaflet. So leaflet can depend on version ‘X’ of jQuery and DT can depend on version ‘Y’ and both will work correctly. Note that multiple widgets of the same type will share the same set of dependencies in order to avoid unnecessary duplication. There is very little reason to set this option to FALSE.

widgetframe_widgets_dir

This option, if provided, determines where the HTML code for the wrapped htmlwidgets instance is written. Before discussing more we should note that knitr_print.htmlwidgets() never actually creates any files/directories or touches the file-system in any way. It merely returns a R list which then used by the output format to embed the widget’s HTML (and dependencies) in the final document. However, knitr_print.widgetframe() does create HTML file/s and dependencies directories for the wrapped widget. It needs to, so that the final document can embed the widget’s HTML which is saved separately, inside an iframe. This may not be important to know from a user perspective but a must know, if you want to extend or contribute to widgetframe.

For almost all output formats except bookdown, this option should not be set and it will default to file.path(knitr::opts_chunk$get('fig.path'),'widgets'). That is a widgets directory inside the directory knitr::opts_chunk$get('fig.path'). This is done this way because as noted knitr_print has no way of knowing the arguments of the output format including the final destination directory of the document. However it does have access to the fig.path knitr chunk option. Refer to this Github issue for a discussion around this. See the bookdown section below for a use case where this value needs to be explicitly set.

Another scenario where you may need to set this option is when your RMarkdown output format has self_contained=TRUE (Note: This is the self_contained argument of your HTML output format e.g. rmarkdown::html_document and not the widgetframe_self_contained chunk argument described above).

Chunk Options and Output

To understand how each of the three knitr chunk options affect the wrapped widget’s HTML document, see the code and the table below.

```{r leaflet-01}
library(leaflet)
library(widgetframe)
l <- leaflet() %>% addTiles() %>% setView(0,0,1)
frameWidget(l, width='90%')
```

```{r dygraph-01}
library(dygraphs)
ts <- dygraph(nhtemp, main = "New Haven Temperatures")
frameWidget(ts, height = 350, width = '95%')
```

I’ve ommitted the widgetframe_ prefix for each of the 3 options below to conserve space.

self_contained isolate_widgets widgets_dir Output
FALSE TRUE Not Set Inside widgets directory, widget_leaflet-01.html and widget_dygraph-01.html files and leaflet_libs directory for leaflet and dygraph_libs directory for dygraph dependencies.
FALSE FALSE Not Set Inside widgets directory, widget_leaflet-01.html and widget_dygraph-01.html files and a single libs directory for both leaflet and dygraph dependencies.
TRUE DOESN’T MATTER Not Set Inside widgets directory, two huge widget_leaflet-01.html and widget_dygraph-01.html files with respective dependencies inlined.

†: Default Value.

By default the widget HTML + dependencies (JS/CSS files) are located inside widgets directory, which is created inside wherever knitr::opts_chunk$get('fig.path') points too. However if widgetframe_widgets_dir is set then the widget HTML + dependencies are placed inside a directory whose name is the value of this option, and the path is resolved relative to the current working directory (getwd()) while knitting.

widgetframe and bookdown

For the bookdown output format, we need some additional steps to correctly use widgetframes. This section describes those steps.

  1. The default output directory of the bookdown book is _book, which can be configured via _bookdown.yml file.

  2. For the bookdown::gitbook format we need to explicitly set the widgetframe_widgets_dir to some value (e.g. ‘widgets’), so that the embedded widgets HTML code is saved in this directory instead of the default.

  3. After 'bookdown::render_book("index.Rmd") has been called, we need to move the widgets directory inside the final output directory.

You can easily use a Makefile for this. e.g.

Your _bookdown.yml file

output_dir: "book"

and your Makefile

book: index.Rmd
    Rscript -e 'bookdown::render_book("index.Rmd")'
    mv widgets book/.

and inside index.Rmd you should have something like

```{r, include=FALSE}
knitr::opts_chunk$set(widgetframe_widgets_dir = 'widgets' ) 
```