Skip navigation

Author Archives: hrbrmstr

Don't look at me…I do what he does — just slower. #rstats avuncular • ?Resistance Fighter • Cook • Christian • [Master] Chef des Données de Sécurité @ @rapid7

WebR has a template for React, but I’m not a fan of it or Vue (a fact longtime readers are likely tired of hearing right about now). It’s my opinion and experience that Lit webcomponents come closer to “bare metal” webcomponents, which means the “lock-in” with Lit is way less of a potential headache than all the baggage that comes with the other frameworks.

I leaned into exposition for most of my WebR Experiments, but that very likely made it hard to reproduce or even use those repos without some pain. So, I decided to reduce the pain, remove the inertia and make a template (GH) you can use almost immediately from the command line.

The only “inertia” is that you need npm installed. Subsequently, cd into the parent directory of the new project you want to make and:

npx create-webr-vite-lit my-webr-project
cd my-webr-project
npm install
npx vite --port=4000

then, hit `http://localhost:4000/` (change the port if you’re already using it for something else).

You can check it out on this demo site.

Batteries Included

  • Vite (for fast building)
  • WebR (duh)
    • r.js which has all the setup code for WebR + some helpers (more coming here, too)
  • Pyodide (initiall disabled)
    • py.js which does not get used but is available if you want to use pyodide
  • Lit (webcomponents) — it ships with 3:
    • one for my usual “loading…” status message (which gets a facelift)
    • one generic webcomponent for Observable Plots
    • one simple “button” webcomponent to trigger simple actions
    • more are coming! The goal is to wrap all the inputs and outputs provided by Bonsai (below). PR’s welcome!
  • A lightweight CSS framework called Bonsai that I added dark-mode support for. The post-create default page is the Bonsai grid & CSS reference. The webcomponents show how to make all the Bonsai styles available to the components (there’s a default full separation of all things from the webcomponents).
  • An example justfile since I’ve grown quite fond of Just

The default/demo “app” demonstrates how all the components work.

light mode version

FIN

This setup should have you up and running with your own apps in no time.

I tested light/dark mode switching in Chrome and Safari (macOS/iOS) and the dark/light switching works as intended. Arc doesn’t respond to it, so I’ll be debugging that.

Drop issues in GH if I need to tweak the dark mode, or if you run into snags.

I was cranking out a blog post for work earlier this week that shows off just how many integrations our platform has. I won’t blather about that content here, but as I was working on it, I really wanted to show off all the integrations.

A table seemed far too boring.

Several categorized unordered lists seemed too unwieldy.

Then, it dawned on me that I could make a visual representation of all the integration partners we have by thinking of the entire integrations’ ecosystem as a “universe” with each category being a “solar system” of that universe.

I’ve been leaning more heavily on javsascript for datavis these days, but I will always be more comfortable in {ggplot2}, so I headed to R to design a way to:

  • generate concentric orbits for “n” solar systems
  • randomize the placement of the planets in each ring
  • make a decent plot!

I worked with one of the most amazing designers on the planet (heh) to come up with some stellar (heh) styling for it, and this was the result:

5 solar system panels

I took the styling guidance and wrapped the messy, individual functions I had into a new {ggsolar} package, you can find at https://github.com/hrbrmstr/ggsolar.

It’s pretty raw, and I need to “geomify” it at some point, but it has

  • a function to generate the concentric circle polygons
  • another one to identify a random point on each ring
  • a naive plotting function, and
  • a theme cleanup function for decent output.

The default is to generate uniformly distributed concentric circles, but you have the option of supplying a custom radii vector to make it more “real”/“solar-sysetm-y”.

Here’s the general flow:

# sol_planets is a built in vector of our system's planet names
sol_orbits <- generate_orbits(sol_planets)

set.seed(1323) # this produced a decent placements

# naive but it works! You can specify your own point picker, too.
placed_planets <- randomize_planet_positions(sol_orbits)

# do the thing!
plot_orbits(
  orbits = sol_orbits, 
  planet_positions = placed_planets,
  label_planets = TRUE,
  label_family = hrbrthemes::font_es_bold
) +
  hrbrthemes::theme_ipsum_es(grid="") +
  coord_equal() +
  labs(
    title = "Sol",
    caption = "Pluto is 100% a planet"
  ) +
  theme_enhance_solar()

Random Systems

I included a generate_random_planets() function that uses a hidden Markov model to create believable planetary names, so you can now make your own universe with {ggplot2}!

set.seed(42)
(rando_planets <- generate_random_planets(12))

rando_orbits <- generate_orbits(rando_planets)

set.seed(123) # this produced decent placements

placed_planets <- randomize_planet_positions(rando_orbits)

plot_orbits(
  orbits = rando_orbits, 
  planet_positions = placed_planets,
  label_planets = TRUE,
  label_family = hrbrthemes::font_es_bold
) +
  hrbrthemes::theme_ipsum_es(grid="") +
  coord_equal() +
  labs(
    title = "Rando System"
  ) +
  theme_enhance_solar()

random system

FIN

Kick the tyres, use {gganimate} to make some animations, and be the ruler of your own universe! (We’re going to try to generate team “org charts” with these later in the week, so be creative, too!).

The official example WebR REPL is definitely cool and useful to get the feel for WebR. But, it is far from an ideal way to deal with it interactively, even as just a REPL.

As y’all know, I’ve been conducing numerous experiments with WebR and various web technologies. I started doing this for numerous reasons, one was to get folks excited about WebR and try to show there are endless possibilities for it (and hopefully avoid lock-in to prescribed views on how you should work with it). Another was to brush up on rusty web skills and have something fun to do during the continuing long aftermath of my spike protein invasion.

I started poking under the WebR covers this past weekend, and until there’s a more pyodide-like JS bridge on the R side of WebR, I decided to forego said spelunking. Instead, I began a dive into {plot2}, a really neat {ggplot2}-esque enhancement to base R plotting. While I could use any R-compatible IDE (there are many, btw), I wanted to do all the experiments in WebR-proper, since base plots work out of the box and the {ggplot2} ecosystem takes a bit of time to install. The tinkering began just fine, but it became a bit tedious doing browser refreshes (they’re automatic with Vite in dev mode) for small tweaks. There was no way I was using the official REPL given the lack of real interactivity in the console. And, I wanted to avoid keeping re-rendering Quarto documents, since that would have been as tedious as the Vite refreshes.

So, I decided to make an “IDE REPL” for WebR, so I could work with it like I would R in Sublime Text, VS Code, or RStudio. I mean, wouldn’t everyone?

You can check it out here, and the source is on GitHub.

I’m not going to take up much more time here, since it comes with some explanations out of the box, but I will reproduce the GH README for it at the end. I will present the structure of the project, here, to make it easier to build upon it (clone/fork away!).

I’m using Monaco, the editor that powers VS Code and the online GitHub editor. It has so many batteries included that it’s hard not to want to use it, even considering how much I despise Microsoft as a company. It is dead simple to use.

The entire project is in vanilla javascript, and there is no builder this time, since I wanted to make this as accessible to as many folks as possible.

This is the project structure:

├── boilerplate.js # text that appears in the source on first load or hard refresh
├── completions.js # a decent number of R completions (I'll add more)
├── index.css      # core CSS
├── index.html     # HTML shell
├── main.js        # Main "app"
├── resizers.js    # We need to keep the panes sized properly
├── r.js           # Some WebR bits
└── rlang.js       # Language stuff for Microsoft's Monaco editor

Rather than adorn the interface with silly buttons and baubles, I am putting functionality into the Monaco command palette.

Here’s what you’ve got with v0.1.0:

  • Auto-saves current source pane contents to local storage
  • R syntax highlighting
  • An oddly decent # of auto-completes
  • cmd-shift-p to bring up command palette
    • WebR: Clear Local Storage — nuke local storage and replace with the default document
    • WebR: Save Canvas as PNG — captain obvious
    • WebR: Save Source Pane — captain obvious
    • WebR: View WebR Environment Summary — captain obvious
    • WebR: View WebR History — captain obvious
  • cmd-shift-i inserts |>
  • option-shift-minus inserts <-
  • watches for ?… and will open up a new tab for web help on whatev u searched for (XSS protected)
  • watches for broweURL(…) and will open up the URL in a new tab (XSS protected)
  • baked-in install.runiverse(pkg) which will try to install a pkg from R Universe. It is ON YOU to load the deps and ensure all deps and the pkg itself will work in WebR. You can use this tool I made to help you out.

