webR - WebAssembly for R

webr
rprogramming
Author

Maciej Nasinski

Published

April 9, 2023

TL;DR Play with R code in real-time in the web browser using webR (WebAssembly for R). Additionally supported a plot output with canvas.

WebR R Console to Play with the Code

Please install any needed dependency with call like webr::install("dplyr").
The available R packages can be get with the rownames(available.packages(repos = "https://repo.r-wasm.org/")) call.

Loading webR, please wait...

Intro

Now you can experiment with R code directly in your web browser, without the need for any installation or server setup.

The webR Console is powered by WebAssembly, a new technology that allows us to run high-performance applications in the web browsers. With the webR Console, you can write, test, and run R code right in your browser, with a user interface that’s intuitive and easy to use.

To get started with the webR Console, simply scroll down to the webR terminal. You’ll be taken to a new page with a clean, minimal interface that allows you to focus on your code.

I am thrilled to offer this powerful tool on my blog, and I hope you’ll find it as useful for your own projects.

Please be aware of major limitation which is the limited number of avaiable packages to be installed in the webR console. The webR team will continuously add more packages, taking into account many factors.

How it was done

Below is the core of the HTML/JS code that is required to build the WebR Console on a static webpage. The complete code can be found on this GitHub page.

I spent some time adding support for presenting a plot output. This part was challenging because it is not yet well-documented. In order to enable support for graphics, you must use the canvas options.

When using the GitHub pages with ServiceWorker, one of two possible communication channels must be utilized. This means that two service worker scripts, webr-serviceworker.js and webr-worker.js, must be hosted on the same origin as the page loading WebR. It’s important to remember to add these two files to the page resources, which in my case, is done in the _quarto.yaml.

<div style="border: solid #333 1px; padding: 5px; width: 100%;">
  <pre id="loading"><code>Loading webR, please wait...</code></pre>
  <pre id="container" style="max-height: 600px"><code id="out"></code></pre>
  <div class="input-group mb-3">
    <span class="input-group-text" id="prompt"></span>
    <input spellcheck="false" autocomplete="off" id="input" type="text" style="width:500px;height:50px;">
    <button type="button" class="btn btn-secondary" onclick="globalThis.sendInput()" id="run">Run</button>
  </div>
</div>
<div id="container" style="border: solid #333 1px; padding: 5px; width: 100%;">
  <button type="button" class="collapsible-item">Click to Show/Hide the Last Plot</button>
  <div class="content-item" style="display:block;">
    <canvas id="plot-canvas" width="500" height="500"></canvas>
  </div>
</div>


<script type="module">
  import { Console } from 'https://webr.r-wasm.org/latest/webr.mjs';

  var canvas = document.getElementById("plot-canvas")
  var ctx = canvas.getContext('2d');

  const webRConsole = new Console({
    stdout: line => document.getElementById('out').append(line + '\n'),
    stderr: line => document.getElementById('out').append(line + '\n'),
    prompt: p => document.getElementById('out').append(p),
    canvasImage: ci => ctx.drawImage(ci, 0, 0),
    canvasNewPage: () => ctx.reset(),
  });
  webRConsole.run();
    
  webRConsole.webR.init()
  .then(() => document.getElementById('loading').remove())
  .then(() => {
    let rcode_array = ["# Available to install R packages \n", "# rownames(available.packages(repos = 'https://repo.r-wasm.org/'))\n", "options(device=webr::canvas(250, 250))\n", "webr::install('ggplot2', quiet = TRUE)\n", "ggplot2::ggplot(mtcars, ggplot2::aes(mpg, wt)) + ggplot2::geom_point()\n"];

    rcode_array.forEach(element => {
     document.getElementById('out').append("> " + element);
     webRConsole.stdin(element);
    });

  });

  /* Write to the webR console using the ``stdin()`` method */
  let input = document.getElementById('input');
  globalThis.sendInput = () => {
    webRConsole.stdin(input.value);
    document.getElementById('out').append(input.value + '\n');
    input.value = "";
  }
  
  /* Send input on Enter key */
  input.addEventListener(
    "keydown",
    (evt) => {if(evt.keyCode === 13) globalThis.sendInput()}
  );
</script>


<script>
  var coll = document.getElementsByClassName("collapsible-item");
  coll[0].addEventListener("click", function() {
    this.classList.toggle("active-item");
    var content = this.nextElementSibling;
    if (content.style.display === "block") {
      content.style.display = "none";
    } else {
      content.style.display = "block";
    }
  });
</script>

Reference: