Last active
October 5, 2025 05:00
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| 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>⤓ 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