FIN

Apart from making the current functionality more robust/pretty, one big forthcoming advancement will be the ability to save/load the WebR workspace to local browser storage. What that will mean for you, is that you can go to an instance of the app, all source changes will automagicallly be saved/restored to the session between visits. Plus — if you’ve saved the workspace image — it will be auto-restored on the visit, leaving you to just have to re-install/load any necessary packages. This means you can get right back to “work”.

I’ll be adding the ability to load files from your local system and use {svglite} for graphics (Monaco has an amazing SVG viewer), and to actually work in the R Console area (either with some janky input box or janky xterm.js).

Kick the tyres, file bugs, feature enhancements, and PRs, and start playing more with WebR!

I won’t wax long and poetic here since I’ve already posted the experiment that has all the details.

TL;DR: there are still only ~90-ish 📦 in the WebR WASM “CRAN”, but more are absolutely on the way, including the capability to build your own CRAN and dev packages via Docker and host your own WebR WASM pkg repo.

@timelyportfolio created an experimental method to install built base R packages from R Universe, and I enhanced that method in another, recent experiment, but that’s a bit wonky, and you have to do some leg work to figure out if a package can be installed and then do a bunch of manual work (though that Observable notebook will save you time).

The aforelinked new experiment shows how to use Pyodide side-by-side with WebR. While this one doesn’t have them sharing data or emscripten filesystems yet, we’ll get there! This puts SCADS of Python packages at your fingertips to fill in the gap while we wait for more R 📦 to arrive.

Code is up on GitHub but hit the experiment first to see what’s going on.

R output, Python output, python plot

A small taste of the experiment.

See it live before reading!

The previous post brought lit-webr, to introduce Lit and basic reactivity.

Today, is more of the same, but we bring the OG Shiny demo plot into the modern age by using Observbable Plot to make the charts.

We’re still pulling data from R, but we’re letting Plot do all the heavy lifting.

Here’s what’s changed…

First, main.js no longer has an {svglite} dependency. This means slightly faster load times, and less code. After ensuring we have datasets available, this is remainder of what happens (please see the larger example for more extended “what’s goin’ on?” comments):

//  WE WILL TALK ABOUT THIS BELOW
import { webRDataFrameToJS } from './utils.js'

const regions = document.getElementById("regionsInput")
const plotOutput = document.getElementById("regionsOutput")

regions.options = await (await R.webR.evalR(`colnames(WorldPhones)`)).toArray()

//  WE WILL TALK ABOUT THIS BELOW
plotOutput.worldPhones = webRDataFrameToJS(
  await (await webR.evalR(
    `as.data.frame.table(WorldPhones, stringsAsFactors=FALSE) |> 
       setNames(c("year", "region", "phones"))`
  )).toJs())

plotOutput.region = regions.options[ 0 ]

The webRDataFrameToJS() function in utils.js was mentioned in a previous experiment. Its sole mission in life is to turn the highly structured object that is the result of calling WebR’s toJs() function on an R data.frame. Most JS data things like the structure webRDataFrameToJS() puts things into, and Observable Plot is a cool JS data thing.

The ugly await… await… sequence is to get the data from R to give to webRDataFrameToJS(). We got lucky thins time since as.data.frame.table does a niiice job taking the WorldPhones rownamed matrix and pivoting it longer.

We store the output of that into the region-plot component. I could/should have made it a private property, but no harm, no foul in this setting.

Lastly, in region-plot.js, our component is reduced to two properties: one to store the region name and one for the data you saw, above. We still use events to trigger updates between the popup and the plotter, and said plotter is doing this in render():

