Skip navigation

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.

> 

2 Trackbacks/Pingbacks

  1. […] 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… Continue reading → […]

  2. […] app, my first approach was to try to draw the histogram in base R and send it back to the browser. boB has a great blogpost about how to do exactly that, but after a lot of bad code and good 4 letter words, I realized there […]

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.