- 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.
273 lines
8.9 KiB
TypeScript
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>
|
|
);
|
|
} |