Hello JS in R Markdown

February 10, 2018    R Markdown javascript visualization knitr

One of many rad features of R Markdown is that, despite the name, you can add chunks in languages other than R, including python, bash, Rcpp, and SQL. Until recently, I hadn’t realized that you could also add javascript chunks — on my version of Rstudio, it doesn’t come up as an option on the insert chunk dropdown. Simply replacing the r at the start of the chunk with js will insert the code within the chunk into script tags in the document. As I’m in the process of learning d3 and exploring various javascript visualization libraries, I was curious to try this feature out.

A blog post from Nick Strayer was really helpful for getting started with JS in R Markdown. The main change I wanted to make from the approach in that post was to place the plot below the code, similar to what happens with R code. After figuring out a quick way to do this, I ended up becoming interested in how knitr’s language engines work, and was pleasantly surprised by how accessible the engines are — with a few lines of code you can add a new chunk option to affect the output of a javascript chunk!

In this post I’ll go over the basic of adding javascript code to R Markdown and conclude with a modified javascript knitr engine.

Sending data from R to javascript

If you want to be able to access data from R in a javascript chunk, you’ll need to explicitly pass that data to javascript. Nick’s blog post includes a handy little function for passing a data frame from R to javascript by converting it to JSON and then wrapping it in a <script> tag. I modified this function to enable specifying the variable name and to take advantage of the htmltools tags functionality so that the chunk does not have to be set with option results='asis':

# Function adapted from Nick Strayer's at 
# http://livefreeordichotomize.com/2017/01/24/custom-javascript-visualizations-in-rmarkdown/
df_to_js <- function(x, var_name = "data", ...){
  
  json_data <- jsonlite::toJSON(x, ...)
  
  htmltools::tags$script(paste0("var ",var_name," = ", json_data, ";"))
}

df_to_js(iris)

Importing javascript libraries

To import javascript libraries, you can add script tags to the body of the R markdown in plain html. Here I wanted to make a plot using dimple.js (which also requires d3). To add it in via an R code chunk so that it shows up in a chunk, the htmltools package is again useful:

htmltools::tagList(
  htmltools::tags$script(src = "https://d3js.org/d3.v4.min.js"),
  htmltools::tags$script(src = "https://cdnjs.cloudflare.com/ajax/libs/dimple/2.3.0/dimple.latest.min.js")
)

Making a javascript chunk

A javascript chunk is made in R Markdown by specifying “js” instead of “r” as the language in the chunk header. It can also be useful to set the “class.source” chunk option in the header:

```{js, class.source = "jsvis1"}
// javascript code goes here
```

The “class.source” option will set the class of the the source code chunk. Assigning that chunk a class name makes it easier to insert a ‘div’ from within the javascript to hold your visualization after the source chunk1 so that it appears in the right place in the document (or at least behaves similarly to a plot created by an R chunk).

We’ll use jquery to insert the div:

htmltools::tags$script(src = "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js")

The javascript:

// Create the div that the figure will be made in 
$('<div id="vis"></div>').insertAfter(".jsvis1");

// Make the visualization
var svg = dimple.newSvg("#vis", "100%", 600);
var chart = new dimple.chart(svg, data);
chart.addMeasureAxis("x", "Sepal.Length");
chart.addMeasureAxis("y", "Petal.Width");
chart.addSeries(["Sepal.Length","Petal.Width","Species"], dimple.plot.bubble);
chart.draw();

// Responsive sizing
window.addEventListener('resize', function(event){
  chart.draw(0, true);
});

Woohoo! The figure showed up below the code.

Modifiying the knitr language engine

I was curious if there was a way to add that extra div by specifying a custom option to the knitr chunk. That ended up being easier than I was expecting thanks to the incredible control knitr lets you take over how chunks in different languages are executed.

I took a look at what the default knitr “engine” for javascript was by looking up knitr::knit_engines$get("js"). I edited that to take into consideration an extra chunk option, div. If that argument is present, a div is created in front of the output with that id.

knitr::knit_engines$set("js" = function(options){
    out = if (options$eval) {
        div_pre <- if (!is.null(options$div)) 
          paste0('<div id="',options$div,'"></div>')
        code = c(div_pre,
                '<script type="text/javascript">',
                 options$code,
                 '</script>')
        paste(code, collapse = "\n")
    }
    options$results = "asis"
    knitr::engine_output(options, options$code, out)
})

For the next chunk we set it up like:

```{js, div = "vis2"}
// javascript code goes here
```

With the code:

// Make the visualization
var svg2 = dimple.newSvg("#vis2", "100%", 600);
var chart2 = new dimple.chart(svg2, data);
chart2.addMeasureAxis("x", "Sepal.Width");
chart2.addMeasureAxis("y", "Petal.Length");
chart2.addSeries(["Sepal.Width","Petal.Length","Species"], dimple.plot.bubble);
chart2.draw();

// Responsive sizing
window.addEventListener('resize', function(event){
  chart2.draw(0, true);
});

Yippee!

The downsides of JS in R Markdown

The approach described above for incorporating Javascript in R Markdown works pretty well for incorporating a snippet of javascript into an R Markdown document. However, when editing the document interactively in RStudio, you won’t be able to see the immediate output the way you would for R plots — you can only see the output after knitting the document.


  1. Just moving the ‘div’ element below the chunk doesn’t work, because it won’t exist when the code is first run… Alternatively you could modify the javascript code (either wihtin the chunk or via the knitr engine) to only run after the document has been created.



comments powered by Disqus