Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
This post is the second one of a series of post about webR:
- Using webR in an Express JS REST API
- The Old Faithful Geyser Data shiny app with webR, Bootstrap & ExpressJS
Note: the first post of this series explaining roughly what webR is, I won’t introduce it again here.
In this post, I’ll attempt to recreate a version of the famous Old Faithful Geyser Data {shiny}
app using webR
, Bootstrap
& ExpressJS
.
(If you really don’t know which app I’m talking about, it’s this one.)
Introductory notes
Before starting, here are two notes regarding the app built in this post:
-
this app could have been build in full “web mode” (no server required, just an HTML page), but this is not the approach I’m currently experimenting with. Going full browser-based doesn’t work for all cases, and most of the time with production apps you’ll need some part of your code to be computed by the server (because of resources, because you’ll connect to API with token, because you need access to DB with passwords, because you don’t want the full data to be available in the brower, or many other good reason…).
-
In order to recreate the 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 was no sane reason for me to display a base R plot instead of a JavaScript based one. That’s why I chose to return the data for the barplot (using the
cut()
function from R) and draw with chart.js, instead of trying to display the “not so user friendly” base plot.
Project init
Let’s start by creating a new Express app:
mkdir express-webr-old-faithful cd express-webr-old-faithful npm init -y # Installing the deps we'll need npm i @r-wasm/webr express # Creating a server, and the front page touch index.js touch index.html
Server
Let’s move to the server side first (index.js
).
We’ll start by taking the file from the previous blog post, and modify it to:
- serve index.html on
/
- create a route that returns the data of the bins for our histogram
Let’s start with our code to init webR
.
'use strict'; const express = require("express") // For serving the html const path = require("path") const app = express() const { WebR } = require('@r-wasm/webr'); (async () => { globalThis.webR = new WebR(); await globalThis.webR.init(); // Given that we will reuse this value, // we assign it at launch await globalThis.webR.evalR('x <- faithful[, 2]') console.log("webR is ready"); app.listen(3000, '0.0.0.0', () => { console.log('http://localhost:3000') }) })();
Then, an endpoint that serves index.html
:
app.get("/", (req, res) => { res.sendFile(path.join(__dirname, "index.html")) })
And an endpoint that sends the bins for our plot, using ExpressJS parameter notation (path/:n
).
The value is sent to webR via the env
option of evalR
app.get("/hist-data/:n", async (req, res) => { let result = await globalThis.webR.evalR( 'table(cut(x, seq(min(x), max(x), length.out = n + 1)))', { env: { n: parseInt(req.params.n) } } ); let output = await result.toJs(); res.send(output) })
Now that our backend is ready, let’s check that we can now call it from the command line:
curl http://localhost:3000/hist-data/10 {"type":"integer","names":["(43,48.3]","(48.3,53.6]","(53.6,58.9]","(58.9,64.2]","(64.2,69.5]","(69.5,74.8]","(74.8,80.1]","(80.1,85.4]","(85.4,90.7]","(90.7,96]"],"values":[15,28,26,24,9,23,62,55,23,6]}
Front
Now, time to build the front.
We’ll start with the Boostrap boilerplate from https://getbootstrap.com/docs/5.3/getting-started/introduction/#quick-start.
Note also that I’ve chosen (for simplicity’s sake) to use the CDN version of the external deps, instead of installing them in my Node project, which would be what I would do in a normal context.
<!doctype html> <html lang="en"> <head> <charset="utf-8"> <name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap demo</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous"> </head> <body> <h1>Hello, world!</h1> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script> </body> </html>
Let’s change the title, and add:
- the range input
- a div to receive the chart
<div class="container"> <h1>Old Faithful Geyser Data</h1> <!-- Bootstrap grid system --> <div class="row align-items-start"> <div class="col-4"> <label for="customRange1" class="form-label">Number of bins:</label> <input type="range" class="form-range" id="customRange1" min=1 max=30 value=10> <div id="bins">Selected: 10</div> </div> <div class="col-8"> <div> <!-- Where we'll get the chart drawn --> <canvas id="myChart"></canvas> </div> </div> </div> </div>
In order to draw the graph, I’ll rely on a dead simple (yet powerful) JavaScript lib called Chart.js. It has a great bar chart graph that will work perfectly for our case.
Let’s start by adding the lib with <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
.
We then need some JavaScript to:
- Initiate the chart at launch
// https://colinfay.me/api-from-client-shiny/ // Default to 10 bins fetch("hist-data/10") .then((data) =>{ // Convert the data to json and // create a chart data.json().then((res) => { // Keeping a global object with the chart globalThis.chart = new Chart( // This is where the chart will go document.getElementById('myChart'), { type: 'bar', data: { // webR returns the names labels: res.names, datasets: [{ label: "Histogram of waiting times", data: res.values }] } }); }) .catch((error) => { alert("Error catchin result from R") }) }) .catch((error) => { alert("Error catchin result from R") })
- Update the chart when the slider is moved
const update = function (n = 10) { fetch(`hist-data/${n}`).then((data) => { data.json().then((res) => { globalThis.chart.data.labels = res.names; globalThis.chart.data.datasets.forEach(dataset => { dataset.data = res.values; }) globalThis.chart.update(); }) }) document.querySelector('#bins').innerHTML = `Selected: ${n}`; } document.querySelector('#customRange1').addEventListener( 'change', function() { update(this.value); } );
And here it is! You can see the app live at srv.colinfay.me/express-webr-old-faithful. You can find the code here.
You can also try it with:
docker run -it -p 3000:3000 colinfay/express-webr-old-faithful
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.