render() {
return html`
<div>
<slot></slot>
${
  Plot.plot({
    style: {
      background: "#001e38",
      color: "#c6cdd7",
      padding: "30px",
      fontSize: "10pt",
      fontFamily: '-apple-system, BlinkMacSystemFont, …'
    },
    inset: 10,
    marginLeft: 60,
    caption: "Data from AT&T (1961) The World's Telephones",
    x: {
      label: null,
      type: "band"
    },
    y: {
      label: "Number of ☎️ (K)",
      grid: true
    },
    marks: [
      Plot.barY(
        this.worldPhones.filter((d) => d.region === this.region),
        { x: "year", y: "phones", fill: "#4a6d88" }
      ),
      Plot.ruleY([0])
    ]
  })
}
</div>`;
}

When the region changes, it triggers a reactive update. When the refresh happens, this snippet:

js
this.worldPhones.filter((d) => d.region === this.region)

does the hard work of filtering out all but the region we selected from the tiny, in-memory phones “database”.

Plot may not be {ggplot2}, but it cleans up well, and we’ve even had it match the style we used in the previous experiment.

See it live before reading!

This is a Lit + WebR reproduction of the OG Shiny Demo App

Lit is a javascript library that makes it a bit easier to work with Web Components, and is especially well-suited in reactive environments.

My recent hack-y WebR experiments have been using Reef which is an even ligher-weight javascript web components-esque library, and it’s a bit more (initially) accessible than Lit. Lit’s focus on “Web Components-first” means that you are kind of forced into a structure, which is good, since reactive things can explode if not managed well.

I also think this might Shiny folks feel a bit more at home.

This is the structure of our Lit + WebR example (I keep rejiggering this layout, which likely frustrates alot of folks 🙃)_

lit-webr
├── css
│   └── style.css             # you know what this is
├── favicon.ico               # when developing locally I want my icon
├── index.html                # you know what this is
├── main.js                   # the core experiment runner
├── md
│   └── main.md               # the core experiment markdown file
├── r-code
│   └── region-plot.R         # we keep longer bits of R code in source files here
├── r.js                      # place for WebR work
├── renderers.js              # these experiment templates always use markdown
├── themes
│   └── ayu-dark.json         # my fav shiki theme
├── utils.js                  # handy utilities (still pretty bare)
├── wc
│   ├── region-plot.js        # 👉🏼 WEB COMPONENT for the plot
│   ├── select-list.js        # 👉🏼               for the regions popup menu
│   └── status-message.js     # 👉🏼               for the status message
├── webr-serviceworker.js.map # not rly necessary; just for clean DevTools console
└── webr-worker.js.map        # ☝🏽

A great deal has changed (due to using Lit) since the last time you saw one of these experiments. You should scan through the source before continuing.

The core changes to index.html are just us registering our web components:

<script type="module" src="./wc/select-list.js"></script>
<script type="module" src="./wc/region-plot.js"></script>
<script type="module" src="./wc/status-message.js"></script>

We could have rolled them up into one JS file and minified them, but we’re keeping things simple for these experiments.

Web Components (“components” from now on) become an “equal citizen” in terms of HTMLElements, and they’re registered right in the DOM.

The next big change is in this file (the rendered main.md), where we use these new components instead of our <div>s. The whittled down version of it is essentially:

<status-message id="status"></status-message>

<region-plot id="regionsOutput" svgId="lit-regions">
  <select-list label="Select a region:" id="regionsInput"></select-list>
</region-plot>

The intent of those elements is pretty clear (much clearer than the <div> versions), which is one aspect of components I like quite a bit.

You’ll also notice components are - (dash) crazy. That’s part of the Web Components spec and is mandatory.

We’re using pretty focused components. What I mean by that is that they’re not very reusable across other projects without copy/paste. Part of that is on me since I don’t do web stuff for a living. Part of it was also to make it easier to show how to use them with WebR.

With more modular code, plus separating out giant chunks of R source means that we can actually put the entirety of main.js right here (I’ve removed all the annotations; please look at main.js to see them; we will be explaining one thing in depth here, vs there, tho.):

import { renderMarkdownInBody } from "./renderers.js";
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";

await renderMarkdownInBody(
  `main`,
  "ayu-dark",
  [ 'javascript', 'r', 'json', 'md', 'xml', 'console' ],
  false
)

