--- title: "widgetframe and knitr" author: "Bhaskar V. Karambelkar" date: "`r Sys.Date()`" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{widgetframe and knitr} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ## `knitr` The [`knitr`](https://yihui.name/knitr/) package, along with the [`rmarkdown`](http://rmarkdown.rstudio.com/) 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. 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... * Different `htmlwidgets` may depend on different versions of the same Javascript/CSS dependency. But when the final document is produced, only a single version of the said dependency is used. This may cause certain widgets to not display, or work incorrectly. * The styling rules (CSS) of the widget may be overridden by those of the output format. This will again result in the widget not displaying properly in the final document. 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 an HTML [iframe](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe). It also uses NPR's [pym.js](http://blog.apps.npr.org/pym.js/) Javascript library to make the iframes [responsive](https://en.wikipedia.org/wiki/Responsive_web_design). 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 `widgetframe`s 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 ` ``{r, include=FALSE} # widgetframe supports 3 custom knitr chunk options... # For all practical purposes, this should always be FALSE knitr::opts_chunk$set(widgetframe_self_contained = FALSE) # default = FALSE # For all practical 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. Whether the final HTML document has inlined dependencies depends on the `self_contained` 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 an R list which is 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](https://github.com/yihui/knitr/issues/1390) 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 ` ``{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](https://bookdown.org/home/getting-started.html) output format, we need some additional steps to correctly use `widgetframe`s. 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 ```json output_dir: "book" ``` and your `Makefile` ```makefile book: index.Rmd Rscript -e 'bookdown::render_book("index.Rmd")' mv widgets book/. ``` and inside `index.Rmd` you should have something like ```r ` ``{r, include=FALSE} knitr::opts_chunk$set(widgetframe_widgets_dir = 'widgets' ) ` `` ```