- 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.
307 lines
11 KiB
TypeScript
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>
|
|
);
|
|
} |