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 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 17:11:08 +02:00
parent e27f42942c
commit cd677d9244
+21 -19
View File
@@ -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<number, Record<string, number | null>> = {};
// Use string keys so float actual-point ages don't collide with integer WHO ages
const rows = new Map<number, Record<string, number | null>>();
for (const ageWeeks of whoAges) {
const row: Record<string, number | null> = { 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() {
<ResponsiveContainer width="100%" height={200}>
<LineChart data={weightData} margin={{ top: 0, right: 10, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke={gridStroke} />
<XAxis dataKey="ageWeeks" tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={fmtAgeWeeks} axisLine={false} tickLine={false} />
<XAxis dataKey="ageWeeks" type="number" domain={["dataMin", "dataMax"]} tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={fmtAgeWeeks} axisLine={false} tickLine={false} />
<YAxis tick={{ fontSize: 10, fill: "#94a3b8" }} axisLine={false} tickLine={false} />
<Tooltip content={(props) => <WeightTooltip {...props} style={tooltipStyle} />} />
{WHO_CURVE_KEYS.map((key, i) => (
@@ -379,7 +381,7 @@ export default function GrowthPage() {
<ResponsiveContainer width="100%" height={160}>
<LineChart data={heightData} margin={{ top: 0, right: 10, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke={gridStroke} />
<XAxis dataKey="ageWeeks" tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={fmtAgeWeeks} axisLine={false} tickLine={false} />
<XAxis dataKey="ageWeeks" type="number" domain={["dataMin", "dataMax"]} tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={fmtAgeWeeks} axisLine={false} tickLine={false} />
<YAxis tick={{ fontSize: 10, fill: "#94a3b8" }} axisLine={false} tickLine={false} domain={["auto", "auto"]} />
<Tooltip formatter={(val) => [`${val} cm`, "Taille"]} labelFormatter={(v) => fmtAgeLabel(Number(v))} contentStyle={tooltipStyle} />
<Line dataKey="value" stroke="#0ea5e9" strokeWidth={2.5} dot={{ r: 4, fill: "#0ea5e9", strokeWidth: 2, stroke: "#fff" }} />
@@ -395,7 +397,7 @@ export default function GrowthPage() {
<ResponsiveContainer width="100%" height={160}>
<LineChart data={headData} margin={{ top: 0, right: 10, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke={gridStroke} />
<XAxis dataKey="ageWeeks" tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={fmtAgeWeeks} axisLine={false} tickLine={false} />
<XAxis dataKey="ageWeeks" type="number" domain={["dataMin", "dataMax"]} tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={fmtAgeWeeks} axisLine={false} tickLine={false} />
<YAxis tick={{ fontSize: 10, fill: "#94a3b8" }} axisLine={false} tickLine={false} domain={["auto", "auto"]} />
<Tooltip formatter={(val) => [`${val} cm`, "PC"]} labelFormatter={(v) => fmtAgeLabel(Number(v))} contentStyle={tooltipStyle} />
<Line dataKey="value" stroke="#10b981" strokeWidth={2.5} dot={{ r: 4, fill: "#10b981", strokeWidth: 2, stroke: "#fff" }} />