Compare commits
2 Commits
6b12820592
...
8981214e55
| Author | SHA1 | Date | |
|---|---|---|---|
| 8981214e55 | |||
| b6301156f9 |
+53
-19
@@ -814,14 +814,40 @@
|
|||||||
if (e.target === lightbox) lightbox.close();
|
if (e.target === lightbox) lightbox.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------- daily charts (last 14 days) ----------
|
// ---------- chart window ----------
|
||||||
const CHART_DAYS = 14;
|
// 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) {
|
function weeklyData(events) {
|
||||||
const today = startOfDay(new Date());
|
const today = startOfDay(new Date());
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const days = [];
|
const days = [];
|
||||||
for (let i = CHART_DAYS - 1; i >= 0; i--) {
|
for (let i = chartDays() - 1; i >= 0; i--) {
|
||||||
const d = new Date(today);
|
const d = new Date(today);
|
||||||
d.setDate(d.getDate() - i);
|
d.setDate(d.getDate() - i);
|
||||||
const from = startOfDay(d).getTime();
|
const from = startOfDay(d).getTime();
|
||||||
@@ -848,10 +874,11 @@
|
|||||||
return date.toLocaleDateString(undefined, { weekday: "short" });
|
return date.toLocaleDateString(undefined, { weekday: "short" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// With 14 columns there is no room for a label under every bar, so label
|
// Wider windows can't fit a label under every bar: label today and every
|
||||||
// today and every second day counting back from it.
|
// 2nd (or 4th) day counting back from it, depending on the window.
|
||||||
function showDayLabel(i, len) {
|
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
|
// 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 rawMax = Math.max(...days.map(d => d.sleepHours));
|
||||||
const { yMax, steps: ySteps } = niceAxisSleepHours(rawMax);
|
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 barW = (innerW - (days.length - 1) * gap) / days.length;
|
||||||
|
|
||||||
const parts = [];
|
const parts = [];
|
||||||
@@ -964,8 +991,8 @@
|
|||||||
const rawMax = Math.max(...days.flatMap(d => [d.pees, d.poos, d.meals]));
|
const rawMax = Math.max(...days.flatMap(d => [d.pees, d.poos, d.meals]));
|
||||||
const { yMax, steps: ySteps } = niceAxis(rawMax);
|
const { yMax, steps: ySteps } = niceAxis(rawMax);
|
||||||
|
|
||||||
const groupGap = 4;
|
const groupGap = days.length > 14 ? 2 : 4;
|
||||||
const innerBarGap = 1.5;
|
const innerBarGap = days.length > 14 ? 0.5 : 1.5;
|
||||||
const groupW = (innerW - (days.length - 1) * groupGap) / days.length;
|
const groupW = (innerW - (days.length - 1) * groupGap) / days.length;
|
||||||
const barW = (groupW - 2 * innerBarGap) / 3;
|
const barW = (groupW - 2 * innerBarGap) / 3;
|
||||||
|
|
||||||
@@ -1026,7 +1053,7 @@
|
|||||||
|
|
||||||
const { yMax, steps: ySteps } = niceAxisGrams(Math.max(...days.map(d => d.grams)));
|
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 barW = (innerW - (days.length - 1) * gap) / days.length;
|
||||||
|
|
||||||
const parts = [];
|
const parts = [];
|
||||||
@@ -1067,8 +1094,7 @@
|
|||||||
drawGramsChart(days);
|
drawGramsChart(days);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- pattern charts (last 14 days) ----------
|
// ---------- pattern charts ----------
|
||||||
const PATTERN_DAYS = 14;
|
|
||||||
|
|
||||||
// Actogram: one row per day (oldest at top), a midnight-to-midnight track with
|
// 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
|
// the puppy's sleep shaded. Sleep windows are clipped to each day, so a night
|
||||||
@@ -1077,12 +1103,16 @@
|
|||||||
function renderSleepTimeline(events) {
|
function renderSleepTimeline(events) {
|
||||||
const svg = document.getElementById("chart-sleep-timeline");
|
const svg = document.getElementById("chart-sleep-timeline");
|
||||||
if (!svg) return;
|
if (!svg) return;
|
||||||
const N = PATTERN_DAYS;
|
const N = chartDays();
|
||||||
const W = 320, H = 228;
|
const W = 320;
|
||||||
const ML = 44, MR = 8, MT = 16, MB = 4;
|
const ML = 44, MR = 8, MT = 16, MB = 4;
|
||||||
const innerW = W - ML - MR;
|
const innerW = W - ML - MR;
|
||||||
const rowGap = 2;
|
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 dayMs = 86_400_000;
|
||||||
|
|
||||||
const today = startOfDay(new Date());
|
const today = startOfDay(new Date());
|
||||||
@@ -1132,7 +1162,7 @@
|
|||||||
function renderHourHeatmap(events) {
|
function renderHourHeatmap(events) {
|
||||||
const svg = document.getElementById("chart-hour-heatmap");
|
const svg = document.getElementById("chart-hour-heatmap");
|
||||||
if (!svg) return;
|
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 = [
|
const series = [
|
||||||
{ type: "pee", label: "Pees", cls: "hm-pee" },
|
{ type: "pee", label: "Pees", cls: "hm-pee" },
|
||||||
@@ -1337,9 +1367,8 @@
|
|||||||
|
|
||||||
// ---------- training ----------
|
// ---------- training ----------
|
||||||
// Per-exercise stats plus a consistency heatmap. Everything here is rolling
|
// 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.
|
// 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)
|
const expandedExercises = new Set(); // ids showing their instructions (per page load)
|
||||||
|
|
||||||
function trainingStreakDays(times) {
|
function trainingStreakDays(times) {
|
||||||
@@ -1374,7 +1403,7 @@
|
|||||||
if (exercises.length === 0) { wrap.hidden = true; return; }
|
if (exercises.length === 0) { wrap.hidden = true; return; }
|
||||||
wrap.hidden = false;
|
wrap.hidden = false;
|
||||||
|
|
||||||
const N = TRAINING_DAYS;
|
const N = chartDays();
|
||||||
const from = startOfDay(new Date());
|
const from = startOfDay(new Date());
|
||||||
from.setDate(from.getDate() - (N - 1));
|
from.setDate(from.getDate() - (N - 1));
|
||||||
const dayList = [];
|
const dayList = [];
|
||||||
@@ -1531,6 +1560,7 @@
|
|||||||
const events = live();
|
const events = live();
|
||||||
renderHeader();
|
renderHeader();
|
||||||
renderDayBar();
|
renderDayBar();
|
||||||
|
renderChartWindow();
|
||||||
renderBigClock(events);
|
renderBigClock(events);
|
||||||
renderStats(events);
|
renderStats(events);
|
||||||
renderLasts(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.value = ymd(new Date());
|
||||||
dayPicker.addEventListener("change", render);
|
dayPicker.addEventListener("change", render);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
[
|
[
|
||||||
|
{ "date": "2026-07-13", "text": "The top bar fits on one row: timer, day arrows, date picker and Today" },
|
||||||
|
{ "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": "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 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" },
|
{ "date": "2026-07-13", "text": "The sleep, daily counts and food charts now cover the last 14 days instead of 7" },
|
||||||
|
|||||||
+16
-11
@@ -86,8 +86,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<button type="button" id="day-prev" class="ghost" aria-label="Previous day">←</button>
|
<button type="button" id="day-prev" class="ghost" aria-label="Previous day">←</button>
|
||||||
<input type="date" id="day-picker" />
|
<input type="date" id="day-picker" />
|
||||||
<button type="button" id="day-today" class="ghost">Today</button>
|
|
||||||
<button type="button" id="day-next" class="ghost" aria-label="Next day">→</button>
|
<button type="button" id="day-next" class="ghost" aria-label="Next day">→</button>
|
||||||
|
<button type="button" id="day-today" class="ghost">Today</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="quick-actions">
|
<section class="quick-actions">
|
||||||
@@ -108,8 +108,8 @@
|
|||||||
<p id="training-empty" class="empty">No exercises yet. Add one to start tracking training.</p>
|
<p id="training-empty" class="empty">No exercises yet. Add one to start tracking training.</p>
|
||||||
<button type="button" id="exercise-add" class="ghost training-add">Add exercise</button>
|
<button type="button" id="exercise-add" class="ghost training-add">Add exercise</button>
|
||||||
<div class="chart training-chart" id="training-chart-wrap" hidden>
|
<div class="chart training-chart" id="training-chart-wrap" hidden>
|
||||||
<div class="chart-title">Consistency (last 14 days)</div>
|
<div class="chart-title">Consistency <span data-chart-days-label>(last 7 days)</span></div>
|
||||||
<svg id="chart-training" class="chart-svg" viewBox="0 0 320 60" role="img" aria-label="Training sessions per exercise per day over the last 14 days"></svg>
|
<svg id="chart-training" class="chart-svg" viewBox="0 0 320 60" role="img" aria-label="Training sessions per exercise per day"></svg>
|
||||||
<p class="muted-note">Darker = more sessions that day. Tap a cell to open that day.</p>
|
<p class="muted-note">Darker = more sessions that day. Tap a cell to open that day.</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -176,14 +176,19 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="weekly" data-panel="weekly">
|
<section class="weekly" data-panel="weekly">
|
||||||
<h2>Last 14 days</h2>
|
<h2 id="daily-charts-title">Last 7 days</h2>
|
||||||
|
<div class="chart-days-picker" role="group" aria-label="How many days the charts cover">
|
||||||
|
<button type="button" class="ghost" data-days="7">7d</button>
|
||||||
|
<button type="button" class="ghost" data-days="14">14d</button>
|
||||||
|
<button type="button" class="ghost" data-days="30">30d</button>
|
||||||
|
</div>
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
<div class="chart-title">Sleep (hours)</div>
|
<div class="chart-title">Sleep (hours)</div>
|
||||||
<svg id="chart-sleep" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Sleep hours per day for the last 14 days"></svg>
|
<svg id="chart-sleep" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Sleep hours per day"></svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
<div class="chart-title">Daily counts</div>
|
<div class="chart-title">Daily counts</div>
|
||||||
<svg id="chart-counts" class="chart-svg" viewBox="0 0 320 180" role="img" aria-label="Pee, poo and meal counts per day for the last 14 days"></svg>
|
<svg id="chart-counts" class="chart-svg" viewBox="0 0 320 180" role="img" aria-label="Pee, poo and meal counts per day"></svg>
|
||||||
<div class="legend">
|
<div class="legend">
|
||||||
<span class="lg pee"><span class="sw"></span>Pees</span>
|
<span class="lg pee"><span class="sw"></span>Pees</span>
|
||||||
<span class="lg poo"><span class="sw"></span>Poos</span>
|
<span class="lg poo"><span class="sw"></span>Poos</span>
|
||||||
@@ -192,19 +197,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="chart" id="grams-chart-wrap" hidden>
|
<div class="chart" id="grams-chart-wrap" hidden>
|
||||||
<div class="chart-title">Food (grams)</div>
|
<div class="chart-title">Food (grams)</div>
|
||||||
<svg id="chart-grams" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Grams of food eaten per day for the last 14 days"></svg>
|
<svg id="chart-grams" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Grams of food eaten per day"></svg>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="patterns" data-panel="sleep-timeline">
|
<section class="patterns" data-panel="sleep-timeline">
|
||||||
<h2><span id="sleep-timeline-title">When sleeping</span> <span class="muted-note">(last 14 days)</span></h2>
|
<h2><span id="sleep-timeline-title">When sleeping</span> <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
|
||||||
<svg id="chart-sleep-timeline" class="chart-svg" viewBox="0 0 320 228" role="img" aria-label="Sleep periods per day over the last 14 days"></svg>
|
<svg id="chart-sleep-timeline" class="chart-svg" viewBox="0 0 320 125" role="img" aria-label="Sleep periods per day"></svg>
|
||||||
<p class="muted-note">Each row is a day, midnight to midnight; shaded = asleep. Tap a row to open that day.</p>
|
<p class="muted-note">Each row is a day, midnight to midnight; shaded = asleep. Tap a row to open that day.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="patterns" data-panel="hour-heatmap">
|
<section class="patterns" data-panel="hour-heatmap">
|
||||||
<h2>By hour of day <span class="muted-note">(last 14 days)</span></h2>
|
<h2>By hour of day <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
|
||||||
<svg id="chart-hour-heatmap" class="chart-svg" viewBox="0 0 320 120" role="img" aria-label="Pee, poo and meal frequency by hour of day over the last 14 days"></svg>
|
<svg id="chart-hour-heatmap" class="chart-svg" viewBox="0 0 320 120" role="img" aria-label="Pee, poo and meal frequency by hour of day"></svg>
|
||||||
<p class="muted-note">Darker = happens more often at that hour.</p>
|
<p class="muted-note">Darker = happens more often at that hour.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
+27
-6
@@ -141,10 +141,10 @@ body::before {
|
|||||||
top: env(safe-area-inset-top, 0px);
|
top: env(safe-area-inset-top, 0px);
|
||||||
z-index: 60;
|
z-index: 60;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
}
|
}
|
||||||
.day-bar input[type="date"] {
|
.day-bar input[type="date"] {
|
||||||
@@ -153,9 +153,15 @@ body::before {
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
.day-bar button {
|
.day-bar button {
|
||||||
padding: 8px 14px;
|
padding: 8px 10px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
/* Keep the single row intact on narrow phones. */
|
||||||
|
@media (max-width: 370px) {
|
||||||
|
.day-bar { gap: 4px; }
|
||||||
|
.day-bar button { padding: 8px 7px; }
|
||||||
|
.bar-clock { font-size: 0.9rem; padding: 6px 8px; gap: 5px; }
|
||||||
|
}
|
||||||
.day-bar button:disabled {
|
.day-bar button:disabled {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
@@ -163,12 +169,12 @@ body::before {
|
|||||||
.bar-clock {
|
.bar-clock {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
margin-right: auto; /* pin left; the flexible gap sits between it and the day controls */
|
margin-right: auto; /* pin left; the flexible gap sits between it and the day controls */
|
||||||
padding: 8px 16px;
|
padding: 7px 10px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 1.15rem;
|
font-size: 1rem;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
@@ -515,6 +521,21 @@ dialog menu {
|
|||||||
.time-row input { flex: 1 1 120px; min-width: 0; }
|
.time-row input { flex: 1 1 120px; min-width: 0; }
|
||||||
.time-row button { padding: 8px 12px; }
|
.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 { margin-bottom: 16px; }
|
||||||
.chart:last-child { margin-bottom: 0; }
|
.chart:last-child { margin-bottom: 0; }
|
||||||
.chart-title {
|
.chart-title {
|
||||||
|
|||||||
Reference in New Issue
Block a user