🧪 Lit + WebR + Observable Plot: Linking Lit’s Lightweight Web Components And WebR For Vanilla JS Reactivity & JS DataVis

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):

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()

plotOutput.worldPhones = webRDataFrameToJS(
  await (await webR.evalR(
    `as.data.frame.table(WorldPhones, stringsAsFactors=FALSE) |> 
       setNames(c("year", "region", "phones"))`

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`
    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: [
        this.worldPhones.filter((d) => d.region === this.region),
        { x: "year", y: "phones", fill: "#4a6d88" }

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

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.

Cover image from Data-Driven Security
Amazon Author Page