Skip to content

Instantly share code, notes, and snippets.

@pedramamini
Created February 13, 2026 02:26
Show Gist options
  • Select an option

  • Save pedramamini/f26c1e7a8533cdc4fec64b0402d5fdec to your computer and use it in GitHub Desktop.

Select an option

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.
/********************************************************************
* 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