Stats: GitHub-style watch-time heatmap
- api/stats gains by_day: local-date -> seconds (same watch-time heuristic as by_weekday) - ?stats renders a Mon..Sun x week grid (30d window, matches the page header) with 4 intensity levels + tooltip per cell + legend; replaces the by-weekday bar chart the heatmap subsumes - test_stats covers by_day (sums, slug-scoped, empty case)
This commit is contained in:
1 parent
7d9fab6bd9
commit
7b670cff24
4 files changed
+52
-20
No files matched your search
+32
-12
@@ -482,24 +482,44 @@ function histBoard(items, limit = 0) {
|
||||
return [chips, grid];
|
||||
}
|
||||
|
||||
function heatMap(byDay, days) {
|
||||
const today = new Date(); today.setHours(12, 0, 0, 0);
|
||||
const win = new Date(today); win.setDate(win.getDate() - (days - 1));
|
||||
const start = new Date(win);
|
||||
start.setDate(start.getDate() - (start.getDay() + 6) % 7); // back to Monday
|
||||
const max = Math.max(1, ...Object.values(byDay || {}));
|
||||
const box = el("div", "heat");
|
||||
for (let d = new Date(start); d <= today; d.setDate(d.getDate() + 1)) {
|
||||
const k = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
const v = (byDay || {})[k] || 0;
|
||||
const c = el("i");
|
||||
if (d < win) c.className = "none";
|
||||
else if (v) {
|
||||
c.className = "l" + Math.min(4, Math.ceil(v / max * 4));
|
||||
c.title = k + " \u00b7 " + fmtMin(v / 60);
|
||||
}
|
||||
box.append(c);
|
||||
}
|
||||
const lg = el("div", "heat-lg");
|
||||
lg.append(el("span", "muted", "less"));
|
||||
for (const cls of ["", "l1", "l2", "l3", "l4"]) {
|
||||
const s = el("i");
|
||||
if (cls) s.className = cls;
|
||||
lg.append(s);
|
||||
}
|
||||
lg.append(el("span", "muted", "more"));
|
||||
return [box, lg];
|
||||
}
|
||||
|
||||
async function showStats() {
|
||||
skel(5);
|
||||
const d = await api("/api/stats?days=30");
|
||||
main.innerHTML = "";
|
||||
main.append(el("h2", null, "Stats \u2014 last 30 days"));
|
||||
main.append(statsCards(d));
|
||||
const WD = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||
const mx = Math.max(1, ...d.by_weekday);
|
||||
const wk = el("div", "wk");
|
||||
d.by_weekday.forEach((s, i) => {
|
||||
const col = el("div", "wk-col");
|
||||
const b = el("div", "wk-bar");
|
||||
b.style.height = Math.max(3, Math.round(s / mx * 50)) + "px";
|
||||
b.title = fmtMin(s / 60);
|
||||
col.append(b, el("div", "wk-day", WD[i]));
|
||||
wk.append(col);
|
||||
});
|
||||
main.append(wk);
|
||||
const hm = el("section", null);
|
||||
hm.append(el("h2", null, "Watch time by day"), ...heatMap(d.by_day, d.days));
|
||||
main.append(hm);
|
||||
const ta = topAnime(d);
|
||||
if (ta) main.append(ta);
|
||||
if (d.top_searches.length) {
|
||||
|
||||
Reference in new issue
Block a user