Skip to content

Instantly share code, notes, and snippets.

@alekrutkowski
Last active October 20, 2025 14:55
Show Gist options
  • Save alekrutkowski/e8052bb1ffbc812a6540eee20249ac59 to your computer and use it in GitHub Desktop.
Save alekrutkowski/e8052bb1ffbc812a6540eee20249ac59 to your computer and use it in GitHub Desktop.
Observablehq's "Observable Framework" markdown file example with Web-R (https://docs.r-wasm.org/webr), Grid.js table (https://gridjs.io), and a global spinner
---
toc: false
theme: [dashboard]
footer: ""
---
<link href="https://unpkg.com/gridjs/dist/theme/mermaid.min.css" rel="stylesheet" />
<script src="https://unpkg.com/gridjs/dist/gridjs.umd.js"></script>
<div id="global-spinner" style="
display: none;
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
">
<div class="spinner"></div>
</div>
# My Dashboard
```js
// Message displayed before the first Excel file upload
// Reactively watch for a table being added
// Create a message element
const msg = document.createElement("div");
msg.textContent = "Upload an Excel file, select color, pick a date.";
msg.classList.add("info-box");
document.querySelector("h1").insertAdjacentElement("afterend", msg);
// NOT USED SINCE myTable IS IN view()
// // Function to check for a table
// function checkForTable() {
// msg.remove(); // remove message once a table appears
// observer.disconnect(); // stop watching
// }
// const target = document.getElementById("myTable");
// // Create the observer
// const observer = new MutationObserver(checkForTable);
// // Start observing #myTable
// observer.observe(target, {
// childList: true, // detect added/removed rows/cells
// subtree: true, // detect changes inside descendants
// characterData: true, // detect text changes in cells
// attributes: true // uncomment if you also want attribute changes (like style/class)
// });
import * as XLSX from "https://cdn.jsdelivr.net/npm/[email protected]/+esm";
import { WebR } from "https://webr.r-wasm.org/v0.4.4/webr.mjs";
const webR = new WebR();
await webR.init();
await webR.installPackages(["openxlsx2"]);
```
```js
const spinner = document.getElementById("global-spinner");
spinner.style.display = "flex";
// Read the uploaded file into a workbook
const selected = await fileInput; // value of the view() is the selected File (or array if multiple)
const file = Array.isArray(selected) ? selected[0] : selected;
const arrayBuffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
// Load the uploaded file into WebR
await webR.FS.writeFile("input.xlsx", uint8Array);
// Run R script to process the file with openxlsx2
const rCode = `
library(openxlsx2)
wb <- wb_load("/home/web_user/input.xlsx")
wb <- wb_add_worksheet(wb, sheet="Summary", tab_color = wb_color("${input_radios_single_choice}"))
wb <- wb_add_data(wb, sheet="Summary", x=data.frame(Info="Processed with WebR by ${input_text} on ${input_date}"))
wb_save(wb, "/home/web_user/output.xlsx", overwrite = TRUE)
`;
console.log(rCode);
await webR.evalR(rCode);
// https://docs.r-wasm.org/webr/latest/convert-r-to-js.html#:~:text=converted%20into%20a-,D3%2Dstyle%20data%20array
const df = await webR.evalR("wb_to_df(wb)");
const rows = await df.toD3();
// Obtain the contents of the file from the VFS
const xlsxdata = await webR.FS.readFile("/home/web_user/output.xlsx");
// Serialize to a Blob
const outBlob = new Blob([xlsxdata], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
// Derive a filename like originalname-processed.xlsx
const originalName = file?.name?.replace(/\.(xlsx|xls)$/i, "") ?? "data";
const downloadName = `${originalName}-processed.xlsx`;
spinner.style.display = "none";
const element = document.querySelector(".info-box");
if (element) {
element.remove(); // removes it from the DOM
}
```
<div class="grid grid-cols-4" style="align-items: flex-start; grid-auto-rows: auto">
<div class="card">
```js
const input_switch = view(
Inputs.toggle({ label: "Not really used", value: true })
);
```
```js
const input_radios_single_choice = view(
Inputs.radio(["red", "green", "blue"], { label: "Color" , value: "red"})
);
```
```js
const input_date = view(Inputs.date({ label: "Date", required: true }));
```
```js
const fileInput = view(
Inputs.file({
label: "Excel file",
accept: ".xlsx",
required: true,
multiple: true,
})
);
```
```js
const input_text = view(
Inputs.text({
label: "Name",
placeholder: "Enter your name",
value: "Anonymous",
})
);
```
</div>
<div class="card grid-colspan-3">${rows ? "" : ""} <!-- to display the built-in Observable spinner rather than an empty block when waiting for user input -->
```js
view(html`<table id="myTable" style="width:100%;"></table>`);
new gridjs.Grid({
columns: Object.keys(rows[0]).map((k) => ({
id: k,
name: k,
sort: true,
resizable: true,
})),
data: rows,
resizable: true,
search: true,
sort: true,
pagination: { enabled: true, limit: 10 },
height: "600px",
}).render(document.getElementById("myTable"));
```
```js
// Download button
const button = html`<button>&DownArrowBar; Download processed Excel</button>`;
button.onclick = () => {
const a = document.createElement("a");
a.href = URL.createObjectURL(outBlob);
a.download = downloadName;
a.click();
URL.revokeObjectURL(a.href);
};
display(button);
```
</div>
</div>
<style>
/* --- Grid.js table compact + resizable + wrap headers --- */
/* Table layout tweaks */
.gridjs-table {
width: 100%;
table-layout: fixed; /* keeps columns stable when resizing */
}
/* Headers: allow wrapping, prevent clipping */
.gridjs-th {
position: relative; /* needed for the resize handle */
white-space: normal !important; /* let long names wrap */
overflow: visible !important;
text-overflow: unset !important;
vertical-align: middle;
}
/* Body cells: normal overflow (so content can be seen or wrapped if you wish) */
.gridjs-td {
vertical-align: middle;
white-space: normal; /* set to nowrap if you prefer */
overflow: hidden; /* avoid spilling beyond cell */
text-overflow: ellipsis; /* adjust to taste */
}
/* Compact, balanced padding (headers and cells) */
.gridjs-th,
.gridjs-td {
padding: 6px 10px 5px 10px !important; /* top right bottom left */
}
/* Optional: a touch more padding for header row only */
.gridjs-tr:nth-child(1) .gridjs-th {
padding-top: 8px !important;
padding-bottom: 6px !important;
}
/* Column min width so wrapped headers do not collapse too narrowly */
.gridjs-th,
.gridjs-td {
min-width: 100px; /* adjust to your data */
}
/* Show full header content on hover if you keep ellipsis somewhere */
.gridjs-th:hover .gridjs-th-content {
overflow: visible;
}
/* Resize handle: thin right-edge strip, not a full overlay */
.gridjs-th .gridjs-resizable {
position: absolute !important;
top: 0 !important;
bottom: 0 !important;
right: 0 !important;
left: auto !important;
width: 8px !important; /* thickness of the drag area */
height: 100% !important;
cursor: col-resize !important;
background: transparent !important;
z-index: 2;
pointer-events: auto !important;
}
/* Keep header text and sort control above the cell background */
.gridjs-th .gridjs-th-content,
.gridjs-th .gridjs-sort {
position: relative;
z-index: 3;
pointer-events: auto;
}
/* Optional: subtle visual cue when hovering near the edge */
.gridjs-th .gridjs-resizable:hover {
background: rgba(0, 0, 0, 0.06) !important;
}
/* Ensure the last column can also be resized, if desired */
.gridjs-th:last-child .gridjs-resizable {
display: block !important;
}
/* Optional: slightly thinner borders for a denser look */
.gridjs-td,
.gridjs-th {
border-width: 2px;
}
/* Remove the blue/gray rectangle for column resize handles */
.gridjs-th .gridjs-resizable,
.gridjs-th .gridjs-resizable:hover,
.gridjs-th .gridjs-resizable:active {
background: transparent !important;
}
.info-box {
display: inline-block; /* shrink to fit text */
background-color: #f0f8ff;
border: 1px solid #b3d4fc; /* thin border around */
padding: 8px 12px;
margin: 8px 0;
border-radius: 8px;
font-family: Arial, sans-serif;
font-size: 12px;
color: #333;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
width: fit-content; /* adapts to text width */
max-width: 80%; /* prevent over-expansion */
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #ccc;
border-top-color: #333;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment