Created
February 13, 2026 02:26
-
-
Save pedramamini/f26c1e7a8533cdc4fec64b0402d5fdec to your computer and use it in GitHub Desktop.
I pull stats into Oura regularly (manual unfortunately) then generate a table with different lookback windows via this DataViewJS snippet.
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
| /******************************************************************** | |
| * Oura Stats + Regression‑based Trend & Δ % * | |
| * – zebra‑striped rows for easier scanning * | |
| ********************************************************************/ | |
| /* ---------- helpers ---------- */ | |
| function fmtNum(n, d = 0) { return n.toFixed(d).replace(/\B(?=(\d{3})+(?!\d))/g, ","); } | |
| function fmtDate(s) { const [y, m, d] = s.split("-"); return `${parseInt(m)}/${parseInt(d)}`; } | |
| function linReg(x, y) { | |
| const n = x.length, | |
| sx = x.reduce((a, b) => a + b, 0), | |
| sy = y.reduce((a, b) => a + b, 0), | |
| sxy = x.reduce((a, _, i) => a + x[i] * y[i], 0), | |
| sxx = x.reduce((a, v) => a + v * v, 0); | |
| const den = n * sxx - sx * sx; | |
| if (!den) return { a: 0, b: 0 }; | |
| const b = (n * sxy - sx * sy) / den; | |
| const a = (sy - b * sx) / n; | |
| return { a, b }; | |
| } | |
| /* ---------- metric config ---------- */ | |
| const metrics = [ | |
| { k: "ActivityScore", label: "Activity Score", better: "high" }, | |
| { k: "ActivitySteps", label: "Steps", better: "high" }, | |
| { k: "ActivityTotalCalories", label: "Calories", better: "high" }, | |
| { k: "BodyBodyTemperature", label: "Body Temp", better: "low", dec: 1 }, | |
| { k: "BodyReadinessScore", label: "Readiness Score", better: "high" }, | |
| { k: "BodyRestingHeartRate", label: "Resting HR", better: "low" }, | |
| { k: "SleepREM", label: "REM Time (min)", better: "high" }, | |
| { k: "SleepScore", label: "Sleep Score", better: "high" } | |
| ].sort((a, b) => a.label.localeCompare(b.label)); | |
| /* ---------- time‑range dropdown ---------- */ | |
| const spans = [ | |
| { v: "1w", lbl: "1 Week", since: dv.date("today").minus({ weeks: 1 }) }, | |
| { v: "2w", lbl: "2 Weeks", since: dv.date("today").minus({ weeks: 2 }) }, | |
| { v: "1m", lbl: "1 Month", since: dv.date("today").minus({ months: 1 }) }, | |
| { v: "6w", lbl: "6 Weeks", since: dv.date("today").minus({ weeks: 6 }) }, | |
| { v: "2m", lbl: "2 Months",since: dv.date("today").minus({ months: 2 }) }, | |
| { v: "90d", lbl: "90 Days", since: dv.date("today").minus({ days: 90 }) }, | |
| { v: "all", lbl: "All Time",since: dv.date("1900‑01‑01") } | |
| ]; | |
| /* ---------- UI ---------- */ | |
| dv.paragraph(` | |
| <select id="rangeSel"> | |
| ${spans.map(r => `<option value="${r.v}" ${r.v === "1m" ? "selected" : ""}>${r.lbl}</option>`).join("")} | |
| </select> | |
| <button id="updBtn" style="margin-left:10px;">Update</button> | |
| <div id="tblWrap" style="margin-top:12px;"></div> | |
| `); | |
| /* ---------- core calc ---------- */ | |
| function calcStats(vals, dates, key) { | |
| let rows = []; | |
| for (let i = 0; i < vals.length; i++) | |
| if (typeof vals[i] === "number" && !isNaN(vals[i])) | |
| rows.push({ v: vals[i], d: dates[i] }); | |
| if (key === "BodyRestingHeartRate") rows = rows.filter(r => r.v >= 25); | |
| if (key === "BodyBodyTemperature") rows = rows.filter(r => r.v >= 85); | |
| if (!rows.length) | |
| return { min: "N/A", max: "N/A", avg: "N/A", last: "N/A", trend: "—", delta: "—" }; | |
| rows.sort((a, b) => new Date(a.d) - new Date(b.d)); // oldest → newest | |
| let min = rows[0], max = rows[0], sum = 0; | |
| for (const r of rows) { | |
| if (r.v < min.v) min = r; | |
| if (r.v > max.v) max = r; | |
| sum += r.v; | |
| } | |
| const dec = key === "BodyBodyTemperature" ? 1 : 0; | |
| const avg = sum / rows.length; | |
| const last = rows[rows.length - 1]; | |
| const t0 = new Date(rows[0].d).getTime(); | |
| const xs = rows.map(r => (new Date(r.d).getTime() - t0) / 8.64e7); // days since start | |
| const ys = rows.map(r => r.v); | |
| const { a, b } = linReg(xs, ys); | |
| const firstPred = a; | |
| const lastPred = a + b * xs[xs.length - 1]; | |
| const changePct = ((lastPred - firstPred) / firstPred) * 100; | |
| const cfg = metrics.find(m => m.k === key); | |
| const upGood = cfg.better === "high"; | |
| const flatBand = 1.0; // ±1 % = flat band | |
| let arrow, color; | |
| if (Math.abs(changePct) < flatBand) { | |
| arrow = "➡"; | |
| color = "darkgray"; | |
| } else { | |
| const goingUp = changePct > 0; | |
| arrow = goingUp ? "▲" : "▼"; | |
| const good = upGood ? goingUp : !goingUp; | |
| color = good ? "limegreen" : "crimson"; | |
| } | |
| return { | |
| min: `${fmtNum(min.v, dec)} (${fmtDate(min.d)})`, | |
| max: `${fmtNum(max.v, dec)} (${fmtDate(max.d)})`, | |
| avg: fmtNum(avg, dec), | |
| last: fmtNum(last.v, dec), | |
| trend:`<span style="font-weight:bold;color:${color};">${arrow}</span>`, | |
| delta:`<span style="color:${color};">${changePct >= 0 ? "+" : ""}${fmtNum(changePct, 1)}%</span>` | |
| }; | |
| } | |
| /* ---------- renderer ---------- */ | |
| function drawTable() { | |
| const spanVal = dv.container.querySelector("#rangeSel").value; | |
| const since = spans.find(s => s.v === spanVal).since; | |
| const files = dv.pages('"Body/Oura"') | |
| .where(p => p.file.name.match(/^\d{4}-\d{2}-\d{2}$/)) | |
| .where(p => dv.date(p.file.name) >= since) | |
| .sort(p => p.file.name, 'desc'); | |
| const rows = metrics.map(m => { | |
| const vals = files.map(f => f[m.k]); | |
| const dates = files.map(f => f.file.name); | |
| const s = calcStats(vals, dates, m.k); | |
| return [m.label, s.min, s.max, s.avg, s.last, s.trend, s.delta]; | |
| }); | |
| const headers = ["Variable", "Min", "Max", "Average", "Last Value", "Trend", "Δ %"]; | |
| const html = ` | |
| <table style="width:100%;border-collapse:collapse;"> | |
| <tr>${headers.map(h => `<th style="border:1px solid #555;padding:6px;text-align:left;">${h}</th>`).join("")}</tr> | |
| ${rows.map((r, i) => { | |
| const bg = i % 2 === 0 ? "rgba(255,255,255,0.05)" : "transparent"; | |
| return `<tr style="background-color:${bg};"> | |
| ${r.map(c => `<td style="border:1px solid #555;padding:6px;">${c}</td>`).join("")} | |
| </tr>`; | |
| }).join("")} | |
| </table>`; | |
| dv.container.querySelector("#tblWrap").innerHTML = html; | |
| } | |
| drawTable(); | |
| dv.container.querySelector("#updBtn").addEventListener("click", drawTable); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment