library(tidyverse)
library(jsonlite)
<- function(df){
send_df_to_js cat(
paste(
'<script>
var data = ',toJSON(df),';
</script>'
sep="")
,
) }
Custom JavaScript visualizations in RMarkdown
I happened to stumble upon the preview release page for RStudio recently and noticed something that made me exorbitantly happy.
A preview release of RStudio v1.0.136 is now available for testing and feedback. This release includes… Support for JavaScript and CSS chunks in R Notebooks…
“Support for JavaScript and CSS chunks in R Notebooks”! As someone who loves using javascript for plotting (and secretly for manipulating) data this is massively exciting. Previously my workflow for generating an interactive graphic would be something like:
- Download data
- Clean data and do preliminary visualization in R
- Export what I liked to a csv file and make a new directory with a set of
.html, .js, .css
files in it. - Load data into javascript/d3.js in the new javascript file.
- Plot.
This workflow has served me very well, I did it probably 20 times a day when working at the New York Times and I am very fast with it. If you want to just make a stand alone visualization it’s fantastic, however, as a biostatistician who works with a lot of other biostatisticians, people tend to want to see where the data comes from.
With Javascript chunks in .Rmd
files you can explicitly show the code for data gathering/cleaning etc. in a language that your collaborators can understand, along with making custom d3 charts with that data. All without ever leaving RStudio. I will show you my quick and dirty solution to doing so.
Getting data into Javascript
Instead of generating a csv file in R and then loading that into javascript we will instead send the data directly through the html to javascript. (Note: This wont work well with super large data).
Inspired by this medium post I wrote a little function that takes a dataframe and sends it to the html document in the .json
format.
To illustrate how it works we will generate some random data into a dataframe.
#Generate some random x and y data to plot
<- 300
n <- data_frame(x = runif(n)*10) %>%
random_data mutate(y = 0.5*x^3 - 1.3*x^2 + rnorm(n, mean = 0, sd = 80),
group = paste("group", sample(c(1,2,3), n, replace = T)))
Warning: `data_frame()` was deprecated in tibble 1.1.0.
ℹ Please use `tibble()` instead.
Now we send a snippit of the dataframe into the function to see the output…
%>%
random_data head() %>%
send_df_to_js()
<script>
var data = [{"x":8.324,"y":124.6508,"group":"group 1"},{"x":7.2886,"y":22.3722,"group":"group 1"},{"x":3.0761,"y":113.6496,"group":"group 2"},{"x":2.9031,"y":102.2687,"group":"group 3"},{"x":9.6126,"y":431.5504,"group":"group 1"},{"x":7.199,"y":146.1856,"group":"group 3"}];
</script>
Beautiful, we have our data, in json
format, wrapped in a script tag. Now we can send the whole dataframe through. This time I am using the results = "asis"
option in the code chunk ({r, results = "asis"}
), to write the results directly to the html document and not to the output like we did above.
#Initiate data transfer protocol one
send_df_to_js(random_data)
Now our data is inside our page’s javascript scope and ready to be played with!
Drawing pretty pictures
Let’s make a super simple d3 scatter plot to see this randomly generated data. All I have to do is include my desired javascript libraries, make a div
element for my visualization to go into and then put my {js}
block in. RMarkdown will slot the javascript into the page and we’re good to go.
In this example I did…
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id = "viz"></div>
` ` `{js}
//code goes here
` ` `
Did it work?
var point_vals = d3.select("#viz")
.append("p")
.attr("align", "center")
.text("Mouseover some data!");
//Get how wide our page is in pixels so we can draw our plot in it
var page_width = $("#did-it-work").width();
// set the dimensions and margins of the graph
var margin = 30,
= page_width - 2*margin,
width = page_width*0.8 - 2*margin;
height
// Find max data values
var x_extent = d3.extent(data, d => d.x);
var y_extent = d3.extent(data, d => d.y);
// Set the scales
var x = d3.scaleLinear()
.domain(x_extent)
.range([0, width]);
var y = d3.scaleLinear()
.domain(y_extent)
.range([height, 0]);
//Set up our SVG element
var svg = d3.select("#viz").append("svg")
.attr("width", width + 2*margin)
.attr("height", height + 2*margin)
.append("g")
.attr("transform",
"translate(" + margin + "," + margin + ")");
var bounce_select = d3.transition()
.duration(1000)
.ease(d3.easeElastic.period(0.4));
// Add the scatterplot
.selectAll(".dots")
svg.data(data)
.enter().append("circle")
.attr("class", "dots")
.attr("fill", d => d.group === "group 1"? "steelblue":"orangered")
.attr("fill-opacity", 0.3)
.attr("r", 5)
.attr("cx", d => x(d.x) )
.attr("cy", d => y(d.y) )
.on("mouseover", function(d){
.selectAll(".dots").attr("r", 5) //make sure all the dots are small
d3.select(this)
d3.transition(bounce_select)
.attr("r", 10);
.text("X:" + d.x + " Y:" + d.y) //change the title of the graph to the datapoint
point_vals;
})
// Draw the axes
// Add the X Axis
.append("g")
svg.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add the Y Axis
.append("g")
svg.call(d3.axisLeft(y));
Why/ When
This is a bad example of a visualization for this scenario as something like plotly could do this in much less effort. If you’re doing something more complicated/ bespoke then this is a great resource to have.
Addendum
If you’re interested, here’s the javascript code I included to make the above graph.
var point_vals = d3.select("#viz").append("p").text("Mouseover some data!");
//Get how wide our page is in pixels so we can draw our plot in it
var page_width = $("#did-it-work").width();
// set the dimensions and margins of the graph
var margin = 20,
width = page_width - 2*margin,
height = page_width*0.8 - 2*margin;
// Find max data values
var x_extent = d3.extent(data, d => d.x);
var y_extent = d3.extent(data, d => d.y);
// Set the scales
var x = d3.scaleLinear()
.domain(x_extent)
.range([0, width]);
var y = d3.scaleLinear()
.domain(y_extent)
.range([height, 0]);
//Set up our SVG element
var svg = d3.select("#viz").append("svg")
.attr("width", width + 2*margin)
.attr("height", height + 2*margin)
.append("g")
.attr("transform",
"translate(" + margin + "," + margin + ")");
var bounce_select = d3.transition()
.duration(1000)
.ease(d3.easeElastic.period(0.4));
// Add the scatterplot
svg.selectAll(".dots")
.data(data)
.enter().append("circle")
.attr("class", "dots")
.attr("fill", d => d.group === "group 1"? "steelblue":"orangered")
.attr("fill-opacity", 0.3)
.attr("r", 5)
.attr("cx", d => x(d.x) )
.attr("cy", d => y(d.y) )
.on("mouseover", function(d){
d3.selectAll(".dots").attr("r", 5) //make sure all the dots are small
d3.select(this)
.transition(bounce_select)
.attr("r", 10);
point_vals.text("X:" + d.x + " Y:" + d.y) //change the title of the graph to the datapoint
});
// Draw the axes
// Add the X Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add the Y Axis
svg.append("g")
.call(d3.axisLeft(y));