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.
This commit is contained in:
307
maternal-web/components/analytics/GrowthPercentileChart.tsx
Normal file
307
maternal-web/components/analytics/GrowthPercentileChart.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user