From cd677d924412e0bbe556966990240cb53967d5c9 Mon Sep 17 00:00:00 2001 From: Arnaud Nelissen Date: Mon, 15 Jun 2026 17:11:08 +0200 Subject: [PATCH] fix(growth): show distinct points for same-week measurements Use fractional week values (float) for actual data points instead of floor'd integers. Two measurements on different days within the same week now appear at distinct x positions. XAxis set to type="number" with continuous domain so the chart renders them correctly. Co-Authored-By: Claude Sonnet 4.6 --- src/app/(app)/growth/page.tsx | 40 ++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/app/(app)/growth/page.tsx b/src/app/(app)/growth/page.tsx index e14f9bd..f5daa4c 100644 --- a/src/app/(app)/growth/page.tsx +++ b/src/app/(app)/growth/page.tsx @@ -55,47 +55,49 @@ function filterLogsByRange(logs: GrowthLog[], range: TimeRange): GrowthLog[] { return logs.filter((l) => new Date(l.date) >= cutoff); } +const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000; +function ageWeeksFloat(birthDate: string, measureDate: string): number { + return (new Date(measureDate).getTime() - new Date(birthDate).getTime()) / MS_PER_WEEK; +} + function buildWeightChartData(logs: GrowthLog[], baby: BabyShape, allLogs: GrowthLog[]) { const actualPoints = logs .filter((l) => l.weight) - .map((l) => ({ - ageWeeks: getAgeInWeeks(new Date(baby.birthDate), new Date(l.date)), - weight: l.weight! / 1000, - })); + .map((l) => ({ ageWeeks: ageWeeksFloat(baby.birthDate, l.date), weight: l.weight! / 1000 })); if (actualPoints.length === 0) return []; const maxWeek = Math.max(...actualPoints.map((p) => p.ageWeeks)); - // minWeek: either earliest in filtered set, or 0 if showing all - const allPoints = allLogs.filter((l) => l.weight).map((l) => - getAgeInWeeks(new Date(baby.birthDate), new Date(l.date)) - ); - const globalMin = allPoints.length ? Math.min(...allPoints) : 0; + const globalMin = allLogs.filter((l) => l.weight).reduce((min, l) => { + const w = ageWeeksFloat(baby.birthDate, l.date); + return w < min ? w : min; + }, Infinity); const localMin = Math.min(...actualPoints.map((p) => p.ageWeeks)); - // Only clip if the range is meaningfully narrower than global const minWeek = localMin > globalMin ? Math.max(0, localMin - 1) : 0; const whoAges = Object.keys(WHO_WEIGHT_PERCENTILES) .map(Number) - .filter((a) => a >= minWeek && a <= maxWeek + 2) + .filter((a) => a >= Math.floor(minWeek) && a <= Math.ceil(maxWeek) + 2) .sort((a, b) => a - b); - const mergedMap: Record> = {}; + // Use string keys so float actual-point ages don't collide with integer WHO ages + const rows = new Map>(); for (const ageWeeks of whoAges) { const row: Record = { ageWeeks }; PERCENTILE_LABELS.forEach((label, i) => { row[`p${label.slice(1)}`] = interpolatePercentile(ageWeeks, i); }); - mergedMap[ageWeeks] = row; + rows.set(ageWeeks, row); } for (const pt of actualPoints) { - mergedMap[pt.ageWeeks] = { ...(mergedMap[pt.ageWeeks] ?? { ageWeeks: pt.ageWeeks }), actualWeight: pt.weight }; + const existing = rows.get(pt.ageWeeks) ?? { ageWeeks: pt.ageWeeks }; + rows.set(pt.ageWeeks, { ...existing, actualWeight: pt.weight }); } - return Object.values(mergedMap).sort((a, b) => (a.ageWeeks as number) - (b.ageWeeks as number)); + return Array.from(rows.values()).sort((a, b) => (a.ageWeeks as number) - (b.ageWeeks as number)); } function buildSimpleChartData(logs: GrowthLog[], baby: BabyShape, field: "height" | "headCirc") { return logs .filter((l) => l[field]) - .map((l) => ({ ageWeeks: getAgeInWeeks(new Date(baby.birthDate), new Date(l.date)), value: l[field]! })) + .map((l) => ({ ageWeeks: ageWeeksFloat(baby.birthDate, l.date), value: l[field]! })) .sort((a, b) => a.ageWeeks - b.ageWeeks); } @@ -359,7 +361,7 @@ export default function GrowthPage() { - + } /> {WHO_CURVE_KEYS.map((key, i) => ( @@ -379,7 +381,7 @@ export default function GrowthPage() { - + [`${val} cm`, "Taille"]} labelFormatter={(v) => fmtAgeLabel(Number(v))} contentStyle={tooltipStyle} /> @@ -395,7 +397,7 @@ export default function GrowthPage() { - + [`${val} cm`, "PC"]} labelFormatter={(v) => fmtAgeLabel(Number(v))} contentStyle={tooltipStyle} />