D3 and ObservablePlot are awesome data visualization packages. Unfortunately, the packages to work with D3 from R are no longer maintained (R2D3 was last updated in 2021, and the obsplot R package was abandoned in 2023). The challenge is that D3 is still very actively developed. So any new package risks being outpaced by Mike Bostock’s hyper-productivity.
Instead of a new package, I will show how to dynamically load any Javascript library and pass data from R to Javascript. I will use the jrc package to set up the communication channel between R and Javascript1.
I adapted the following code from the D3 gallery:
# I am using R raw strings to avoid conflicting quotation marks
javascript_code <- r"(
// Import the latest D3 version (or whichever you want!)
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
function chart(data, drag){
// Specify the chart's dimensions.
const width = 928;
const height = 600;
// Compute the graph and start the force simulation.
const root = d3.hierarchy(data);
const links = root.links();
const nodes = root.descendants();
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id).distance(0).strength(1))
.force("charge", d3.forceManyBody().strength(-50))
.force("x", d3.forceX())
.force("y", d3.forceY());
// Create the container SVG.
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("style", "max-width: 100%; height: auto;");
// Append links.
const link = svg.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(links)
.join("line");
// Append nodes.
const node = svg.append("g")
.attr("fill", "#fff")
.attr("stroke", "#000")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("fill", d => d.children ? null : "#000")
.attr("stroke", d => d.children ? null : "#fff")
.attr("r", 3.5)
.call(drag(simulation));
node.append("title")
.text(d => d.data.name);
simulation.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
});
return svg.node();
}
function drag(simulation){
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
// The data is hosted at http://const-ae.name/post/2024-12-02_d3_plots_in_r/d3_in_r/flare-2.json
const data = await d3.json("./flare-2.json")
console.log(data)
document.querySelector("#container").append(chart(data, drag));
)"
# Helper function to escape javascript template characters
escape_js_template_str <- function(x){
x <- gsub("`", "\\\\`", x = x)
x <- gsub("\\$\\{", "\\\\${", x = x)
x
}
To get the Javascript into a browser, we will use jrc
.
We first need to open a new page (this should open in Rstudio’s Viewer or in your default browser).
jrc::openPage()
Next, we add an empty div
to the page to which we will dynamically add content. Then we send the javascript code to the website. (We have to wrap the code in a blob and load it with import
so that the code supports ES6 modules.)
jrc::sendHTML("<div id='container'></div>")
jrc::sendCommand(paste0("var module_code = `", escape_js_template_str(javascript_code), "`;\n",
"import(URL.createObjectURL(new Blob([module_code], {type: 'application/javascript'})));"))
This will create the following interactive plot:
Observable Plot
The Observable Plot abstracts away some of the D3 boilerplate code. It is heavily inspired by ggplot2
, so many R programmers will feel right at home.
javascript_code = r"(
import * as Plot from "https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm";
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
const pl_obj = Plot.rectY({length: 10000}, Plot.binX({y: "count"}, {x: d3.randomNormal()})).plot();
document.querySelector("#container").append(pl_obj);
)"
jrc::sendCommand(paste0("var module_code = `", escape_js_template_str(javascript_code), "`;\n",
"import(URL.createObjectURL(new Blob([module_code], {type: 'application/javascript'})));"))
Sending data
The jrc::sendData
function sends a data.frame
from R to Javascript. We will send the mtcars
object, which is internally converted to JSON via jsonlite::toJSON
.
head(mtcars)
## mpg cyl disp hp drat wt qsec vs am gear carb
## Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
## Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
## Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1
## Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1
## Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2
## Valiant 18.1 6 225 105 2.76 3.460 20.22 1 0 3 1
plot(mtcars$mpg, mtcars$cyl, col = as.factor(mtcars$gear > 3))
The string provided as the first argument in sendData
will be used as a global variable name in Javascript.
jrc::sendData("r_mtcars", tibble::remove_rownames(mtcars))
Making a scatter plot in Observable Plot:
javascript_code = r"(
import * as Plot from "https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm";
const pl_obj = Plot.plot({
marks: [
Plot.dot(r_mtcars, {x: "mpg", y: "cyl", fill: (d) => d.gear > 3 ? "red" : "black"})
]
});
document.querySelector("#container").replaceChildren(pl_obj);
)"
jrc::sendCommand(paste0("var module_code = `", escape_js_template_str(javascript_code), "`;\n",
"import(URL.createObjectURL(new Blob([module_code], {type: 'application/javascript'})));"))
Conclusion
With just a few lines of Javascript and with the help of the jrc
package we can make awesome, interactive visualizations.
Session Info
sessionInfo()
## R version 4.4.1 (2024-06-14)
## Platform: aarch64-apple-darwin20
## Running under: macOS Sonoma 14.6
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.0
##
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## time zone: Europe/London
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## loaded via a namespace (and not attached):
## [1] vctrs_0.6.5 cli_3.6.3 knitr_1.49 rlang_1.1.4
## [5] xfun_0.49 stringi_1.8.4 jsonlite_1.8.9 glue_1.8.0
## [9] htmltools_0.5.8.1 sass_0.4.9 hms_1.1.3 fansi_1.0.6
## [13] rmarkdown_2.29 evaluate_1.0.1 jquerylib_0.1.4 tibble_3.2.1
## [17] tzdb_0.4.0 fastmap_1.2.0 yaml_2.3.10 lifecycle_1.0.4
## [21] bookdown_0.41 stringr_1.5.1 compiler_4.4.1 pkgconfig_2.0.3
## [25] rstudioapi_0.17.1 blogdown_1.19 digest_0.6.37 R6_2.5.1
## [29] utf8_1.2.4 readr_2.1.5 pillar_1.9.0 magrittr_2.0.3
## [33] bslib_0.8.0 tools_4.4.1 cachem_1.1.0
Shout-out to my former colleague Svetlana Ovchinnikova for the neat package↩︎