let message = document.getElementById("status");
message.text = "WebR Loading…"

import * as R from "./r.js";

message.text = "Web R Initialized!"

await R.webR.installPackages([ "svglite" ])

await R.library(`svglite`)
await R.library(`datasets`)

const regionRender = await globalThis.webR.evalR(await d3.text("r-code/region-plot.R"))

message.text = "{svglite} installed"

const regions = document.getElementById("regionsInput")
const plotOutput = document.getElementById("regionsOutput")

regions.options = await (await R.webR.evalR(`colnames(WorldPhones)`)).toArray()
plotOutput.region = regions.options[ 0 ]
plotOutput.renderFunction = regionRender
plotOutput.render()

message.text = "Ready"

I want to talk a bit about this line from main.js:

const regionRender = await globalThis.webR.evalR(
  await d3.text("r-code/region-plot.R")
)

That fetches the source of the single R file we have in this app, evaluates it, and returns the evaluated value (which is an R function object) to javascript. This is the script:

renderRegions <- function(region, id = "region-plot") {

  # our base plot theme

  list(
    panel.fill = "#001e38",
    bar.fill = "#4a6d88",
    axis.color = "#c6cdd7",
    label.color = "#c6cdd7",
    subtitle.color = "#c6cdd7",
    title.color = "#c6cdd7",
    ticks.color = "#c6cdd7",
    axis.color = "#c6cdd7"
  ) -> theme

  # get our svg graphics device amp'd
  s <- svgstring(width = 8, height = 4, pointsize = 8, id = id, standalone = FALSE)

  # setup theme stuff we can't do in barplot()
  par(
    bg = theme$panel.fill,
    fg = theme$label.color
  )

  # um, it's a barplot
  barplot(
    WorldPhones[, region],
    main = region,
    col = theme$bar.fill,
    sub = "Data from AT&T (1961) The World's Telephones",
    ylab = "Number of Telephones (K)",
    xlab = "Year",
    border = NA,
    col.axis = theme$axis.color,
    col.lab = theme$label.color,
    col.sub = theme$subtitle.color,
    col.main = theme$title.color
  )

  dev.off()

  # get the stringified SVG
  plot_svg <- s()

  # make it responsive
  plot_svg <- sub("width='\\d+(\\.\\d+)?pt'", "width='100%'", plot_svg)
  plot_svg <- sub("height='\\d+(\\.\\d+)?pt'", "", plot_svg)

  # return it
  plot_svg

}

That R function is callable right from javascript. Creating that ability was super brilliant of George (the Godfather of WebR). We actually end up giving it to the component that plots the barplot (see region-plot.js) right here:

plotOutput.renderFunction = regionRender

We’re getting a bit ahead of ourselves, since we haven’t talked about the components yet. We’ll do so, starting with the easiest one to grok, which is in status-message.js and is represented by the <status-message></status-message> tag.

These custom Lit components get everything HTMLElement has, plus whatever else you provide. I’m not going to show the entire source for status-message.js here as it is (lightly) annotated. We’ll just cover the fundamentals, as Lit components also have alot going on and we’re just using a fraction of what they can do. Here’s the outline of what’s in our status-message:

