Plotting

Plotting with a HTML canvas

WebR’s supporting R package includes a built in graphics device, webr::canvas(). When R uses this device, messages are sent to the main thread containing bitmap image data. The image data can then be displayed using a HTML Canvas element on the page.

A 2x scaling is used to improve the visual quality of the bitmap output. For the best results the width and height of the HTML canvas element displaying the final plot should be twice that of the graphics device. For example, the default arguments for webr::canvas() create a device with a width and height of 504, and so a correctly sized HTML canvas will have width and height attributes set to 1008.

The background colour for the plot can be set with the bg argument, and the text size may be changed by setting the pointsize argument.

Warning

The webr::canvas() graphics device relies on OffscreenCanvas support in the web browser or JavaScript engine running webR. A modern and up-to-date browser will be required for plotting with this device. Older browsers without OffscreenCanvas support should still be able to plot using the Cairo-based graphics devices, such as png().

Output Messages

The webr::canvas() graphics device emits webR output messages when triggered by certain events. The resulting output messages are of type Message with the type property set as 'canvas' and the data property populated with further details about the event that caused the message.

New plot page

When the graphics device creates a new page for plotting, a message is emitted of the form,

{ type: 'canvas', data: { event: 'canvasNewPage' } }

This message can be used as a signal to clear any existing plots, or create a new empty HTML canvas element.

Bitmap image data

When the graphics device is ready to send image data to the main thread for display, a message is emitted with the the image additionally included in the form of a JavaScript ImageBitmap object. The message emitted is of the form,

{ type: 'canvas', data: { event: 'canvasImage', image: ImageBitmap } }

Drawing the bitmap image data

Once the ImageBitmap data has been received by the main thread, it can be displayed on the containing web page. A HTML canvas element can be used to display the image data using its 2D rendering context. The HTML canvas element can be created dynamically by the running JavaScript environment, or it may already exist in the page.

Once a 2D rendering context has been obtained, the image data can be displayed using the drawImage() method.

Setting the default device

R’s default graphics device can be set so that webr::canvas() is always used for new plots. Before running any plotting code, evaluate the following R code to set the default device,

await webR.evalRVoid('options(device=webr::canvas)');

Text rendering and font support

When the webr::canvas() graphics device is used it is the web browser that handles the specifics of text rendering, and so any fonts installed on the host system can be used with the family argument when plotting. Modern features provided by the browser such as RTL text, ligatures, colour emoji, or the use of Arabic, Japanese or Cyrillic script should also be handled automatically.

The editable R code in this example demonstrates font and text features, feel free to experiment.

plot(rnorm(1000), rnorm(1000), col=rgb(0,0,0,0.5), xlim=c(-5, 5), ylim=c(-5, 5), main="This is the title 🚀", xlab="This is the x label", ylab="This is the y label", family="Comic Sans MS") text(-4, 4, "This is English", family="cursive") text(-4, -4, "هذا مكتوب باللغة العربية") text(4, 4, "これは日本語です") text(4, -4, "זה כתוב בעברית")

Example: Handling multiple plots

In the following fully worked example, multiple plots are handled by listening for 'canvasNewPage' events from the graphics device and dynamically adding new HTML canvas elements to the page.

<html>
  <head>
      <title>WebR Multiple Plots Example</title>
  </head>
  <body>
    <h1>WebR Multiple Plots Example</h1>
    <p><div id="loading">Please wait, webR is loading...</div></p>
    <button id="plot-button" disabled="true">Run graphics demo</button>
    <p>See the JavaScript console for additional output messages.</p>
    <div id="plot-container"></div>
    <script type="module">
      import { WebR } from 'https://webr.r-wasm.org/latest/webr.mjs';
      const webR = new WebR();
      let canvas = null;
      let loading = document.getElementById('loading');
      let container = document.getElementById('plot-container');
      let button = document.getElementById('plot-button');

      button.onclick = () => {
        container.replaceChildren();
        webR.evalRVoid(`
          webr::canvas()
          demo(graphics)
          demo(persp)
          dev.off()
        `);
      }

      (async () => {
        // Remove the loading message once webR is ready
        await webR.init();
        loading.remove();
        button.removeAttribute('disabled');

        // Handle webR output messages in an async loop
        for (;;) {
          const output = await webR.read();
          switch (output.type) {
            case 'canvas':
              if (output.data.event === 'canvasImage') {
                // Add plot image data to the current canvas element
                canvas.getContext('2d').drawImage(output.data.image, 0, 0);
              } else if (output.data.event === 'canvasNewPage') {
                // Create a new canvas element
                canvas = document.createElement('canvas');
                canvas.setAttribute('width', '1008');
                canvas.setAttribute('height', '1008');
                canvas.style.width = "450px";
                canvas.style.height = "450px";
                canvas.style.display = "inline-block";
                container.appendChild(canvas);
              }
              break;
            default:
              console.log(output);
          }
        }
      })();
    </script>
  </body>
</html>

Click the button below to see the output of this demo,

Capturing plots

Plots may be captured by the webr::canvas() graphics device when using captureR() to evaluate R code. Captured plots are in the form of JavaScript ImageBitmap objects and may be drawn to the page in the same way as described above.

In the following example, a set of demo plots are captured and then displayed on the page.

