Add more statistics
This commit is contained in:
+48
@@ -473,6 +473,52 @@
|
|||||||
document.getElementById("stat-poos").textContent = count("poo");
|
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) {
|
function renderLasts(events) {
|
||||||
const setLast = (id, type) => {
|
const setLast = (id, type) => {
|
||||||
const ev = lastEventOfType(events, type);
|
const ev = lastEventOfType(events, type);
|
||||||
@@ -832,6 +878,7 @@
|
|||||||
renderBigClock(events);
|
renderBigClock(events);
|
||||||
renderStats(events);
|
renderStats(events);
|
||||||
renderLasts(events);
|
renderLasts(events);
|
||||||
|
renderTiming(events);
|
||||||
renderSleepWindows(events);
|
renderSleepWindows(events);
|
||||||
renderWakeWindows(events);
|
renderWakeWindows(events);
|
||||||
renderWeekly(events);
|
renderWeekly(events);
|
||||||
@@ -1169,6 +1216,7 @@
|
|||||||
renderBigClock(evs);
|
renderBigClock(evs);
|
||||||
renderStats(evs);
|
renderStats(evs);
|
||||||
renderLasts(evs);
|
renderLasts(evs);
|
||||||
|
renderTiming(evs);
|
||||||
renderSleepWindows(evs);
|
renderSleepWindows(evs);
|
||||||
renderWakeWindows(evs);
|
renderWakeWindows(evs);
|
||||||
renderWeekly(evs);
|
renderWeekly(evs);
|
||||||
|
|||||||
@@ -75,6 +75,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="timing">
|
||||||
|
<h2>Bathroom timing <span class="muted-note">(last 7 days)</span></h2>
|
||||||
|
<div class="lasts">
|
||||||
|
<div class="last-row"><span>Typical time between pees</span><span id="gap-pee">—</span></div>
|
||||||
|
<div class="last-row"><span>Shortest between pees</span><span id="gap-pee-min">—</span></div>
|
||||||
|
<div class="last-row"><span>Typical time between poos</span><span id="gap-poo">—</span></div>
|
||||||
|
<div class="last-row"><span>Shortest between poos</span><span id="gap-poo-min">—</span></div>
|
||||||
|
</div>
|
||||||
|
<p class="muted-note timing-hint" id="timing-hint"></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="sleep">
|
<section class="sleep">
|
||||||
<h2>Sleep windows</h2>
|
<h2>Sleep windows</h2>
|
||||||
<ul id="sleep-list" class="wake-list"></ul>
|
<ul id="sleep-list" class="wake-list"></ul>
|
||||||
|
|||||||
@@ -210,6 +210,13 @@ button.danger { background: var(--danger); }
|
|||||||
.last-row:last-child { border-bottom: none; }
|
.last-row:last-child { border-bottom: none; }
|
||||||
.last-row span:first-child { color: var(--muted); }
|
.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 {
|
.history-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user