export class StatusMessage extends LitElement {
  static properties = { /* things you can assign to and read from */ }
  static styles = [ /* component-scoped CSS */ ]
  constructor() { /* initialization bits */
  render() { /* what gets called when things change */ }
}
// register it
customElements.define('status-message', StatusMessage);

Our status-message properties just has one property:

static properties = {
  text: {type: String}, // TypeScript annotations are requried by Lit
};

This means when we do:

let message = document.getElementById("status");
message.text = "WebR Loading…"

we are finding our component in the DOM, then updating the property we defined. That will trigger render() each time, and use any component-restricted CSS we’ve setup.

Things get a tad more complicated in select-list.js. We’ll just cover the highlights, starting with the properties:

static properties = {
  id: { type: String },    // gives us easy access to the id we set
  label: { type: String }, // lets us define the label up front
  options: { type: Array } // where the options for the popup will go
};

If you recall, this is how we used them in the source:

<region-plot id="regionsOutput" svgId="lit-regions">
  <select-list label="Select a region:" id="regionsInput"></select-list>
</region-plot>

The id and label properties will be available right away after the custom element creation.

We start option with an empty list:

constructor() {
  super()
  this.options = []
}

Our render() function places the <label> and <select> tags in the DOM and will eventually populate the menu once it has data:

render() {
  const selectId = `select-list-${this.id}`;
  return html`
  <label for="${selectId}">${this.label} 
    <select id="${selectId}" @change=${this._dispatch}>
      ${this.options.map(option => html`<option>${option}</option>`)}
    </select>
  </label>
  `;
}

Their clever use of JS template strings makes it much easier than ugly string concatenation.

That html in the return is doing alot of work, and not just returning text. You gotta read up on Lit to get more info b/c this is already too long.

The way we wired up reactivity in my Reef examples felt kludgy, and even the nicer way to do it in Reef feels kludgy to me. It’s really nice in Lit. This little addition to the <select> tag:

@change=${this._dispatch}

says to call a function named _dispatch whenever the value changes. That’s in the component as well:

_dispatch(e) {
  const options = {
    detail: e.target,
    bubbles: true,
    composed: true,
  };
  this.dispatchEvent(new CustomEvent(`regionChanged`, options));
}

We setup a data structure and then fire off a custom event that our plot component will listen for. We’ve just linked them together on one side. Now we just need to populate the options list, using some data from R:

const regions = document.getElementById("regionsInput")
regions.options = await (await R.webR.evalR(`colnames(WorldPhones)`)).toArray()

That’ll make the menu appear.

Hearkening back to the main.js plot setup:

const plotOutput = document.getElementById("regionsOutput")
plotOutput.region = regions.options[ 0 ]
plotOutput.renderFunction = regionRender
plotOutput.render()

we see that we:

  • find the element
  • set the default region to the first one in the popup
  • assign our R-created rendering function to it
  • and ask it nicely to render right now vs wait for someone to select something

The other side of that (region-plot.js) is a bit more complex. Let’s start with the properties:

static properties = {
  // we keep a local copy for fun
  region: { type: String },

  // this is where our S
  asyncSvg: { type: String },

  // a DOM-accessible id string (cld be handy)
  svgId: { type: String },

  // the function to be called to render
  renderFunction: { type: Function }
};

WebR === “async”, which is why you see that asyncSvg. Async is great and also a pain. There are way more functions in region-plot.js as a result.

We have to have something in renderFunction before WebR is powered up since the component will be alive before that. We’ll give it an anonymous async function that returns an empty SVG.

this.renderFunction = async () => `<svg></svg>`

Oddly enough, our render function does not call the plotting function. This is what it does:

render() {
  return html`
  <div>
  <slot></slot>
  ${unsafeSVG(this.asyncSvg)}
  </div>`;
}

This bit:

<slot></slot>

just tells render() to take whatever is wrapped in the tag and shove it there (it’s a bit more powerful than just that tho).

This bit:

${unsafeSVG(this.asyncSvg)}

is just taking our string with SVG in it and letting Lit know we really want to live dangerously. Lit does its best to help you avoid security issues and SVGs are dangerous.

So, how do we render the plot? With two new functions:

// this is a special async callback mechanism that 
// lets the component behave normally, but do things 
// asynchronously when necessary.
async connectedCallback() {

  super.connectedCallback();

  // THIS IS WHERE WE CALL THE PLOT FUNCTION
  this.asyncSvg = await this.renderFunction(this.region, this.svgId);

  // We'll catch this event when the SELECT list changes or when
  // WE fire it, like we do down below.
  this.addEventListener('regionChanged', async (e) => {
    this.region = e.detail.value;
    const res = await this.renderFunction(this.region, this.svgId);
    // if the result of the function call is from the R function and
    // not the anonymous one we initialized the oject with
    // we need to tap into the `values` slot that gets return
    // with any call to WebR's `toJs()`
    if (res.values) this.asyncSvg = res.values[ 0 ] ;
  });

}

// special function that will get called when we 
// programmatically ask for a forced update
performUpdate() {
  super.performUpdate();
  const options = {
    detail: { value: this.region },
    bubbles: true,
    composed: true,
  };

  // we fire the event so things happen async
  this.dispatchEvent(new CustomEvent(`regionChanged`, options));
}

That finished the wiring up on the plotting end.

Serving ‘Just’ Desserts (Locally)

I highly recommend using the tiny but awesome Rust-powered miniserve to serve things locally during development:

miniserve \
  --header "Cache-Control: no-cache; max-age=300" \
  --header "Cross-Origin-Embedder-Policy: require-corp" \
  --header "Cross-Origin-Opener-Policy: same-origin" \
  --header "Cross-Origin-Resource-Policy: cross-origin" \
  --index index.html \
  .

You can use that (once installed) from the local justfile, which (presently) has four semantically named actions:

  • install-miniserve
  • serve
  • rsync
  • github

You’ll need to make path changes if you decide to use it.

FIN

I realize this is quite a bit to take in, and — as I keep saying — most folks will be better off using WebR in Shiny (when available) or Quarto.

Lit gives us reactivity without the bloat that comes for the ride with Vue and React, so we get to stay in Vanilla JS land. You’ll notice there’s no “npm” or “bundling” or “rollup” here. You get to code in whatever environment you want, and serving WebR-powered pages is, then, as simple as an rsync.

Drop issues at the repo.

After writing the initial version of a tutorial on wrapping and binding R functions on the javascript side of WebR, I had a number of other WebR projects on the TODO list. But, I stuck around noodling on the whole “wrapping & binding” thing, and it dawned on me that there was a “pretty simple” way to make R functions available to javascript using javascript’s Function() constructor. I’ll eventually get this whole thing into the GH webr-experiments repo, but you can see below and just view-source (it’s as of the time of this post, in the 130’s re: line #’s) to check it out before that.

Dynamic JavaScript Function Creation

The Function() constructor has many modes, one of which involves providing the whole function source and letting it build a function for you. R folks likely know you can do that in R (ref: my {curlconverter} pkg), and it’s as straightforward on the javascript side. Open up DevTools Console and enter this:

const sayHello = new Function('return function (name) { return `Hello, ${name}` }')();

then call the function with some string value.

We only need one input to create a dynamic R function wrapper (for SUPER BASIC R functions ONLY). Yes, I truly mean that you can just do this:

let rbeta = await wrapRFunction("rbeta");
await rbeta(10, 1, 1, 0)

and get back the result in the way WebR’s toJs() function returns R objects:

{
  "type": "double",
  "names": null,
  "values": [
    0.9398577840605595,
    0.42045006265859153,
    0.26946718094298633,
    0.3913958406551122,
    0.8123499099597378,
    0.49116132695862963,
    0.754970193716774,
    0.2952198011408607,
    0.11734111483990002,
    0.6263863870230043
  ]
}

Formal Attire

R’s formals() function will let us get the argument list to a function, including any default values. We won’t be using them in this MVP version of the auto-wrapper, but I see no reason we couldn’t do that in a later iteration. It’s “easy” to make that a javascript function “the hard way”:

async function formals(rFunctionName) {
  let result = await globalThis.webR.evalR(`formals(${rFunctionName})`);
  let output = await result.toJs();
  return(Promise.resolve(output))
}

Now, we just need to use those names to construct the function. This is an ugly little function, but it’s really just doing some basic things:

async function wrapRFunction(rFunctionName) {
  let f = await formals(rFunctionName)
  let argNames = f.names.filter(argName => argName != '...');
  let params = argNames.join(', ')
  let env = argNames.map(name => `${name}: ${name}`).join(', ');
  let fbody =
  `return async function ${rFunctionName}(${params}) {
    let result = await globalThis.webR.evalR(
      '${rFunctionName}(${params})',
      { env: { ${env} }}
   );
    let output = await result.toJs();
    globalThis.webR.destroy(result);
    return Promise.resolve(output);
  }`;
  return new Function(fbody)()
}
  • first, we get the formals for the function name provided to us
  • then we remove ones we can’t handle (yet!)
  • then we take the R function parameter names and make two objects:
    • one that will make a comma-separated parameter declaration list (e.g. param1, param2, param3)
    • another that does the same, but as key: value pairs to pass as the environment (see previous WebR blog posts and experiments)
  • the function body is another template string that just makes the standard WebR set operations to evaluate an R object and get the value back.

We pass the built function string to the Function() constructor, and we have an automagic javascript wrapper for it.

FIN

This is a very naive wrapper. It’s all on the caller to specify all the values. R has some fun features, like the ability to not specify a value for a given parameter, and then check on the R side to see if it’s missing. It can also intermix named and positional parameters, and then there’s the dreaded ....

I’m going to noodle a bit more on how best to account and/or implement ^^, but I’ll 100% be using this idiom to wrap R functions as I keep experimenting.

One more thing: I had to abandon the use of the microlight syntax highlighter. It’s super tiny and fast, but it doesn’t handle the code I need, so I’ll be updating my nascent webr-template to incorporate what I am using now: Shiki. The next release will also include these basic wrapper functions.

It’s difficult to believe it has only been a couple of weeks since WebR has been around. But that might just be my perception. The spike protein invasion has significantly increased sedentary time, and that has enabled me to focus on this new toy to keep my attention focused on something positive. So, I’ve had “WebR on the mind” more than most others.

As folks likely know, I’m keeping a log of these “experiments” over at GH, and I’ve blogged, here, a bit on a few of them. (I promise I’ll get to Part 2 of “ggwebr”, soon.). Today we have an update on two of them.

WebR Filesystem Machinations

This WebR-powered page is pretty a pretty self-contained explanation of how to work with the Emscripten filesystem from both the javascript and R contexts of WebR. I set out to just do that, but I was also sick of writing HTML for everything, and spent some time coming up with a system for hacking on these experiments with Markdown.

Yes, I could just use the exciting new Quarto WebR extension and Quarto itself, but there are advantages to working in plain Markdown, HTML, and Vanilla JS blank canvas (Quarto-rendered HTML is a bit, er, crufty).

The code in this filesystem experiment is also a bit different in another way: it uses a Vanilla JS lightweight reactive framework called Reef.

It worked so well that I made a few more examples, which are covered in the remaining sections.

More Concise Example

The filesystem experiment is a tad big, especially since it introduces some fancy javascript things many R folks are not familiar with. That, combined with the cognitive load associated with learning WebR itself, meant I had to simplify things a bit if I wanted to convey the utility of this setup.

If you’re asking, “why not just use React?”, the answer is that React is overkill for the small projects I have in mind, and that I think many other folks who may want to use WebR also have in mind. I also don’t really like packers/bundlers. The early days of the web were fun and easy to work in, and I wanted to replicate that for WebR.

So, I made the sadly named webr-reef example. It’s a small app that:

  • loads a list of all datasets in the datasets package and shoves it into a <select> list.
  • when a dataset is selected, the str() output of it is displayed below the popup menu.

If you view index.md, you’ll see it’s just some light markdown and a couple bits of HTML for the:

  • reactive #message <div> that will show anything we assign to it
  • reactive #selected-dataset <select> list which will get automagically populated on load and, when changed, will cause the value of the next item in this list to change
  • reactive #dataset-output which will hold a str() of the selected dataset.

The index.html file is really just a shell for opengraph tags plus an element to put this app into. main.js does all the work.

The source is fairly well-annotated, but it still was not simple enough IMO.

ReefR Template

Reef + WebR == ReefR

I wanted to make this super easy to read and start using quickly, so it’s ready-made to deploy to GH Pages. Just tell GHP to use / of the main branch, change a couple settings — like SW_URL — and it should “just work”. Here’s the GHP Link for the template repo, and here it is running on my site.

There are just two reactive components: the #message thing I mentioned earlier, and a <pre> block that gets filled with the value of the WebR WASM R version string, which is taken from executing code in the WebR context.

FIN

I’ll be iterating on ReefR to add some JS and R helpers folks can use to make it easier to work with WebR, so keep an eye on the experiments and template repo if this piques your interest. Also drop issues with ideas or questions.