Skip to content

Instantly share code, notes, and snippets.

@alekrutkowski
Last active October 5, 2025 05:00
Show Gist options
  • Save alekrutkowski/0649e6dcb0fd4b18e4ea473f1d4309be to your computer and use it in GitHub Desktop.
Save alekrutkowski/0649e6dcb0fd4b18e4ea473f1d4309be to your computer and use it in GitHub Desktop.
Observablehq's "Observable Framework" markdown file example with a nice input output layout (1:3), Excel file upload, processing, and download
---
toc: false
theme: [dashboard]
footer: ""
---
# 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...";
msg.style.padding = "0.5em";
msg.style.color = "red";
document.querySelector("h1").insertAdjacentElement("afterend", msg);
// Function to check for a table
function checkForTable() {
if (document.querySelector("table")) {
msg.remove(); // remove message once a table appears
observer.disconnect(); // stop watching
}
}
// Start observing DOM changes
const observer = new MutationObserver(checkForTable);
observer.observe(document.body, { childList: true, subtree: true });
// Initial check in case table is already there
checkForTable();
```
```js
import * as XLSX from "https://cdn.jsdelivr.net/npm/[email protected]/+esm";
// Helper to convert the first sheet to an array of objects using row 1 as headers
function sheetToObjects(sheet) {
const aoa = XLSX.utils.sheet_to_json(sheet, { header: 1, blankrows: false });
if (aoa.length === 0) return [];
const [headers, ...rows] = aoa;
return rows.map((r) =>
Object.fromEntries(headers.map((h, i) => [h ?? `col_${i + 1}`, r[i]]))
);
}
// Helper to convert objects back to a sheet
function objectsToSheet(objs) {
if (!objs || objs.length === 0) return XLSX.utils.aoa_to_sheet([["No data"]]);
const headers = Object.keys(objs[0]);
const aoa = [headers, ...objs.map((o) => headers.map((h) => o[h]))];
return XLSX.utils.aoa_to_sheet(aoa);
}
// 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 inWb = XLSX.read(arrayBuffer, { type: "array" });
// Take the first sheet as input data
const inSheetName = inWb.SheetNames[0];
const inSheet = inWb.Sheets[inSheetName];
let rows = sheetToObjects(inSheet);
// --- YOUR PROCESSING LOGIC GOES HERE ---
// Example: add a computed column "row_sum" that sums numeric columns
const numericKeys = rows.length
? Object.keys(rows[0]).filter((k) => rows.some((r) => typeof r[k] === "number"))
: [];
rows = rows.map((r) => ({
...r,
row_sum: numericKeys.reduce((acc, k) => (acc + (typeof r[k] === "number" ? r[k] : 0)), 0)
}));
// Example 2: uppercase the contents of column `name` if it exists
if (rows.length && "name" in rows[0]) {
rows = rows.map((r) => ({ ...r, name: r.name == null ? r.name : String(r.name).toUpperCase() }));
}
// --- END PROCESSING ---
// Build a new workbook to download
const outWb = XLSX.utils.book_new();
const outSheet = objectsToSheet(rows);
XLSX.utils.book_append_sheet(outWb, outSheet, "Processed");
// Serialize to a Blob
const outArray = XLSX.write(outWb, { type: "array", bookType: "xlsx" });
const outBlob = new Blob([outArray], {
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`;
```
<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: "Mute", value: true }));
```
```js
const input_radios_single_choice = view(
Inputs.radio(["red", "green", "blue"], { label: "color" })
);
```
```js
const input_checks_multi_choice = view(
Inputs.checkbox(["red", "green", "blue"], { label: "Color" })
);
```
```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">
```js
Inputs.table(rows.slice(0, 25), { rows: 10 })
```
```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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment