diff --git a/src/app.js b/src/app.js index 2e0502f..9dd7338 100644 --- a/src/app.js +++ b/src/app.js @@ -473,6 +473,52 @@ document.getElementById("stat-poos").textContent = count("poo"); } + // Gaps (ms) between consecutive events of `type` logged within the last + // `days` days, sorted ascending. These intervals are what tell you how + // often the puppy needs to go out. + function gapsBetween(events, type, days = 7) { + const cutoff = startOfDay(new Date()); + cutoff.setDate(cutoff.getDate() - (days - 1)); + const from = cutoff.getTime(); + const times = events + .filter(e => e.type === type && e.at >= from) + .map(e => e.at) + .sort((a, b) => a - b); + const gaps = []; + for (let i = 1; i < times.length; i++) gaps.push(times[i] - times[i - 1]); + return gaps.sort((a, b) => a - b); + } + + // Median is used for the "typical" gap because it ignores the single long + // overnight gap each day, so it reflects real daytime frequency. + function median(sorted) { + if (sorted.length === 0) return null; + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; + } + + function renderTiming(events) { + const peeGaps = gapsBetween(events, "pee"); + const pooGaps = gapsBetween(events, "poo"); + const show = (id, ms) => { + document.getElementById(id).textContent = ms == null ? "—" : formatDuration(ms); + }; + show("gap-pee", median(peeGaps)); + show("gap-pee-min", peeGaps[0] ?? null); + show("gap-poo", median(pooGaps)); + show("gap-poo-min", pooGaps[0] ?? null); + + const hint = document.getElementById("timing-hint"); + const typicalPee = median(peeGaps); + if (typicalPee != null) { + hint.textContent = + `Based on ${peeGaps.length} pee gap${peeGaps.length === 1 ? "" : "s"}. ` + + `Aim to take the puppy out a little before the typical ${formatDuration(typicalPee)} mark.`; + } else { + hint.textContent = "Log a few more pees and poos to see typical timings."; + } + } + function renderLasts(events) { const setLast = (id, type) => { const ev = lastEventOfType(events, type); @@ -832,6 +878,7 @@ renderBigClock(events); renderStats(events); renderLasts(events); + renderTiming(events); renderSleepWindows(events); renderWakeWindows(events); renderWeekly(events); @@ -1169,6 +1216,7 @@ renderBigClock(evs); renderStats(evs); renderLasts(evs); + renderTiming(evs); renderSleepWindows(evs); renderWakeWindows(evs); renderWeekly(evs); diff --git a/src/index.html b/src/index.html index 3579e4f..0d8546d 100644 --- a/src/index.html +++ b/src/index.html @@ -75,6 +75,17 @@ + + Bathroom timing (last 7 days) + + Typical time between pees— + Shortest between pees— + Typical time between poos— + Shortest between poos— + + + + Sleep windows diff --git a/src/style.css b/src/style.css index 25f15de..50dddb9 100644 --- a/src/style.css +++ b/src/style.css @@ -210,6 +210,13 @@ button.danger { background: var(--danger); } .last-row:last-child { border-bottom: none; } .last-row span:first-child { color: var(--muted); } +.muted-note { + color: var(--muted); + font-size: 0.8rem; + font-weight: normal; +} +.timing-hint { margin: 10px 4px 0; line-height: 1.4; } + .history-controls { display: flex; align-items: center;