Let the chart window be picked: 7, 14 or 30 days (default 7)
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.
This commit is contained in:
+53
-19
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user