Files
maternal-app/maternal-web/components/analytics/GrowthCurve.tsx
2025-10-01 19:01:52 +00:00

312 lines
11 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import {
Box,
Typography,
CircularProgress,
Alert,
ToggleButtonGroup,
ToggleButton,
FormControl,
InputLabel,
Select,
MenuItem,
} from '@mui/material';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
ReferenceLine,
} from 'recharts';
import { format } from 'date-fns';
import apiClient from '@/lib/api/client';
interface GrowthData {
age: number; // months
weight?: number; // kg
height?: number; // cm
headCircumference?: number; // cm
date: string;
}
// WHO Growth Standards Percentiles (simplified for 0-24 months)
// These are approximate values - in production, use exact WHO data
const WHO_WEIGHT_PERCENTILES = {
male: {
p3: [3.3, 4.4, 5.1, 5.6, 6.0, 6.4, 6.7, 7.0, 7.2, 7.5, 7.7, 7.9, 8.1, 8.3, 8.5, 8.7, 8.9, 9.0, 9.2, 9.4, 9.5, 9.7, 9.9, 10.0, 10.2],
p15: [3.6, 4.8, 5.5, 6.1, 6.5, 6.9, 7.2, 7.5, 7.8, 8.0, 8.3, 8.5, 8.7, 8.9, 9.1, 9.3, 9.5, 9.7, 9.9, 10.1, 10.3, 10.4, 10.6, 10.8, 11.0],
p50: [4.0, 5.3, 6.1, 6.7, 7.2, 7.6, 8.0, 8.3, 8.6, 8.9, 9.2, 9.4, 9.7, 9.9, 10.1, 10.4, 10.6, 10.8, 11.0, 11.2, 11.5, 11.7, 11.9, 12.1, 12.3],
p85: [4.4, 5.8, 6.7, 7.4, 7.9, 8.4, 8.8, 9.1, 9.5, 9.8, 10.1, 10.4, 10.6, 10.9, 11.2, 11.4, 11.7, 11.9, 12.2, 12.4, 12.7, 13.0, 13.2, 13.5, 13.7],
p97: [4.8, 6.3, 7.3, 8.0, 8.6, 9.1, 9.5, 9.9, 10.3, 10.6, 11.0, 11.3, 11.6, 11.9, 12.2, 12.5, 12.8, 13.1, 13.4, 13.7, 14.0, 14.3, 14.6, 14.9, 15.2],
},
female: {
p3: [3.2, 4.2, 4.8, 5.3, 5.7, 6.0, 6.3, 6.6, 6.8, 7.0, 7.2, 7.4, 7.6, 7.8, 8.0, 8.2, 8.3, 8.5, 8.7, 8.8, 9.0, 9.2, 9.3, 9.5, 9.7],
p15: [3.5, 4.6, 5.3, 5.8, 6.2, 6.5, 6.8, 7.1, 7.4, 7.6, 7.8, 8.1, 8.3, 8.5, 8.7, 8.9, 9.1, 9.3, 9.4, 9.6, 9.8, 10.0, 10.2, 10.4, 10.6],
p50: [3.9, 5.1, 5.8, 6.4, 6.9, 7.3, 7.6, 7.9, 8.2, 8.5, 8.7, 9.0, 9.2, 9.5, 9.7, 10.0, 10.2, 10.4, 10.7, 10.9, 11.1, 11.4, 11.6, 11.8, 12.1],
p85: [4.3, 5.6, 6.5, 7.1, 7.6, 8.0, 8.4, 8.7, 9.1, 9.4, 9.7, 10.0, 10.2, 10.5, 10.8, 11.0, 11.3, 11.6, 11.8, 12.1, 12.4, 12.6, 12.9, 13.2, 13.5],
p97: [4.7, 6.1, 7.0, 7.7, 8.3, 8.7, 9.1, 9.5, 9.9, 10.2, 10.6, 10.9, 11.2, 11.5, 11.8, 12.1, 12.4, 12.7, 13.0, 13.3, 13.6, 13.9, 14.2, 14.5, 14.9],
},
};
export default function GrowthCurve() {
const [data, setData] = useState<GrowthData[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [metric, setMetric] = useState<'weight' | 'height' | 'headCircumference'>('weight');
const [gender, setGender] = useState<'male' | 'female'>('male');
useEffect(() => {
fetchGrowthData();
}, []);
const fetchGrowthData = async () => {
try {
setIsLoading(true);
// Fetch growth measurements from backend
const response = await apiClient.get('/api/v1/activities/growth');
const growthActivities = response.data.data;
// Process growth data
const processedData: GrowthData[] = growthActivities.map((activity: any) => {
const ageInMonths = calculateAgeInMonths(activity.childBirthDate, activity.measurementDate);
return {
age: ageInMonths,
weight: activity.weight,
height: activity.height,
headCircumference: activity.headCircumference,
date: format(new Date(activity.measurementDate), 'MMM dd, yyyy'),
};
});
// Sort by age
processedData.sort((a, b) => a.age - b.age);
setData(processedData);
// Fetch child profile to determine gender
const profileResponse = await apiClient.get('/api/v1/children/profile');
if (profileResponse.data.data.gender) {
setGender(profileResponse.data.data.gender.toLowerCase());
}
} catch (err: any) {
console.error('Failed to fetch growth data:', err);
// If endpoint doesn't exist, create sample data for demonstration
if (err.response?.status === 404) {
setData(createSampleData());
} else {
setError(err.response?.data?.message || 'Failed to load growth data');
}
} finally {
setIsLoading(false);
}
};
const calculateAgeInMonths = (birthDate: string, measurementDate: string): number => {
const birth = new Date(birthDate);
const measurement = new Date(measurementDate);
const months = (measurement.getFullYear() - birth.getFullYear()) * 12 +
(measurement.getMonth() - birth.getMonth());
return Math.max(0, months);
};
const createSampleData = (): GrowthData[] => {
// Sample data for demonstration (0-12 months)
return [
{ age: 0, weight: 3.5, height: 50, headCircumference: 35, date: 'Birth' },
{ age: 1, weight: 4.5, height: 54, headCircumference: 37, date: '1 month' },
{ age: 2, weight: 5.5, height: 58, headCircumference: 39, date: '2 months' },
{ age: 3, weight: 6.3, height: 61, headCircumference: 40, date: '3 months' },
{ age: 4, weight: 7.0, height: 64, headCircumference: 41, date: '4 months' },
{ age: 6, weight: 7.8, height: 67, headCircumference: 43, date: '6 months' },
{ age: 9, weight: 8.9, height: 72, headCircumference: 45, date: '9 months' },
{ age: 12, weight: 9.8, height: 76, headCircumference: 46, date: '12 months' },
];
};
const getPercentileData = () => {
const percentiles = WHO_WEIGHT_PERCENTILES[gender];
const maxAge = Math.max(...data.map(d => d.age), 24);
return Array.from({ length: maxAge + 1 }, (_, i) => ({
age: i,
p3: percentiles.p3[i] || null,
p15: percentiles.p15[i] || null,
p50: percentiles.p50[i] || null,
p85: percentiles.p85[i] || null,
p97: percentiles.p97[i] || null,
}));
};
const handleMetricChange = (event: React.MouseEvent<HTMLElement>, newMetric: 'weight' | 'height' | 'headCircumference' | null) => {
if (newMetric !== null) {
setMetric(newMetric);
}
};
const handleGenderChange = (event: any) => {
setGender(event.target.value);
};
if (isLoading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<CircularProgress />
</Box>
);
}
if (error) {
return (
<Alert severity="error" sx={{ borderRadius: 2 }}>
{error}
</Alert>
);
}
const percentileData = getPercentileData();
const combinedData = percentileData.map(p => {
const userDataPoint = data.find(d => Math.round(d.age) === p.age);
return {
...p,
userValue: userDataPoint?.[metric] || null,
};
});
return (
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Box>
<Typography variant="h6" gutterBottom fontWeight="600">
Growth Curve (WHO Standards)
</Typography>
<Typography variant="body2" color="text.secondary">
Track your child's growth against WHO percentiles
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 2 }}>
<FormControl size="small" sx={{ minWidth: 100 }}>
<InputLabel>Gender</InputLabel>
<Select value={gender} label="Gender" onChange={handleGenderChange}>
<MenuItem value="male">Male</MenuItem>
<MenuItem value="female">Female</MenuItem>
</Select>
</FormControl>
<ToggleButtonGroup
value={metric}
exclusive
onChange={handleMetricChange}
size="small"
>
<ToggleButton value="weight">Weight</ToggleButton>
<ToggleButton value="height">Height</ToggleButton>
<ToggleButton value="headCircumference">Head</ToggleButton>
</ToggleButtonGroup>
</Box>
</Box>
{data.length === 0 ? (
<Alert severity="info" sx={{ borderRadius: 2 }}>
No growth measurements recorded yet. Start tracking to see growth curves!
</Alert>
) : (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={combinedData}>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis
dataKey="age"
stroke="#666"
label={{ value: 'Age (months)', position: 'insideBottom', offset: -5 }}
/>
<YAxis
stroke="#666"
label={{
value: metric === 'weight' ? 'Weight (kg)' : metric === 'height' ? 'Height (cm)' : 'Head Circumference (cm)',
angle: -90,
position: 'insideLeft',
}}
/>
<Tooltip
contentStyle={{
backgroundColor: 'rgba(255, 255, 255, 0.95)',
border: 'none',
borderRadius: 8,
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
}}
/>
<Legend />
{/* WHO Percentile Lines */}
<Line
type="monotone"
dataKey="p3"
stroke="#FFE4E1"
strokeWidth={1}
dot={false}
name="3rd Percentile"
strokeDasharray="5 5"
/>
<Line
type="monotone"
dataKey="p15"
stroke="#FFD4C9"
strokeWidth={1}
dot={false}
name="15th Percentile"
strokeDasharray="3 3"
/>
<Line
type="monotone"
dataKey="p50"
stroke="#FFB6C1"
strokeWidth={2}
dot={false}
name="50th Percentile (Median)"
/>
<Line
type="monotone"
dataKey="p85"
stroke="#FFD4C9"
strokeWidth={1}
dot={false}
name="85th Percentile"
strokeDasharray="3 3"
/>
<Line
type="monotone"
dataKey="p97"
stroke="#FFE4E1"
strokeWidth={1}
dot={false}
name="97th Percentile"
strokeDasharray="5 5"
/>
{/* User's Actual Data */}
<Line
type="monotone"
dataKey="userValue"
stroke="#FF1493"
strokeWidth={3}
dot={{ fill: '#FF1493', r: 6 }}
name="Your Child"
/>
</LineChart>
</ResponsiveContainer>
)}
<Box sx={{ mt: 2 }}>
<Typography variant="caption" color="text.secondary">
* WHO (World Health Organization) growth standards are based on healthy breastfed children
from diverse populations. Consult your pediatrician for personalized growth assessment.
</Typography>
</Box>
</Box>
);
}