diff --git a/src/app.js b/src/app.js index f4ff3ec..57ea2c7 100644 --- a/src/app.js +++ b/src/app.js @@ -1349,18 +1349,25 @@ setChartSVG(svg, parts); // wires the .bar[data-day] click → select that day } + // Which heatmap block is focused, remembered across re-renders so a background + // sync doesn't wipe the block the user just tapped. The counts behind it are + // recomputed every render, so the readout stays current. + let hourCellSel = null; // `${type}-${hour}`, or null for nothing focused + const HOUR_CELL_HINT = "Tap a block to see how many it counts."; + // Hour-of-day heatmap: one row per event type, 24 cells shaded by how often // that event lands in each hour across the window. Reveals daily rhythm that // the median gap can't show (e.g. "always poos ~7am and ~6pm"). function renderHourHeatmap(events) { const svg = document.getElementById("chart-hour-heatmap"); if (!svg) return; + const info = document.getElementById("hour-heatmap-info"); const from = startOfDay(new Date(Date.now() - (chartDays() - 1) * 86_400_000)).getTime(); const series = [ - { type: "pee", label: "Pees", cls: "hm-pee" }, - { type: "poo", label: "Poos", cls: "hm-poo" }, - { type: "eat", label: "Meals", cls: "hm-eat" }, + { type: "pee", label: "Pees", cls: "hm-pee", one: "pee", many: "pees" }, + { type: "poo", label: "Poos", cls: "hm-poo", one: "poo", many: "poos" }, + { type: "eat", label: "Meals", cls: "hm-eat", one: "meal", many: "meals" }, ]; const counts = {}; for (const s of series) counts[s.type] = new Array(24).fill(0); @@ -1376,7 +1383,15 @@ const rowH = (H - MT - MB - (series.length - 1) * rowGap) / series.length; const cellW = innerW / 24; + // "3 meals between 07:00 and 08:00" — self-describing, so the same string + // serves as the pointer tooltip and as the tap readout under the chart. + const cellText = (s, h, c) => + `${c === 0 ? "No" : c} ${c === 1 ? s.one : s.many} between ` + + `${pad2(h)}:00 and ${pad2((h + 1) % 24)}:00`; + const parts = []; + const hits = []; // appended last so they sit above every row + const details = {}; // cell key → readout text series.forEach((s, r) => { const y = MT + r * (rowH + rowGap); const max = Math.max(1, ...counts[s.type]); @@ -1384,11 +1399,18 @@ const c = counts[s.type][h]; const op = c === 0 ? 0.06 : 0.2 + 0.8 * (c / max); const x = ML + h * cellW; - const range = `${pad2(h)}:00–${pad2((h + 1) % 24)}:00`; + const key = `${s.type}-${h}`; + details[key] = cellText(s, h, c); parts.push( - `` + - `${escapeText(`${s.label} · ${range}: ${c}`)}` + `` + ); + // A cell is only ~12px wide on a phone, so the tap target claims the + // spacing between cells and half the gap to the neighbouring rows. + hits.push( + `` + + `${escapeText(details[key])}` ); } parts.push(`${s.label}`); @@ -1401,7 +1423,23 @@ } parts.push(`24h`); - svg.innerHTML = parts.join(""); + svg.innerHTML = parts.concat(hits).join(""); + + // tooltips only ever show on a pointer, which left phones with no + // way to read a block's count. Tap/hover names the block under the chart; + // tapping the focused block again clears it. + const cells = svg.querySelectorAll(".hm-cell[data-cell]"); + const focusCell = (key) => { + hourCellSel = details[key] ? key : null; + cells.forEach(c => c.classList.toggle("hm-active", c.dataset.cell === hourCellSel)); + if (info) info.textContent = hourCellSel ? details[hourCellSel] : HOUR_CELL_HINT; + }; + svg.querySelectorAll(".hm-hit").forEach(hit => { + const key = hit.dataset.cell; + hit.addEventListener("click", () => focusCell(hourCellSel === key ? null : key)); + hit.addEventListener("mouseenter", () => focusCell(key)); + }); + focusCell(hourCellSel); } // ---------- sleep trend ---------- diff --git a/src/changelog.json b/src/changelog.json index d22989b..010f99a 100644 --- a/src/changelog.json +++ b/src/changelog.json @@ -1,4 +1,5 @@ [ + { "date": "2026-08-24", "text": "The “By hour of day” chart is now tappable: tap any block to read what it counts (“3 meals between 07:00 and 08:00”) just under the chart — until now that number only showed as a hover tooltip, which phones never get. The block you tapped gets a ring; tap it again to clear it" }, { "date": "2026-08-21", "text": "The sleep timer pill in the day bar — the one that appears once you've scrolled past the big timer — is now tappable: tap it while the puppy is asleep to log the wake-up, or while awake to log a sleep start, without scrolling back up to the buttons" }, { "date": "2026-08-20", "text": "Added reminders: turn them on in Settings and your phone gets a notification when it's time to sleep (\"Awake for 45 min\") or when there's been no pee, poo or meal for a while. Each one has its own interval, they arrive even with the app closed, and they stay quiet while the puppy is logged as asleep so you're not nagged all night. On iPhone, add Puppy Tracker to your Home Screen first — iOS only allows notifications for installed apps" }, { "date": "2026-08-18", "text": "The sleep button that would just repeat the last one is now disabled: while asleep you can only tap ⏰ Sleep end, and while awake only 😴 Sleep start — no more accidental double taps creating zero-length sleep windows. If you did miss a boundary, you can still add it at the right time from the event log" }, diff --git a/src/index.html b/src/index.html index e11c5d3..9579e9a 100644 --- a/src/index.html +++ b/src/index.html @@ -240,6 +240,7 @@ <section class="patterns" data-panel="hour-heatmap"> <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"></svg> + <p id="hour-heatmap-info" class="muted-note hour-point-info" aria-live="polite"></p> <p class="muted-note">Darker = happens more often at that hour.</p> </section> diff --git a/src/style.css b/src/style.css index 52b2bc6..4edaecd 100644 --- a/src/style.css +++ b/src/style.css @@ -662,7 +662,8 @@ dialog menu { stroke-width: 1.5; } -.weight-point-info { +.weight-point-info, +.hour-point-info { margin: 6px 4px 0; min-height: 1.2em; text-align: center; @@ -1013,6 +1014,13 @@ input.switch:checked::after { transform: translateX(18px); } .lg[hidden] { display: none; } .chart-svg .hm-cell { stroke: none; } +/* Focused block: an accent ring, drawn at full opacity so it stays visible on + the palest (zero-count) cells. */ +.chart-svg .hm-cell.hm-active { + stroke: var(--accent); + stroke-width: 1.2; +} +.chart-svg .hm-hit { fill: transparent; cursor: pointer; } .chart-svg .hm-pee { fill: var(--pee); } .chart-svg .hm-poo { fill: var(--poo); } .chart-svg .hm-eat { fill: var(--eat); }