The Old Faithful Geyser Data shiny app with webR, Bootstrap & ExpressJS
Find all my posts about webR here.
Note: the first post of this series explaining roughly what
webR
is, I won’t introduce it again here.
Note 2: webR and the tools I’ve been writing are moving fast and if you’re reading this from the future, some of the things here might be obsolete.
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>
<meta charset="utf-8">
<meta 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
What do you think?