Files
maternal-app/maternal-web/components/analytics/TrendAnalysisChart.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

273 lines
8.9 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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Progress } from '@/components/ui/progress';
import {
LineChart,
Line,
Area,
AreaChart,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
ReferenceLine,
} from 'recharts';
import {
TrendingUp,
TrendingDown,
Minus,
Calendar,
Target,
ChartLine,
Activity,
} from 'lucide-react';
import { TrendAnalysis } from '@/lib/api/analytics';
import { format } from 'date-fns';
interface TrendAnalysisChartProps {
data: TrendAnalysis | null;
activityType: string;
loading?: boolean;
error?: Error | null;
}
export function TrendAnalysisChart({ data, activityType, loading, error }: TrendAnalysisChartProps) {
const [selectedTimeframe, setSelectedTimeframe] = useState<'short' | 'medium' | 'long'>('short');
if (loading) {
return (
<Card>
<CardContent className="flex items-center justify-center h-64">
<div className="animate-pulse">Analyzing trends...</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className="border-red-200">
<CardContent className="flex items-center justify-center h-64 text-red-500">
<Activity className="h-5 w-5 mr-2" />
Error loading trend analysis
</CardContent>
</Card>
);
}
if (!data) {
return null;
}
const getTrendIcon = (direction: string) => {
switch (direction) {
case 'improving':
return <TrendingUp className="h-4 w-4 text-green-500" />;
case 'declining':
return <TrendingDown className="h-4 w-4 text-red-500" />;
default:
return <Minus className="h-4 w-4 text-gray-500" />;
}
};
const getTrendColor = (direction: string) => {
switch (direction) {
case 'improving':
return 'bg-green-100 text-green-800';
case 'declining':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const getTrendData = () => {
switch (selectedTimeframe) {
case 'short':
return data.shortTermTrend;
case 'medium':
return data.mediumTermTrend;
case 'long':
return data.longTermTrend;
}
};
const currentTrend = getTrendData();
// Prepare chart data for predictions
const chartData = data.prediction.next7Days.map((point, index) => ({
day: format(point.date, 'MMM dd'),
predicted: point.predictedValue,
upperBound: point.confidenceInterval.upper,
lowerBound: point.confidenceInterval.lower,
}));
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<ChartLine className="h-5 w-5" />
{activityType} Trend Analysis
</span>
<Badge className={getTrendColor(currentTrend.direction)}>
<span className="flex items-center gap-1">
{getTrendIcon(currentTrend.direction)}
{currentTrend.direction}
</span>
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Timeframe Tabs */}
<Tabs value={selectedTimeframe} onValueChange={(v) => setSelectedTimeframe(v as any)}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="short">Short (7 days)</TabsTrigger>
<TabsTrigger value="medium">Medium (14 days)</TabsTrigger>
<TabsTrigger value="long">Long (30 days)</TabsTrigger>
</TabsList>
<TabsContent value={selectedTimeframe} className="space-y-4 mt-4">
{/* Trend Metrics */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Change</span>
<span className="font-medium">
{currentTrend.changePercent > 0 ? '+' : ''}{currentTrend.changePercent.toFixed(1)}%
</span>
</div>
<Progress
value={Math.abs(currentTrend.changePercent)}
className="h-2"
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Confidence</span>
<span className="font-medium">{(currentTrend.confidence * 100).toFixed(0)}%</span>
</div>
<Progress value={currentTrend.confidence * 100} className="h-2" />
</div>
</div>
{/* Statistical Details */}
<div className="p-3 bg-gray-50 rounded-lg grid grid-cols-3 gap-3 text-sm">
<div>
<p className="text-gray-600">Slope</p>
<p className="font-medium">{currentTrend.slope.toFixed(3)}</p>
</div>
<div>
<p className="text-gray-600">R² Score</p>
<p className="font-medium">{currentTrend.r2Score.toFixed(3)}</p>
</div>
<div>
<p className="text-gray-600">Trend</p>
<p className="font-medium capitalize">{currentTrend.direction}</p>
</div>
</div>
</TabsContent>
</Tabs>
{/* Prediction Chart */}
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Target className="h-4 w-4" />
7-Day Forecast
<Badge variant="outline" className="ml-auto">
{(data.prediction.confidence * 100).toFixed(0)}% confidence
</Badge>
</h4>
<ResponsiveContainer width="100%" height={250}>
<AreaChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="day" />
<YAxis />
<Tooltip />
<Legend />
{/* Confidence interval area */}
<Area
type="monotone"
dataKey="upperBound"
stroke="none"
fill="#e0e7ff"
fillOpacity={0.3}
name="Upper bound"
/>
<Area
type="monotone"
dataKey="lowerBound"
stroke="none"
fill="#ffffff"
fillOpacity={1}
name="Lower bound"
/>
{/* Predicted trend line */}
<Line
type="monotone"
dataKey="predicted"
stroke="#6366f1"
strokeWidth={2}
dot={{ fill: '#6366f1', r: 4 }}
name="Predicted"
/>
</AreaChart>
</ResponsiveContainer>
{/* Prediction Factors */}
{data.prediction.factors.length > 0 && (
<div className="p-3 bg-blue-50 rounded-lg space-y-2">
<p className="text-xs font-medium text-blue-900 uppercase tracking-wider">
Factors Influencing Prediction
</p>
<div className="flex flex-wrap gap-2">
{data.prediction.factors.map((factor, index) => (
<Badge key={index} variant="secondary" className="text-xs">
{factor}
</Badge>
))}
</div>
</div>
)}
</div>
{/* Seasonal Patterns */}
{data.seasonalPatterns && data.seasonalPatterns.length > 0 && (
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Calendar className="h-4 w-4" />
Seasonal Patterns Detected
</h4>
<div className="space-y-2">
{data.seasonalPatterns.map((pattern, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-purple-50 rounded-lg">
<div>
<p className="text-sm font-medium text-purple-900 capitalize">
{pattern.type} Pattern
</p>
<p className="text-xs text-purple-700">{pattern.pattern}</p>
</div>
<div className="text-right">
<p className="text-xs text-purple-600">Strength</p>
<p className="text-sm font-medium text-purple-900">
{(pattern.strength * 100).toFixed(0)}%
</p>
</div>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
);
}