From b6301156f9e68c7bf72071fe1364bec072c9e807 Mon Sep 17 00:00:00 2001 From: Alexander Heldt Date: Mon, 13 Jul 2026 21:20:31 +0000 Subject: [PATCH] Let the chart window be picked: 7, 14 or 30 days (default 7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A 7d/14d/30d picker in the charts card sets how many days every rolling chart covers — daily bars, sleep timeline, hour heatmap and training consistency grid. The choice is stored per device like the theme; headers show the current window, day labels thin out and bar gaps tighten as the window widens, and the sleep timeline grows rows to fit. --- src/app.js | 72 ++++++++++++++++++++++++++++++++++------------ src/changelog.json | 1 + src/index.html | 25 +++++++++------- src/style.css | 15 ++++++++++ 4 files changed, 84 insertions(+), 29 deletions(-) diff --git a/src/app.js b/src/app.js index 2793525..dfeb191 100644 --- a/src/app.js +++ b/src/app.js @@ -814,14 +814,40 @@ if (e.target === lightbox) lightbox.close(); }); - // ---------- daily charts (last 14 days) ---------- - const CHART_DAYS = 14; + // ---------- chart window ---------- + // How many days the rolling charts cover. Device-global (like the theme), + // picked via the 7d/14d/30d buttons in the charts card and applied to every + // day-window chart: daily bars, sleep timeline, hour heatmap, training grid. + const CHART_DAYS_KEY = "puppy-tracker:chart-days:v1"; + const CHART_DAY_CHOICES = [7, 14, 30]; + function chartDays() { + const v = Number(localStorage.getItem(CHART_DAYS_KEY)); + return CHART_DAY_CHOICES.includes(v) ? v : 7; + } + function setChartDays(n) { + try { localStorage.setItem(CHART_DAYS_KEY, String(n)); } catch { /* ignore */ } + render(); + } + // Sync every "(last N days)" header and the picker's active button. + function renderChartWindow() { + const n = chartDays(); + const title = document.getElementById("daily-charts-title"); + if (title) title.textContent = `Last ${n} days`; + document.querySelectorAll("[data-chart-days-label]").forEach(el => { + el.textContent = `(last ${n} days)`; + }); + document.querySelectorAll(".chart-days-picker button").forEach(b => { + b.classList.toggle("active", Number(b.dataset.days) === n); + }); + } + + // ---------- daily charts ---------- function weeklyData(events) { const today = startOfDay(new Date()); const now = Date.now(); const days = []; - for (let i = CHART_DAYS - 1; i >= 0; i--) { + for (let i = chartDays() - 1; i >= 0; i--) { const d = new Date(today); d.setDate(d.getDate() - i); const from = startOfDay(d).getTime(); @@ -848,10 +874,11 @@ return date.toLocaleDateString(undefined, { weekday: "short" }); } - // With 14 columns there is no room for a label under every bar, so label - // today and every second day counting back from it. + // Wider windows can't fit a label under every bar: label today and every + // 2nd (or 4th) day counting back from it, depending on the window. function showDayLabel(i, len) { - return (len - 1 - i) % 2 === 0; + const every = len <= 8 ? 1 : len <= 16 ? 2 : 4; + return (len - 1 - i) % every === 0; } // Pick a chart Y maximum and tick count so every tick label is a clean @@ -920,7 +947,7 @@ const rawMax = Math.max(...days.map(d => d.sleepHours)); const { yMax, steps: ySteps } = niceAxisSleepHours(rawMax); - const gap = 4; + const gap = days.length > 14 ? 2 : 4; const barW = (innerW - (days.length - 1) * gap) / days.length; const parts = []; @@ -964,8 +991,8 @@ const rawMax = Math.max(...days.flatMap(d => [d.pees, d.poos, d.meals])); const { yMax, steps: ySteps } = niceAxis(rawMax); - const groupGap = 4; - const innerBarGap = 1.5; + const groupGap = days.length > 14 ? 2 : 4; + const innerBarGap = days.length > 14 ? 0.5 : 1.5; const groupW = (innerW - (days.length - 1) * groupGap) / days.length; const barW = (groupW - 2 * innerBarGap) / 3; @@ -1026,7 +1053,7 @@ const { yMax, steps: ySteps } = niceAxisGrams(Math.max(...days.map(d => d.grams))); - const gap = 4; + const gap = days.length > 14 ? 2 : 4; const barW = (innerW - (days.length - 1) * gap) / days.length; const parts = []; @@ -1067,8 +1094,7 @@ drawGramsChart(days); } - // ---------- pattern charts (last 14 days) ---------- - const PATTERN_DAYS = 14; + // ---------- pattern charts ---------- // Actogram: one row per day (oldest at top), a midnight-to-midnight track with // the puppy's sleep shaded. Sleep windows are clipped to each day, so a night @@ -1077,12 +1103,16 @@ function renderSleepTimeline(events) { const svg = document.getElementById("chart-sleep-timeline"); if (!svg) return; - const N = PATTERN_DAYS; - const W = 320, H = 228; + const N = chartDays(); + const W = 320; const ML = 44, MR = 8, MT = 16, MB = 4; const innerW = W - ML - MR; const rowGap = 2; - const rowH = (H - MT - MB - (N - 1) * rowGap) / N; + // Fixed row height, chart grows with the window (13px × 14 days matches + // the original 228-high viewBox; wider windows use thinner rows). + const rowH = N > 14 ? 9 : 13; + const H = MT + MB + N * rowH + (N - 1) * rowGap; + svg.setAttribute("viewBox", `0 0 ${W} ${H}`); const dayMs = 86_400_000; const today = startOfDay(new Date()); @@ -1132,7 +1162,7 @@ function renderHourHeatmap(events) { const svg = document.getElementById("chart-hour-heatmap"); if (!svg) return; - const from = startOfDay(new Date(Date.now() - (PATTERN_DAYS - 1) * 86_400_000)).getTime(); + const from = startOfDay(new Date(Date.now() - (chartDays() - 1) * 86_400_000)).getTime(); const series = [ { type: "pee", label: "Pees", cls: "hm-pee" }, @@ -1337,9 +1367,8 @@ // ---------- training ---------- // Per-exercise stats plus a consistency heatmap. Everything here is rolling - // (last session / last 7 days / streak / last 14 days) rather than scoped to + // (last session / last 7 days / streak / chart window) rather than scoped to // the day picker — the point is keeping the habit up, not reviewing one day. - const TRAINING_DAYS = 14; const expandedExercises = new Set(); // ids showing their instructions (per page load) function trainingStreakDays(times) { @@ -1374,7 +1403,7 @@ if (exercises.length === 0) { wrap.hidden = true; return; } wrap.hidden = false; - const N = TRAINING_DAYS; + const N = chartDays(); const from = startOfDay(new Date()); from.setDate(from.getDate() - (N - 1)); const dayList = []; @@ -1531,6 +1560,7 @@ const events = live(); renderHeader(); renderDayBar(); + renderChartWindow(); renderBigClock(events); renderStats(events); renderLasts(events); @@ -2275,6 +2305,10 @@ }); }); + document.querySelectorAll(".chart-days-picker button").forEach(b => { + b.addEventListener("click", () => setChartDays(Number(b.dataset.days))); + }); + dayPicker.value = ymd(new Date()); dayPicker.addEventListener("change", render); diff --git a/src/changelog.json b/src/changelog.json index f13952a..1d52d5b 100644 --- a/src/changelog.json +++ b/src/changelog.json @@ -1,4 +1,5 @@ [ + { "date": "2026-07-13", "text": "Pick how many days the charts cover — 7, 14 or 30 (default 7)" }, { "date": "2026-07-13", "text": "Much finer y-axis on the food chart" }, { "date": "2026-07-13", "text": "The awake/asleep timer is bigger and sits first in the top bar" }, { "date": "2026-07-13", "text": "The sleep, daily counts and food charts now cover the last 14 days instead of 7" }, diff --git a/src/index.html b/src/index.html index d52c5e2..48064bd 100644 --- a/src/index.html +++ b/src/index.html @@ -108,8 +108,8 @@

No exercises yet. Add one to start tracking training.

@@ -176,14 +176,19 @@
-

Last 14 days

+

Last 7 days

+
+ + + +
Sleep (hours)
- +
Daily counts
- +
Pees Poos @@ -192,19 +197,19 @@
-

When sleeping (last 14 days)

- +

When sleeping (last 7 days)

+

Each row is a day, midnight to midnight; shaded = asleep. Tap a row to open that day.

-

By hour of day (last 14 days)

- +

By hour of day (last 7 days)

+

Darker = happens more often at that hour.

diff --git a/src/style.css b/src/style.css index 8ad64b3..f0e1ea8 100644 --- a/src/style.css +++ b/src/style.css @@ -515,6 +515,21 @@ dialog menu { .time-row input { flex: 1 1 120px; min-width: 0; } .time-row button { padding: 8px 12px; } +.chart-days-picker { + display: flex; + gap: 6px; + margin: -4px 0 14px; +} +.chart-days-picker button { + padding: 5px 12px; + font-size: 0.8rem; +} +.chart-days-picker button.active { + background: var(--accent); + border-color: transparent; + color: #fff; +} + .chart { margin-bottom: 16px; } .chart:last-child { margin-bottom: 0; } .chart-title {