diff --git a/static/app.js b/static/app.js index 9b74774..506f7c8 100644 --- a/static/app.js +++ b/static/app.js @@ -488,17 +488,37 @@ function heatMap(byDay, days) { 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); + const MN = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + const grid = el("div", "heat"); + const lbl = el("div", "heat-lbl"); + let lastM = -1; + for (let d = new Date(start); d <= today; ) { + const col = el("div", "heat-col"); + const lab = el("span", "heat-lab"); + const labD = new Date(d); + for (let i = 0; i < 7 && labD < win; i++) labD.setDate(labD.getDate() + 1); + if (labD <= today && labD.getMonth() !== lastM) { + lab.textContent = MN[labD.getMonth()]; + lastM = labD.getMonth(); } - box.append(c); + lbl.append(lab); + for (let i = 0; i < 7; i++) { + const c = el("i"); + if (d > today) c.className = "none"; // pad the final column + else { + const k = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; + const v = (byDay || {})[k] || 0; + 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); + } + d.setDate(d.getDate() + 1); + } + col.append(c); + } + grid.append(col); } const lg = el("div", "heat-lg"); lg.append(el("span", "muted", "less")); @@ -508,7 +528,7 @@ function heatMap(byDay, days) { lg.append(s); } lg.append(el("span", "muted", "more")); - return [box, lg]; + return [lbl, grid, lg]; } async function showStats() { @@ -518,8 +538,17 @@ async function showStats() { main.append(el("h2", null, "Stats \u2014 last 30 days")); main.append(statsCards(d)); const hm = el("section", null); - hm.append(el("h2", null, "Watch time by day"), ...heatMap(d.by_day, d.days)); + const hmh = el("div", "sec-head"); + hmh.append(el("h2", null, "Watch time by day"), el("span", "muted", "past year")); + hm.append(hmh, el("p", "muted", "loading\u2026")); main.append(hm); + api("/api/stats?days=365").then(d2 => { + hm.querySelector("p.muted")?.remove(); + const [l, g, leg] = heatMap(d2.by_day, 365); + const w = el("div", "heat-wrap"); + w.append(l, g); + hm.append(w, leg); + }).catch(() => hm.querySelector("p.muted")?.remove()); const ta = topAnime(d); if (ta) main.append(ta); if (d.top_searches.length) { diff --git a/static/style.css b/static/style.css index dde09d6..7bbdbcf 100644 --- a/static/style.css +++ b/static/style.css @@ -56,7 +56,11 @@ h2 { margin: 0.4rem 0; } .grid.stats { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); } .card.stat { padding: .8rem; gap: .2rem; } .stat-value { font-size: 1.3rem; font-weight: 700; } -.heat { display: grid; grid-auto-flow: column; grid-template-rows: repeat(7, 11px); gap: 3px; width: fit-content; margin: .5rem 0 .3rem; } +.heat-wrap { overflow-x: auto; margin: .5rem 0 .3rem; } +.heat-lbl { display: flex; gap: 3px; margin-bottom: 2px; } +.heat-lbl .heat-lab { width: 14px; font-size: .65rem; color: var(--muted); white-space: nowrap; } +.heat { display: flex; gap: 3px; } +.heat-col { display: flex; flex-direction: column; gap: 3px; } .heat i, .heat-lg i { width: 11px; height: 11px; border-radius: 2px; background: var(--bd); } .heat i.none { background: transparent; } .heat i.l1, .heat-lg i.l1 { background: color-mix(in srgb, var(--acc) 30%, var(--bd)); }