I have graphics working in Vanilla JS WebR, now, and I’ll cover the path to that in two parts.
The intent was to jump straight into ggplot2-land, but, as you saw in my previous post, WASM’d ggplot2 is a bear. And, I really didn’t grok what the WebR site docs were saying about how to deal with the special WebR canvas()
device until I actually tried to work with it and failed miserably.
You will need to have gotten caught up on the previous WebR blog posts and experiments as I’m just covering some of the gnarly bits.
Not Your Parents’ evalR…()
If you’ve either been playing a bit with WebR or peeked under the covers of what others are doing, you’ve seen the evalR…()
family of functions which evaluate supplied R code and optionally return a result. Despite reading the WebR docs on “canvas”, daft me tried to simply use one of those “eval” functions, to no avail.
The solution involves:
I’m going to block quote a key captureR
since it explains “why” pretty well. Hit me up anywhere you like if you desire more info.
Unlike evalR() which only returns one R object, captureR() returns a variable number of objects when R conditions are captured. Since this makes memory management of individual objects unwieldy, captureR() requires the shelter approach to memory management, where all the sheltered objects are destroyed at once.
Let’s work through the “plottR” function I made to avoid repeating code to just get images out of R. It takes, as input:
- an initialized WebR context
- code that will produce something on a graphics device
- dimensions of the image
- the HTML
<canvas>
id
to shove the image data to (we’ll explain this after the code block)
async function plottR(webR, plot_code = "plot(mtcars, col='blue')",
width = 400, height = 400,
id = "base-canvas") {
const webRCodeShelter = await new webR.Shelter();
await webR.evalRVoid(`canvas(width=${width}, height=${height})`);
const result = await webRCodeShelter.captureR(`${plot_code}`, {
withAutoprint: true,
captureStreams: true,
captureConditions: false,
env: webR.objs.emptyEnv,
});
await webR.evalRVoid("dev.off()");
const msgs = await webR.flush();
const canvas = document.getElementById(id)
canvas.setAttribute("width", 2 * width);
canvas.setAttribute("height", 2 * height);
msgs.forEach(msg => {
if (msg.type === "canvasExec") Function(`this.getContext("2d").${msg.data}`).bind(canvas)()
});
}
You 100% need to read up on the HTML canvas element if you’re going to wield WebR yourself vs use Quarto, Shiny, Jupyter-lite, or anything else clever folks come up with. The output of your plots is going to be a series of HTML canvas instructions to do things like “move here”, “switch to this color”, “draw an ellipse”, etc. I will be linking to a full example of the canvas instructions output towards the end.
Now, let’s work through the function’s innards.
const webRCodeShelter = await new webR.Shelter();
gives us a temporary place to execute R code, knowing all the memory consumed will go away after we’re immediately done with it. Unlike the baked-in “global” shelter, this one is super ephemeral.
await webR.evalRVoid(`canvas(width=${width}, height=${height})`);
This is just like a call to png(…)
, svglite(…)
, pdf(…)
, etc. Check out coolbutuseless’ repo for tons of great examples of alternate graphics devices. I have a half-finished one for omnigraffle. They aren’t “hard” to write, but I think they are very tedious to crank through.
const result = await webRCodeShelter.captureR(`${plot_code}`, {
withAutoprint: true,
captureStreams: true,
captureConditions: false,
env: webR.objs.emptyEnv,
});
is different from what you’re used to. The captureR
function will evaluate the given code, and takes some more options, described in the docs. TL;DR: we’re asking the evaluator to give us back pretty much what’d we see in the R console: all console messages and output streams, plus it does the familiar “R object autoprint” that you get for free when you fire up an R console.
So, we’ve sent our plot code into the abyss, and — since this is 100% like “normal” graphics devices — we also need to do the dev.off
dance:
await webR.evalRVoid("dev.off()");
This will cause the rendering to happen.
Right now, where you can’t see it, is the digital manifestation of your wonderful plot. That’s great, but we’d like to see it!
const msgs = await webR.flush();
will tell it to get on with it and make sure everything that needs to be done is done. If you’re not familiar with async/await yet, you really need to dig into that to survive in DIY WebR land.
const canvas = document.getElementById(id)
canvas.setAttribute("width", 2 * width); // i still need to read "why 2x"
canvas.setAttribute("height", 2 * height);
msgs.forEach(msg => {
if (msg.type === "canvasExec") Function(`this.getContext("2d").${msg.data}`).bind(canvas)()
});
finds our HTML canvas element and then throws messages at it; alot of messages. To see the generated code for the 3D perspective plot example, head to this gist where I’ve pasted all ~10K instructions.
To make said persp
plot, it’s just a simple call, now:
await plottR(webR, `basetheme("dark"); persp(x, y, z, theta=-45)`)
I used the default id
for the canvas in the online example.
“How Did You Use The basetheme Package? It’s Not In The WASM R Repo?”
I yanked the four R source code files from the package and just source
‘d them into the WebR environment:
const baseThemePackage = [ "basetheme.R", "coltools.R", "themes.R", "utils.R" ];
// load up the source from the basetheme pkg
for (const rSource of baseThemePackage) {
console.log(`Sourcing: ${rSource}…`)
await globalThis.webR.evalRVoid(`source("https://rud.is/w/ggwebr/r/${rSource}")`)
}
10K+ Lines Is Alot Canvas Code…
Yep! But, that’s how the HTML canvas element works and it’s shockingly fast, as you’ve seen via the demo.
FIN
We’ll cover a bit more in part 2 when we see how to get ggplot2 working, which will include a WebR version of {hrbrthemes}! I also want to thank James Balamuta for the Quarto WebR project which helped me out quite a bit in figuring this new tech out.
Before I let you go, I wanted to note that in those “messages” (the ones we pulled canvasExec
call out of), there are message types that are not canvasExec
(hence our need to filter them).
I thought you might want to know what they are, so I extracted the JSON, and ran it through some {dplyr}:
msgs |>
filter(
type != "canvasExec"
) |>
pull(data) |>
writeLines()
R version 4.1.3 (2022-03-10) -- "One Push-Up"
Copyright (C) 2022 The R Foundation for Statistical Computing
Platform: wasm32-unknown-emscripten (32-bit)
R is free software and comes with ABSOLUTELY NO WARRANTY.
You are welcome to redistribute it under certain conditions.
Type 'license()' or 'licence()' for distribution details.
R is a collaborative project with many contributors.
Type 'contributors()' for more information and
'citation()' on how to cite R or R packages in publications.
Type 'demo()' for some demos, 'help()' for on-line help, or
'help.start()' for an HTML browser interface to help.
Type 'q()' to quit R.
>
Do Not Purchase Kucht Appliances
I’ll follow up with a more detailed post in a ~week or so, but if you are considering purchasing a Kucht appliance, please, please reconsider your decision. They have repeatedly lied to us (I have proof) and are incapable of manufacturing functioning equipment.
Thanks to them, we are out thousands of dollars and are in the process of contacting the Maine AG and having our personal legal counsel help us to recoup our losses. I’m just thankful our “professional” oven didn’t harm any of our family as it continued to malfunction and degrade whilst the Kucht representatives just ignored us.