Site icon R-bloggers

Using htmlwidgets with knitr and Jekyll

[This article was first published on Brendan Rocks >> R, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.


A few weeks ago I gave a talk at BARUG (and wrote a post) about blogging with the excellent knitr-jekyll repo. Yihui’s system is fantastic, but it does have one drawback: None of those fancy new htmlwidgets packages seem to work…

A few people have run into this. I recently figured out how to fix it for this blog (which required a bit of time reading through the rmarkdown source), so I thought I’d write it up in case it helps anyone else, or my future-self.

TL;DR

You can add a line to build.R which calls a small wrapper function I cobbled together (brocks::htmlwidgts_deps), add a snippet of liquid syntax to ./_layouts/post.html, and you’re away.

What’s going on?

Often, when you ‘knit’ an .Rmd file to html, (perhaps without knowing it) you’re doing it via the rmarkdown package, which adds its own invisible magic to the process. Behind the scenes, rmarkdown uses knitr to convert the file to markdown format, and then uses pandoc to convert the markdown to HTML.

While knitr executes R code and embeds results, htmlwidgets packages (such as leaflet, DiagrammR, threejs, and metricsgraphics) also have js and css dependencies. These are handled by rmarkdown’s second step, and so don’t get included when using knitr alone.

The rmarkdown invisible magic works as follows:

A fix: htmlwdigets_deps

Happily, including bits of HTML in other bits of HTML is one of Jekyll’s strengths, and it’s possible to high-jack the internals of rmarkdown to do something appropriate. I did this with a little function htmlwdigets_deps, which:

With a small tweak to the post.html file, Jekyll’s liquid templating system can be used to pull in that little HTML file, if htmlwidgets are detected in your post.

If you’re using knitr-jekyll, all that’s needed to make everything work as you’d expect, is to call the function from your build.R file, like so:

local({
  # Your existing configurations...
  # See https://github.com/yihui/knitr-jekyll/blob/gh-pages/build.R
  brocks::htmlwidgets_deps(a)
})

(The parameter a refers to the input file — if you’re using a build file anything like Yihui’s example, this will work fine.)

If you’d like to have a look at the internals of htmlwidgets_deps yourself, it’s in my personal package up on GitHub. Long story short, it hi-jacks rmarkdown:::html_dependencies_as_string. The rest of this post walks through what it actually does.

1. Copying dependencies to your site

To keep things transparent, the dependency source files are kept in their own folder (./htmlwidgets_deps). If it doesn’t exist, it’ll be created. This behaviour is different to the rmarkdown default of compressing everything into huge in-line data:uri blobs. While that works great for keeping everything in one big self-contained file (e.g. to email to someone), it makes for a very slow web page. For a blog, having separate files is preferable, as it allows the browser to load files asynchronously, reducing the load time.

After compiling your sites, if you’ve used htmlwidgets you’ll have an extra directory within your blog, containing the source for all the dependencies, a bit like this:

- _includes
- _layouts
- _posts
- _sass
- _site
- _source
- js/
- css/
- htmlwidgets_deps/
    - d3-3.5.3/
        - LICENCE
        - bower.json
        - d3.js
        - d3.min.js
    - jquery-1.11.1
        - AUTHORS.txt
        - jquery.min.js
    - ...
- ...

2. Writing the extra HTML

Once all the dependencies are ready to be served from your site, you still need to add HTML pointers to your blog post, so that it knows where to find them. htmlwidgets_deps automates this, by adding a file for each htmlwidgets post to the ./_includes directory (which is where Jekyll goes to look for HTML files to include). For each post which requires it, the extra HTML file will be generated in the htmlwidgets sub-directory, like this:

- _includes/
    - htmlwidgets/
        - my-new-htmlwidgets-post.html
    - footer.html
    - head.html
    - header.html
- _layouts/
...

The file itself if pretty simple. Here’s an example:

<script src="{{ "/htmlwidgets_deps/htmlwidgets-0.5/htmlwidgets.js" | prepend: site.baseurl }}"></script>
<script src="{{ "/htmlwidgets_deps/jquery-2.1.3/dist/jquery.min.js" | prepend: site.baseurl }}"></script>
<script src="{{ "/htmlwidgets_deps/d3-3.5.3/d3.min.js" | prepend: site.baseurl }}"></script>
<link href="{{ "/htmlwidgets_deps/metrics-graphics-2.1.0/dist/metricsgraphics.css" | prepend: site.baseurl }}" rel="stylesheet" />
<script src="{{ "/htmlwidgets_deps/metrics-graphics-2.1.0/dist/metricsgraphics.min.js" | prepend: site.baseurl }}"></script>
<script src="{{ "/htmlwidgets_deps/metricsgraphics-binding-0.8.5/metricsgraphics.js" | prepend: site.baseurl }}"></script>

The HTML comes pre-wrapped in the usual liquid syntax.

3. Including the extra HTML

Now you have a little file to include, you just need to get it into the HTML of the blog post. Jekyll’s templating system liquid is all about doing this.

Because htmlwdigets_deps gives the dependency file the same name as your .Rmd input (and thus the post), it’s quite easy to write a short {% include %} statement, based on the name of the page itself. However, things get tricky if the file doesn’t exist. By default, htmlwdigets_deps only produces files when necessary (e.g. when you are actually using htmlwidgets). To handle this, I used a plugin, providing the file_exists function.

Adding the following the bottom of ./_layouts/default.html did the trick. You could also use ./_layouts/post.html if you wanted to. It’s a good idea to put it towards to the bottom, otherwise the page won’t load until all the htmlwdigets dependencies are loaded, which could make things feel rather slow.

<!-- htmlwidgets dependencies --> 
{% assign dep_file = page.url | replace_regex:'/$','.html' |
   prepend : 'htmlwidgets' %}
{% assign dep_file_inc = dep_file | prepend : '_includes/' %}
{% capture hw_used %}{% file_exists {{ dep_file_inc }} %}{% endcapture %}

{% if hw_used == "true" %}
{% include {{dep_file}} %}
{% endif %}

With GitHub Pages

The solution above proves a little tricky if you’re using GitHub pages, as this doesn’t allow plugins. While I’m sure an expert with the liquid templating engine could come up with a brilliant solution to this, in lieu, I present a filthy untested hack.

By setting the htmlwdigets_deps parameter always = TRUE, a dependencies file will always be produced, even if there’s no htmlwidgets detected (the file will be empty). This means that you can do-away with the logic part (and the plugin), and simply add the lines:

<!-- htmlwidgets dependencies --> 
{% assign dep_file = page.url | replace_regex:'/$','.html' |
   prepend : 'htmlwidgets' %}
{% include {{dep_file}} %}

The disadvantage is that you’ll end up with some empty HTML files in ./_includes/htmlwidgets/, which may or may not bother you. If you’re only going to be using htmlwidgets for blog posts (and not the rest of your site) I’d recommend doing this for the ./_layouts/post.html file, (as opposed to default.html) so that other pages don’t have trouble finding dependencies they don’t need.

If you give this a crack, let me know!

How to do the same

In summary:

brocks::htmlwidgets_deps(a)

And you should be done!

Showing Off

After all that, it would be a shame not to show off some interactive visualisations. Here are some of the htmlwidgets packages I’ve had the chance to muck about with so far.

MetricsGraphics

MetricsGraphics.js is a JavaScript API, built on top of d3.js, which allows you to produce a lot of common plots very quickly (without having to start from scratch each time). There’s a few libs like this, but MetricsGraphics is especially pleasing. Huge thanks to Ali Almossawi and Mozilla, and also to Bob Rudis for the R interface.

library(metricsgraphics)

plots <- lapply(1:4, function(x) {
  mjs_plot(rbeta(1000, x, x), width = 300, height = 300, linked = TRUE) %>%
    mjs_histogram(bar_margin = 2) %>%
    mjs_labs(x_label = sprintf("Plot %d", x))
})

mjs_grid(plots)
< !--html_preserve-->
< !--/html_preserve-->

leaflet

leaflet.js allows you to create beautiful, mobile-friendly maps (based on OpenStreetMap data), incredibly easily. Hat tip to Vladimir Agafonkin, and Joe Cheng et al for the R interface!

Here’s the Pride of Spitalfields, which I occasionally pine for, from beneath the palm trees of sunny California.

library(leaflet)

m <- leaflet() %>%
  addTiles() %>%  # Add default OpenStreetMap map tiles
  addMarkers(lng = -0.07125, lat = 51.51895, 
             popup = "Reasonably Priced Stella Artois")
m
< !--html_preserve-->
< !--/html_preserve-->

threejs

three.js is a gobsmackingly brilliant library for creating animated, interactive 3D graphics from within a Web browser. Here’s an interactive 3D globe with the world’s populations mapped as, erm, light-sabers. Probably not as informative as a base graphics plot, but it is much more Bond villianish. Drag it around and have a zoom!

library("threejs")
library("maps")
## 
##  # ATTENTION: maps v3.0 has an updated 'world' map.        #
##  # Many country borders and names have changed since 1990. #
##  # Type '?world' or 'news(package="maps")'. See README_v3. #
data(world.cities, package = "maps")
cities <- world.cities[order(world.cities$pop,decreasing = TRUE)[1:1000],]
value  <- 100 * cities$pop / max(cities$pop)

# Set up a data color map and plot
col <- rainbow(10, start = 2.8 / 6, end = 3.4 / 6)
col <- col[floor(length(col) * (100 - value) / 100) + 1]
globejs(lat = cities$lat, long = cities$long, value = value, color = col,
        atmosphere = TRUE)
< !--html_preserve-->

To leave a comment for the author, please follow the link and comment on their blog: Brendan Rocks >> R.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.