Files
maternal-app/maternal-web/components/analytics/GrowthPercentileChart.tsx
Andrei b0ac2f71df
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
feat: Add advanced analytics UI components in frontend
- Add comprehensive API client methods for all advanced analytics endpoints
- Create CircadianRhythmCard component for sleep pattern visualization
- Create AnomalyAlertsPanel for anomaly detection and alerts
- Create GrowthPercentileChart with WHO/CDC percentiles
- Create CorrelationInsights for activity correlations
- Create TrendAnalysisChart with predictions
- Add advanced analytics page with all new components
- Add UI component library (shadcn/ui) setup
- Add navigation link to advanced analytics from insights page

All advanced analytics features are now accessible from the frontend UI.
2025-10-06 11:46:05 +00:00

307 lines
11 KiB
TypeScript

'use client';
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Alert, AlertDescription } from '@/components/ui/alert';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
ReferenceLine,
Area,
AreaChart,
} from 'recharts';
import {
Ruler,
Weight,
TrendingUp,
Baby,
AlertCircle,
ChevronUp,
ChevronDown,
Minus,
} from 'lucide-react';
import { GrowthAnalysis, GrowthPercentile } from '@/lib/api/analytics';
import { format } from 'date-fns';
interface GrowthPercentileChartProps {
data: GrowthAnalysis | null;
loading?: boolean;
error?: Error | null;
}
export function GrowthPercentileChart({ data, loading, error }: GrowthPercentileChartProps) {
const [selectedMetric, setSelectedMetric] = useState<'weight' | 'height' | 'headCircumference'>('weight');
if (loading) {
return (
<Card>
<CardContent className="flex items-center justify-center h-64">
<div className="animate-pulse">Loading growth analysis...</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className="border-red-200">
<CardContent className="flex items-center justify-center h-64 text-red-500">
<AlertCircle className="h-5 w-5 mr-2" />
Error loading growth data
</CardContent>
</Card>
);
}
if (!data) {
return null;
}
const getPercentileColor = (percentile: number) => {
if (percentile < 5 || percentile > 95) return 'text-red-600';
if (percentile < 15 || percentile > 85) return 'text-yellow-600';
return 'text-green-600';
};
const getPercentileBadge = (percentile: number) => {
if (percentile < 5 || percentile > 95) return 'bg-red-100 text-red-800';
if (percentile < 15 || percentile > 85) return 'bg-yellow-100 text-yellow-800';
return 'bg-green-100 text-green-800';
};
const getVelocityIcon = (value: number) => {
if (value > 0) return <ChevronUp className="h-4 w-4 text-green-500" />;
if (value < 0) return <ChevronDown className="h-4 w-4 text-red-500" />;
return <Minus className="h-4 w-4 text-gray-500" />;
};
const formatMeasurement = (value: number | undefined, metric: string) => {
if (!value) return 'N/A';
switch (metric) {
case 'weight':
return `${value.toFixed(1)} kg`;
case 'height':
return `${value.toFixed(1)} cm`;
case 'headCircumference':
return `${value.toFixed(1)} cm`;
default:
return value.toFixed(1);
}
};
// Prepare chart data
const chartData = data.growthCurve.measurements.map(m => ({
date: format(m.date, 'MMM dd'),
weight: m.weight,
height: m.height,
headCircumference: m.headCircumference,
p3: data.growthCurve.percentileCurves[selectedMetric]?.p3?.[0],
p15: data.growthCurve.percentileCurves[selectedMetric]?.p15?.[0],
p50: data.growthCurve.percentileCurves[selectedMetric]?.p50?.[0],
p85: data.growthCurve.percentileCurves[selectedMetric]?.p85?.[0],
p97: data.growthCurve.percentileCurves[selectedMetric]?.p97?.[0],
}));
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Baby className="h-5 w-5" />
Growth Analysis
</span>
{data.alerts.length > 0 && (
<Badge variant="destructive">
{data.alerts.length} Alert{data.alerts.length > 1 ? 's' : ''}
</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Current Percentiles */}
{data.currentPercentiles && (
<div className="grid grid-cols-3 gap-4">
{data.currentPercentiles.weight && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Weight className="h-4 w-4 text-gray-500" />
<span className="text-sm text-gray-600">Weight</span>
</div>
<p className="text-lg font-semibold">
{data.currentPercentiles.weight.value.toFixed(1)} kg
</p>
<Badge className={getPercentileBadge(data.currentPercentiles.weight.percentile)}>
{Math.round(data.currentPercentiles.weight.percentile)}th percentile
</Badge>
<p className="text-xs text-gray-500">
{data.currentPercentiles.weight.interpretation}
</p>
</div>
)}
{data.currentPercentiles.height && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Ruler className="h-4 w-4 text-gray-500" />
<span className="text-sm text-gray-600">Height</span>
</div>
<p className="text-lg font-semibold">
{data.currentPercentiles.height.value.toFixed(1)} cm
</p>
<Badge className={getPercentileBadge(data.currentPercentiles.height.percentile)}>
{Math.round(data.currentPercentiles.height.percentile)}th percentile
</Badge>
<p className="text-xs text-gray-500">
{data.currentPercentiles.height.interpretation}
</p>
</div>
)}
{data.currentPercentiles.headCircumference && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Baby className="h-4 w-4 text-gray-500" />
<span className="text-sm text-gray-600">Head Circ.</span>
</div>
<p className="text-lg font-semibold">
{data.currentPercentiles.headCircumference.value.toFixed(1)} cm
</p>
<Badge className={getPercentileBadge(data.currentPercentiles.headCircumference.percentile)}>
{Math.round(data.currentPercentiles.headCircumference.percentile)}th percentile
</Badge>
<p className="text-xs text-gray-500">
{data.currentPercentiles.headCircumference.interpretation}
</p>
</div>
)}
</div>
)}
{/* Growth Velocity */}
{data.growthVelocity && (
<div className="p-4 bg-blue-50 rounded-lg space-y-3">
<h4 className="text-sm font-medium text-blue-900">Growth Velocity</h4>
<div className="grid grid-cols-2 gap-4">
{data.growthVelocity.weightVelocity && (
<div className="flex items-center justify-between">
<span className="text-sm text-blue-700">Weight Gain</span>
<div className="flex items-center gap-1">
{getVelocityIcon(data.growthVelocity.weightVelocity.value)}
<span className="font-medium text-blue-900">
{data.growthVelocity.weightVelocity.value.toFixed(2)} {data.growthVelocity.weightVelocity.unit}
</span>
</div>
</div>
)}
{data.growthVelocity.heightVelocity && (
<div className="flex items-center justify-between">
<span className="text-sm text-blue-700">Height Growth</span>
<div className="flex items-center gap-1">
{getVelocityIcon(data.growthVelocity.heightVelocity.value)}
<span className="font-medium text-blue-900">
{data.growthVelocity.heightVelocity.value.toFixed(2)} {data.growthVelocity.heightVelocity.unit}
</span>
</div>
</div>
)}
</div>
<p className="text-xs text-blue-600">{data.growthVelocity.interpretation}</p>
</div>
)}
{/* Growth Chart */}
<Tabs value={selectedMetric} onValueChange={(v) => setSelectedMetric(v as any)}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="weight">Weight</TabsTrigger>
<TabsTrigger value="height">Height</TabsTrigger>
<TabsTrigger value="headCircumference">Head Circ.</TabsTrigger>
</TabsList>
<TabsContent value={selectedMetric} className="mt-4">
<ResponsiveContainer width="100%" height={300}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
{/* Percentile reference lines */}
<ReferenceLine
y={chartData[0]?.p50}
stroke="#22c55e"
strokeDasharray="3 3"
label="50th %ile"
/>
<ReferenceLine
y={chartData[0]?.p15}
stroke="#eab308"
strokeDasharray="3 3"
label="15th %ile"
/>
<ReferenceLine
y={chartData[0]?.p85}
stroke="#eab308"
strokeDasharray="3 3"
label="85th %ile"
/>
{/* Child's measurements */}
<Line
type="monotone"
dataKey={selectedMetric}
stroke="#3b82f6"
strokeWidth={3}
dot={{ fill: '#3b82f6', r: 6 }}
name={`Child's ${selectedMetric}`}
/>
</LineChart>
</ResponsiveContainer>
</TabsContent>
</Tabs>
{/* Alerts */}
{data.alerts.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-700">Growth Alerts</h4>
{data.alerts.map((alert, index) => (
<Alert key={index} className={
alert.severity === 'high' ? 'border-red-300 bg-red-50' :
alert.severity === 'medium' ? 'border-yellow-300 bg-yellow-50' :
'border-blue-300 bg-blue-50'
}>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<p className="font-medium">{alert.message}</p>
<p className="text-sm mt-1">{alert.action}</p>
</AlertDescription>
</Alert>
))}
</div>
)}
{/* Recommendations */}
{data.recommendations.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-700">Recommendations</h4>
<ul className="space-y-1">
{data.recommendations.map((rec, index) => (
<li key={index} className="flex items-start gap-2 text-sm text-gray-600">
<TrendingUp className="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>{rec}</span>
</li>
))}
</ul>
</div>
)}
</CardContent>
</Card>
);
}