Stats: heatmap to GitHub scale (365 days + month labels)
- heatmap now fetches /api/stats?days=365 (endpoint already caps at 365; no server change) so the grid spans the past year - week columns with month labels (Aug, Sep, ...), first column labelled by its first in-window day, final column padded - grid wrapped in a horizontal-scroll container (53 weeks ~ 750px)
This commit is contained in:
1 parent
7b670cff24
commit
df8307bf49
2 files changed
+46
-13
No files matched your search
+41
-12
@@ -488,17 +488,37 @@ function heatMap(byDay, days) {
|
|||||||
const start = new Date(win);
|
const start = new Date(win);
|
||||||
start.setDate(start.getDate() - (start.getDay() + 6) % 7); // back to Monday
|
start.setDate(start.getDate() - (start.getDay() + 6) % 7); // back to Monday
|
||||||
const max = Math.max(1, ...Object.values(byDay || {}));
|
const max = Math.max(1, ...Object.values(byDay || {}));
|
||||||
const box = el("div", "heat");
|
const MN = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||||
for (let d = new Date(start); d <= today; d.setDate(d.getDate() + 1)) {
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||||
const k = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
const grid = el("div", "heat");
|
||||||
const v = (byDay || {})[k] || 0;
|
const lbl = el("div", "heat-lbl");
|
||||||
const c = el("i");
|
let lastM = -1;
|
||||||
if (d < win) c.className = "none";
|
for (let d = new Date(start); d <= today; ) {
|
||||||
else if (v) {
|
const col = el("div", "heat-col");
|
||||||
c.className = "l" + Math.min(4, Math.ceil(v / max * 4));
|
const lab = el("span", "heat-lab");
|
||||||
c.title = k + " \u00b7 " + fmtMin(v / 60);
|
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");
|
const lg = el("div", "heat-lg");
|
||||||
lg.append(el("span", "muted", "less"));
|
lg.append(el("span", "muted", "less"));
|
||||||
@@ -508,7 +528,7 @@ function heatMap(byDay, days) {
|
|||||||
lg.append(s);
|
lg.append(s);
|
||||||
}
|
}
|
||||||
lg.append(el("span", "muted", "more"));
|
lg.append(el("span", "muted", "more"));
|
||||||
return [box, lg];
|
return [lbl, grid, lg];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showStats() {
|
async function showStats() {
|
||||||
@@ -518,8 +538,17 @@ async function showStats() {
|
|||||||
main.append(el("h2", null, "Stats \u2014 last 30 days"));
|
main.append(el("h2", null, "Stats \u2014 last 30 days"));
|
||||||
main.append(statsCards(d));
|
main.append(statsCards(d));
|
||||||
const hm = el("section", null);
|
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);
|
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);
|
const ta = topAnime(d);
|
||||||
if (ta) main.append(ta);
|
if (ta) main.append(ta);
|
||||||
if (d.top_searches.length) {
|
if (d.top_searches.length) {
|
||||||
|
|||||||
+5
-1
@@ -56,7 +56,11 @@ h2 { margin: 0.4rem 0; }
|
|||||||
.grid.stats { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
|
.grid.stats { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
|
||||||
.card.stat { padding: .8rem; gap: .2rem; }
|
.card.stat { padding: .8rem; gap: .2rem; }
|
||||||
.stat-value { font-size: 1.3rem; font-weight: 700; }
|
.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, .heat-lg i { width: 11px; height: 11px; border-radius: 2px; background: var(--bd); }
|
||||||
.heat i.none { background: transparent; }
|
.heat i.none { background: transparent; }
|
||||||
.heat i.l1, .heat-lg i.l1 { background: color-mix(in srgb, var(--acc) 30%, var(--bd)); }
|
.heat i.l1, .heat-lg i.l1 { background: color-mix(in srgb, var(--acc) 30%, var(--bd)); }
|
||||||
|
|||||||
Reference in new issue
Block a user