Intro a webR (con Svelte)
Yendo desde cero a un plot con R en el navegador usando webR y Svelte
tl;dr
- webR es super interesante porque puedes usar R en el navegador
- George Stagg y Bob Rudis han escrito muchos recursos para ayudarnos a entender webR
- Manejar promises es un papelón
- Svelte ayuda a avanzar
¿Que es webR?
webR es una versión de modificada de R, creada por George Stagg, que corre dentro de navegadores.
Tener a R corriendo tan cerca al usuario de la página es útil por distintas razones: Si se desconecta el internet, el R sigue funcionando; No hay que coordinar servidores para correr decenas de instancias de R; Sobretodo, el poder llamar funciones R desde JavaScript!!
Aun es muy nuevo pero confío en que poco a poco surgirán nuevos usos para webR. Por tanto, este tutorial se enfocará en algunos patrones básicos para utilizarlo en una página.
¿Que necesitas?
Para seguir este tutorial necesitas tener Node.js instalado. Estas set si puedes correr npm
en el terminal y aparece algo asi:
> npm
npm <command>
Usage:
npm install install all the dependencies in your project
...
Y con eso listo ya podremos accesar todo esto como explicaremos en breve:
- webR
- SvelteKit - Para hacernos la vida front-end más fácil
Segundo (y último), necesitas un editor de código IDE. Recomiendo mucho VS Code para este tutorial y desarrollo web en general.
Otras opciones
Si prefieres comenzar a editar sin tener que descargar todo lo mencionado, intenta abrir el proyecto completado en tu navegador usando el Codeflow de Stackblitz:
El código completado está en este repositorio y el app en vivo esta en este enlace.
Recursos para el camino
La gran mayoría del código usado aqui es basado en guías existentes compartidas por miembros de la comunidad R. Si en el camino de leer este tutorial queda algo sin clarificar, no dudes en consultar estos recursos buenísimos:
- ggwebr por Bob Rudis
- El guía oficial de webR para comenzar un proyecto
Tambien las páginas de Bob tienen muchos consejos distribuidos. Busquenlos en Mastodon y Twitter.
Creando el proyecto
webR se monta sobre una página web que crearemos ahora. No te preocupes si nunca has programado para la web porque vamos a usar un template que ya incluye el 99% de lo que necesitamos.
Para este ejemplo particular opté por usar SvelteKit porque provee muchas conveniencias para facilitar el uso de webR.
Abre un terminal en el directorio donde deseas crear el proyecto y entra el siguiente comando:
npm create svelte@latest mi-proyecto-webR
Si te hace las siguientes preguntas, responde asi:
- “Ok to proceed? (y)”, presiona Enter
- “Which Svelte app template?”, selecciona “Skeleton project” con las flechas navegadoras y presiona Enter
- “Add type checking with TypeScript?”, selecciona “Yes, using TypeScript syntax” y presiona Enter
- “Select additional options”, selecciona “Add ESLint” y “Add Prettier” usando las flechas y el spacebar, luego presiona Enter
El resultado es un proyecto de SvelteKit titulado ‘mi-proyecto-webR’ en un directorio nuevo con el mismo nombre. Luego:
cd mi-proyecto-webR
npm install
El cd
nos mueve a dentro del proyecto nuevo y el npm install
descarga todas las dependencias que requiere segun el template. Es similar a install.packages()
en RStudio pero el listado de paquetes está en el archivo package.json
. Espera un momentito hasta que descargue todo.
Este es buen momento para abrir VS Code en el directorio de mi-proyecto-webR. Ahi puedes abrir un terminal usando el keyboard shortcut de Ctrl+Shift+` (eso es un backtick al final).
Para ver que página template hay por default corre:
npm run dev
Esto abre un servidor que presenta tu página, junto a un URL para verla. Tal URL debería ser similar a algo como localhost:5173
pero esos números al final pueden variar. Entra a ese enlace en tu navegador y verás algo así:
Ahora, abre en VS Code el archivo de src/routes/+page.svelte
. Verás que los contenidos de la página cuadran con los contenidos del archivo. Este es el archivo que estaremos editando para añadir webR a nuestra página.
Y ya con eso estamos listos para incorporar webR en nuestro proyecto!
Instalando webR
Si aun tienes el “dev server” abierto y sirviendo tu página, cierralo brevemente usando Ctrl-C en tu terminal y corre esto en vez:
npm install @r-wasm/webr
Una vez culmine, navega en el explorador de archivos de VS Code hacia node_modules/@r-wasm/webr/dist/
. Ahi busca y copia los archivos webr-worker.js
y webr-serviceworker.js
. Luego, pega ambos archivos dentro del directorio static/
del proyecto.
Siguiente: abre el archivo vite.config.ts
y añade esto bajo server.headers
dentro del objeto ya presente dentro de defineConfig()
para que el archivo completo lea como lo siguiente:
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [sveltekit()],
// Tomado de https://rud.is/w/vite-webr-lit/
server: {
headers: {
// for serving locally
"Cache-Control": "no-cache; max-age=1",
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Resource-Policy": "cross-origin",
},
},
});
Es un detalle importante para que webR cargue mucho más rápido en la página.
Vuelve al +page.svelte
, borra las dos lineas que vinieron incluidas, y añade un bloque <script>
y uno <p>
con lo siguiente:
<script>
// Importar webR
import { WebR } from '@r-wasm/webr';
const webR = new WebR();
// Funcion asincrónica para iniciar webR
let webR_ready = false;
async function start_webR() {
await webR.init();
webR_ready = true;
console.log('webR está listo!'); // Mensaje en consola
}
start_webR(); // Comenzar inicializacion
</script>
<p>
<!-- Mensaje en página -->
{#if webR_ready}
webR está listo!
{:else}
webR está cargando...
{/if}
</p>
Es un pedazo algo largo para iniciar webR pero iremos paso a paso sobre que significa en un momento.
Recuerda guardar todos los archivos que edites usando Ctrl-S !
Por ahora, vuelve a correr el servidor de desarrollo usando npm run dev
. En la página aparecerá un mensaje indicando si webR está listo. Asimismo, abre en tu navegador la consola de desarrollador o DevTools (Ctrl+Shift+I) y deberías ver un mensaje diciendo que webR está listo!
(Casi casi) Hola, mundo!
En R, podemos escribir un mensaje usando un comando como print("Hola, mundo!")
. Vamos a realizarlo en WebR pero primero hay que clarificar un pedazo clave de nuestro código hasta ahora.
Dandole la vuelta al async/await
Dentro del script hasta ahora tenemos un pedazo así:
// Funcion asincrónica para iniciar webR
let webR_ready = false;
async function start_webR() {
await webR.init();
webR_ready = true;
console.log("webR está listo!");
}
start_webR(); // Comenzar inicializacion
Esto inicia webR de la siguiente manera:
- Declara una variable bandera
webR_ready
y la asigna como falso ya que webR no está listo aun
Esta variable la pasaremos a las funciones encargadas de correr código R para que sepan si aun deben esperar o si ya pueden enviar sus comandos.
- Declara una función asincrónica (
async
) llamadastart_webR()
que comienza la inicialización de webR y espera (await
) hasta que acabe. Finalmente, actualiza la banderawebR_ready
a true y muestra un mensaje en la consola. Nota importante: Aun NO hemos corrido esta función, solo la hemos preparado
La dinámica entre async/await es lo clave de este código. Iniciar webR y correr comandos R no son procesos instantaneos y pues Javascript necesita que le expliquemos como “esperar”. Me confunde frecuentemente pero estoy optando por este método a lo que aprendo que mejor funciona para mi.
- Llama la función
start_webR()
Y con esto le decimos a webR que inicialize. Parece medio redundante pero separar la inicialización a una función aparte nos permite controlar cuando webR puede usar recursos de la página. Por ejemplo, entiendo que webR descarga alrededor de 40 MB de datos al iniciar y quizas un usuario conectado vía red móvil no desea gastar esa cantidad de datos. En ese caso, pudieramos añadir un botón que inicie webR solo cuando el usuario lo desee. Pero eso sería un feature para más adelante. Por ahora, volvemos a como correr un comando R.
Hola, mundo!
Un código análogo para imprimir “Hola, mundo!” es:
<script>
// ...
// ... lo anterior
// 1. Una función asincrónica para evaluar R
let algun_resultado = undefined;
async function evaluar_codigo_R(mi_codigo) {
const resultado = await webR.evalR(mi_codigo);
const resultado_JS = await resultado.toJs();
algun_resultado = resultado_JS;
}
// 2. Un comando encargado de iniciar dicha función
$: if (webR_ready) {
const code_string = 'print("hola, mundo!")';
evaluar_codigo_R(code_string);
}
</script>
<!-- ... Lo que escribimos en el paso anterior -->
<p>
{#if algun_resultado}
Resultado: {algun_resultado.values}
{:else}
Esperando algun resultado...
{/if}
</p>
La función eval_codigo_R
funciona similar a start_webR
. Es asincrónica porque espera a que se evalua el código R y lo convierte a Javascript (resultado.toJs()
). Luego asigna este resultado final a una variable definida fuera de la función. En este caso, algun_resultado
.
El siguiente pedazo comienza con $:
. Ese símbolo de dolar viene de Svelte y permite que cualquier código que le siga corra reactivamente. La única variable dentro de ese pedazo que puede cambiar es webR_ready
y cuando eso ocurra, se enviará el print statement definido dentro de code_string
a evaluar en R.
Cuando el resultado cargue, aparecerá el mensaje en la página. Los pedazos que dicen {#if algun_resultado}
y {#else}
tambien son Svelte y nos permiten ajustar que enseñar a lo que se prepara el mensaje.
Y así se ve la página cargada:
Un primer gráfico
Podemos dar un paso adicional y replicar el histograma del primer ejemplo de Shiny. Tomará añadir unas funciones para que se encarguen de:
- Capturar la ilustración del gráfico en R
- Dibujar tal resultado en un elemento canvas a través de Javascript
Afortunadamente, Bob Rudis ya se encargó de averiguar estas funciones y compartirlas.
Vamos a copiarlas:
<script>
// ...
// ... lo anterior
let mi_codigo_plot = `
nbins <- 10
x <- faithful$waiting
bins <- seq(min(x), max(x), length.out = nbins + 1)
hist(x, breaks = bins, col = "#75AADB", border = "white",
xlab = "Waiting time to next eruption (in mins)",
main = "Histogram of waiting times")
`
// From https://github.com/hrbrmstr/webr-experiments/blob/batman/ggwebr/main.js
async function plotR(plot_code) {
const webRCodeShelter = await new webR.Shelter();
// Usar un display de canvas
await webR.evalRVoid(
`canvas(width=${canvas_width/2}, \
height=${canvas_height/2})`
);
// Capturar resultado del plot
const result = await webRCodeShelter.captureR(plot_code, {
withAutoprint: true,
captureStreams: true,
captureConditions: false,
env: webR.objs.globalEnv,
});
await webR.evalRVoid("dev.off()");
// Devolver resultados / mensajes
const msgs = await webR.flush();
return msgs;
}
$: if (webR_ready) {
plotR(mi_codigo_plot)
.then((msgs) => {
console.log('plotR msgs', msgs);
// Dibujar en el canvas según
// las instrucciones de los mensajes
msgs.forEach(m => {
if (m.type === 'canvasExec') Function(`this.getContext('2d').${m.data}`).bind(my_canvas)();
});
})
}
// Settings para el elemento canvas
let my_canvas; // El elemento como tal
let canvas_width = 800;
let canvas_height = 600;
</script>
<!-- ... Lo anterior -->
<!-- Un elemento canvas para dibujar el plot -->
<canvas bind:this={my_canvas}
width={canvas_width} height={canvas_height}>
</canvas>
Ok, estoy intentando que no sea tanto código de una pero al menos es el mismo patrón a los ejemplos que ya hemos visto. Definimos otra función asincrónica plotR
(que Bob ya escribio para nosotres), la corremos cuando webR esté listo y hacemos algo con el resultado. La mayor diferencia aqui es que cambiamos el lenguaje de async/await por un .then
ya que es más conciso para este caso.
Basicamente lo que ocurre es que R sabe dibujar a un elemento canvas de HTML ya que George Stagg le escribió un módulo para eso. Recibimos esas instrucciones a través de los mensajes que R envía a Javascript y las corremos dirigidas a nuestro canvas.
Las variables adicionales que pasamos al canvas establecen su tamaño. Además, el bind:this
es Svelte para asignar el elemento canvas como tal a una variable Javascript que se pasa a los mensajes que realizan instrucciones.
Dale refresh a la página y aparecerá un histograma:
Ya un paso más con R y Svelte! Y aun podemos hacer más.
Añadiendo un control
Vamos a aprovechar que Svelte nos conecta todo para darle un poco de interactividad a nuestra gráfica. Añadiremos un slider para controlar el número de bins del histograma. Cambia la definición de mi_codigo_plot
para que lea una variable nueva llamada numero_bins
. Recuerda cambiar el let
por un $:
para que Svelte reconozca que el codigo cambiará y lo envíe a R nuevamente.
<script>
// ...
let numero_bins = 10;
$: mi_codigo_plot = `
nbins <- ${numero_bins}
x <- faithful$waiting
bins <- seq(min(x), max(x), length.out = nbins + 1)
hist(x, breaks = bins, col = "#75AADB", border = "white",
xlab = "Waiting time to next eruption (in mins)",
main = "Histogram of waiting times")
`
// ...
</script>
<!-- ... todo lo de antes excepto el canvas -->
<div>
<input type="range" id="num_bins" name="Numero de bins"
min="1" max="30" bind:value={numero_bins}>
<label for="num_bins">Numero de bins</label>
<p>{numero_bins} bins</p>
</div>
<!-- ... justo antes del canvas -->
El div
que añadimos contiene un input slider que actualiza la variable numero_bins
y vice-versa gracias al bind:value
de Svelte. Asimismo, tambien enseñamos el valor actual para que podamos ver como cambia mientras ajustamos el slider.
Mover el slider de lado a lado actualiza el gráfico en tiempo real:
Si el análisis de R fuera más pesado, quizas sería preferible implementar algun delay entre el cambio del slider y el envío del código a R. Este tipo de ‘throttle’ es algo que Shiny implementa frecuentemente pero por ahora no lo necesitamos.
Un botón para optar a webR
Para cerrar, acabaremos con un ajuste adicional. Mencionamos anteriormente que descargar webR necesita cerca de 40MB de datos. Eso es mucho en un website! Un usuario leyendo tu página en una red celular pudiera preferir ahorrarse esos datos. Podemos darle al usuario la oportunidad de elegir si iniciar ese proceso.
La razón por cual dejamos este feature para el final es porque para tu desarrollar localmente la página es inconveniente tener que darle click a un botón cada vez que cambiemos algo.
Es sencillo porque definimos al comienzo una función que ya decide cuando iniciar R, start_webR()
. En vez de llamarla dentro del script, la activaremos cuando un boton sea presionado. Además, añadiremos una variable flag adicional webR_cargando
para compartir un mensaje más informativo durante el proceso.
<script>
/// ...
// Funcion asincrónica para iniciar webR
let webR_ready = false;
let webR_cargando = false;
async function start_webR() {
webR_cargando = true;
await webR.init();
webR_ready = true;
console.log('webR está listo!'); // Mensaje en consola
}
// start_webR(); // Comenta o elimina esta linea!
/// ...
</script>
<!-- Un nuevo botón al mismo comienzo -->
<button on:click={start_webR} disabled='{webR_cargando}'>
Iniciar!
</button>
<!-- Ajustando el primer mensaje -->
<p>
{#if webR_ready}
webR está listo!
{:else if webR_cargando}
webR está cargando...
{:else}
webR no está listo
{/if}
</p>
Dale click para correr el ejemplo:
Conclusión
Y con eso ya tenemos una aplicación para usar R en la web con chispas de interactividad! Hay muchos detalles que aun tengo que aprender pero ojalá que esto les pueda servir de ayuda en sus inventos :)
Recuerden que pueden buscar el código completo en este repositorio.