diff --git a/src/app.js b/src/app.js index 6a21c5d..71a6896 100644 --- a/src/app.js +++ b/src/app.js @@ -222,26 +222,27 @@ localStorage.setItem(CONFIG_KEY, JSON.stringify(cfg)); } - // Age in whole days / weeks / calendar months from a "YYYY-MM-DD" birthday. - // Returns null for a missing/invalid/future birthday. - function ageParts(birthday) { + // Age in whole days / weeks / calendar months from a "YYYY-MM-DD" birthday, + // measured at `at` (defaults to now — pass a weigh-in's timestamp for its age + // at that point). Returns null for a missing/invalid birthday or a date before it. + function ageParts(birthday, at) { if (!birthday) return null; const [y, mo, d] = birthday.split("-").map(Number); if (!y || !mo || !d) return null; const birth = startOfDay(new Date(y, mo - 1, d)); - const now = startOfDay(new Date()); - if (birth > now) return null; - const days = Math.floor((now - birth) / 86_400_000); + const ref = startOfDay(new Date(Number.isFinite(at) ? at : Date.now())); + if (birth > ref) return null; + const days = Math.floor((ref - birth) / 86_400_000); const weeks = Math.floor(days / 7); - let months = (now.getFullYear() - birth.getFullYear()) * 12 + - (now.getMonth() - birth.getMonth()); - if (now.getDate() < birth.getDate()) months--; + let months = (ref.getFullYear() - birth.getFullYear()) * 12 + + (ref.getMonth() - birth.getMonth()); + if (ref.getDate() < birth.getDate()) months--; if (months < 0) months = 0; return { days, weeks, months }; } - function formatAge(birthday) { - const a = ageParts(birthday); + function formatAge(birthday, at) { + const a = ageParts(birthday, at); if (!a) return ""; const wk = `${a.weeks} week${a.weeks === 1 ? "" : "s"}`; if (a.months < 1) return `${wk} old`; @@ -939,14 +940,23 @@ return { lo, hi, steps: Math.max(1, Math.round((hi - lo) / step)) }; } - function drawWeightChart(weights) { - const svg = document.getElementById("chart-weight"); + // Human-readable detail for one weigh-in: date, weight, and the puppy's age + // at that date (omitted if no birthday is configured). + function weightPointInfo(w, birthday) { + const dateStr = new Date(w.at).toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" }); + const age = formatAge(birthday, w.at); + return `${dateStr} — ${formatWeight(w.weight)}${age ? ` · ${age}` : ""}`; + } + + function drawWeightChart(weights, birthday) { + const svg = document.getElementById("chart-weight"); + const info = document.getElementById("weight-point-info"); const W = 320, H = 180; const ML = 30, MR = 8, MT = 10, MB = 24; const innerW = W - ML - MR; const innerH = H - MT - MB; - if (weights.length === 0) { svg.innerHTML = ""; return; } + if (weights.length === 0) { svg.innerHTML = ""; info.textContent = ""; return; } const vals = weights.map(w => w.weight); const { lo, hi, steps } = niceWeightAxis(Math.min(...vals), Math.max(...vals)); @@ -972,11 +982,16 @@ parts.push(``); } + // Visible dots, then larger transparent hit targets on top (easier to tap + // on touch, and they carry the tooltip + click detail). weights.forEach(w => { - const title = `${new Date(w.at).toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" })} — ${formatWeight(w.weight)}`; + parts.push(``); + }); + weights.forEach((w, i) => { + const detail = weightPointInfo(w, birthday); parts.push( - `` + - `${escapeText(title)}` + `` + + `${escapeText(detail)}` ); }); @@ -987,6 +1002,19 @@ } svg.innerHTML = parts.join(""); + + // Default the caption to the most recent weigh-in; hover/tap focuses a point. + const hits = svg.querySelectorAll(".weight-hit"); + const focus = (i) => { + info.textContent = weightPointInfo(weights[i], birthday); + hits.forEach(h => h.classList.toggle("active", Number(h.dataset.i) === i)); + }; + hits.forEach(h => { + const i = Number(h.dataset.i); + h.addEventListener("mouseenter", () => focus(i)); + h.addEventListener("click", () => focus(i)); + }); + info.textContent = weightPointInfo(weights[weights.length - 1], birthday); } function renderWeight(events) { @@ -999,6 +1027,8 @@ const changeEl = document.getElementById("weight-change"); const list = document.getElementById("weight-list"); + const birthday = loadConfig().birthday; + list.innerHTML = ""; changeEl.classList.remove("up", "down"); @@ -1006,7 +1036,7 @@ empty.hidden = false; latestEl.textContent = "—"; changeEl.textContent = "—"; - drawWeightChart([]); + drawWeightChart([], birthday); return; } empty.hidden = true; @@ -1031,7 +1061,9 @@ li.className = "ww weight-ww"; const date = document.createElement("span"); date.className = "ww-range"; - date.textContent = new Date(w.at).toLocaleDateString(undefined, { month: "short", day: "numeric" }); + const age = formatAge(birthday, w.at); + const dateStr = new Date(w.at).toLocaleDateString(undefined, { month: "short", day: "numeric" }); + date.textContent = age ? `${dateStr} · ${age}` : dateStr; const val = document.createElement("span"); val.className = "ww-dur"; val.textContent = formatWeight(w.weight); @@ -1041,7 +1073,7 @@ list.appendChild(li); } - drawWeightChart(weights); + drawWeightChart(weights, birthday); } function renderDayBar() { diff --git a/src/index.html b/src/index.html index 23189bf..4fe4b4d 100644 --- a/src/index.html +++ b/src/index.html @@ -137,6 +137,7 @@
Weight (kg)
+

No weigh-ins logged yet.

diff --git a/src/style.css b/src/style.css index f7de84e..26d88c3 100644 --- a/src/style.css +++ b/src/style.css @@ -491,6 +491,18 @@ dialog menu { .chart-svg .bar-eat { fill: var(--eat); } .chart-svg .weight-line { stroke: var(--weight); stroke-width: 2; fill: none; } .chart-svg .weight-dot { fill: var(--weight); } +.chart-svg .weight-hit { fill: transparent; cursor: pointer; } +.chart-svg .weight-hit.active { + fill: color-mix(in srgb, var(--weight) 22%, transparent); + stroke: var(--weight); + stroke-width: 1.5; +} + +.weight-point-info { + margin: 6px 4px 0; + min-height: 1.2em; + text-align: center; +} .weight-summary { display: grid; diff --git a/src/sw.js b/src/sw.js index 6fec2b3..d92b650 100644 --- a/src/sw.js +++ b/src/sw.js @@ -1,4 +1,4 @@ -const CACHE = "puppy-tracker-v4"; +const CACHE = "puppy-tracker-v5"; const PHOTO_CACHE = "puppy-tracker-photos-v1"; const ASSETS = [ "./",