<html>
  <head>
    <title>WebR Test Console</title>
  </head>
  <body>
    <div id="plot-output"></div>
    <div>
      <pre><code id="out">Loading webR, please wait...</code></pre>
    </div>

    <script type="module">
      import { WebR } from 'https://webr.r-wasm.org/latest/webr.mjs';
      const webR = new WebR();
      await webR.init();

      const shelter = await new webR.Shelter();
      const capture = await shelter.captureR("demo(graphics)");
      capture.images.forEach((img) => {
        const canvas = document.createElement("canvas");
        canvas.width = img.width;
        canvas.height = img.height;
        const ctx = canvas.getContext("2d");
        ctx.drawImage(img, 0, 0, img.width, img.height);
        document.getElementById("plot-output").appendChild(canvas);
      });

      shelter.purge();
    </script>
  </body>
</html>

Arguments for the capturing webr::canvas() graphics device that’s used during evaluation, such as setting a custom width or height, can be included as part of the optional EvalROptions argument to captureR():

const shelter = await new webR.Shelter();
const capture = await shelter.captureR("hist(rnorm(1000))", {
  captureGraphics: {
    width: 504,
    height: 252,
    bg: "cornsilk",
  }
});

Plotting from the console

The Console class includes callbacks that are used for handling image rendering. This example builds off the interactive webR REPL Console. In addition to the console, there is a <canvas> element to which plots will be drawn. The callbacks canvasImage and canvasNewPage are used to draw plots.

When a new plot is created, the canvasNewPage callback is used clearing the bitmap from the canvas using reset(). Subsequently, the canvasImage callback is used to draw the ImageBitMap object onto the canvas.

<html>
  <head>
    <title>WebR Test Console</title>
    <style>
      body {
          display: flex;
      }
    </style>
  </head>
  <body>
    <div id="plot-output">
      <canvas width="500" height="500" id="plot-canvas"></canvas>
    </div>
    <div>
      <pre><code id="out">Loading webR, please wait...</code></pre>
      <input spellcheck="false" autocomplete="off" id="input" type="text">
      <button onclick="globalThis.sendInput()" id="run">Run</button>
    </div>
    
    <script type="module">
      /* Create a webR console using the Console helper class */
      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();

      /* Set the default graphics device to be half the canvas element size */
      await webRConsole.stdin("options(device=webr::canvas(250, 250))");
      
      /* 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>
  </body>
</html>

Plotting with other graphics devices

In older browsers or JavaScript engines without OffscreenCanvas support, alternative graphics devices may still be used to produce plots. The following methods do not rely on direct rendering in the web browser, but instead the resulting image data is created entirely within the WebAssembly environment and written to the Emscripten virtual filesystem.

Bitmap graphics using Cairo for Wasm, e.g. png()

WebR may be built with bitmap graphics support though the use of a WebAssembly version of the Cairo graphics library and its prerequisites. This support is not enabled by default when building webR from source, as it significantly increases the output WebAssembly binary size and build time, but Cairo graphics support is explicitly enabled for the publicly available distributions of webR via CDN.

When webR is built with Cairo support the following graphics devices are available for use with R in the usual way:

Text rendering and font support

Unlike the HTML canvas device described in the previous section, rendering graphics entirely within the WebAssembly environment presents a challenge in that the sandbox does not have access to the font data installed in the host system. As such, fonts must be made available for use on the Emscripten virtual filesystem before plotting occurs.

When built with Cairo support, webR bundles a minimal selection of fonts. The Noto series of fonts was chosen for this purpose for its open licence and notably high support for internationalisation,

Access to font data is managed through a WebAssembly build of Fontconfig. The fonts bundled by webR support Latin, Cyrillic and Greek scripts, and additional fonts can be uploaded to the Emscripten virtual filesystem in the directory /home/web_user/fonts to allow for different typefaces or additional script support.

Vector graphics using pdf() and svglite()

Vector graphics can be produced with webR through use of the built-in pdf() graphics device, or through the svglite package, which can be installed in webR using the command webr::install("svglite").

Obtaining the plot data from the VFS

The contents of graphics output that has been written to the Emscripten virtual filesystem can be obtained as a JavaScript UInt8Array using the Filesystem API. The data can then be offered for display or download by working with the resulting ArrayBuffer.

In this example, a vector graphics plot is created using the pdf() graphics device, the contents of the file is read from the Emscripten virtual filesystem, and finally the PDF file is offered to the user via a download link.

<html>
  <head>
    <title>WebR PDF Plot Download Example</title>
  </head>
  <body>
    <h1>WebR PDF Plot Download Example</h1>
    <p id="loading">Please wait, webR is working on producing a plot...</div>
    <p id="link-container"></p>
    <script type="module">
      import { WebR } from 'https://webr.r-wasm.org/latest/webr.mjs';
      const webR = new WebR();
      await webR.init();

      // Create a PDF file containing a plot
      await webR.evalRVoid(`
        pdf()
        hist(rnorm(10000))
        dev.off()
      `);

      // Obtain the contents of the file from the VFS
      const plotData = await webR.FS.readFile('/home/web_user/Rplots.pdf');

      // Create a link for the user to download the file contents
      const blob = new Blob([plotData], { type: 'application/octet-stream' });
      const link = document.createElement('a');
      link.download = 'Rplots.pdf';
      link.href = URL.createObjectURL(blob);
      link.textContent = 'Click to download PDF';
      document.getElementById('link-container').appendChild(link);

      // Everything is ready, remove the loading message
      document.getElementById('loading').remove();
    </script>
  </body>
</html>