- Created EnhancedInsightsDashboard with multiple chart types: - Area charts with gradients for activity trends - Radar chart for weekly activity patterns - 24-hour heatmap visualization - Bubble/scatter chart for correlations - Time of day distribution bar chart - Added toggle between basic and enhanced chart views - Implemented chart export functionality (PNG/PDF) - Fixed API endpoint URLs (circadian-rhythm, query params) - Fixed component library conflicts (shadcn/ui → MUI) - Added comprehensive null safety for timestamp handling - Added alert type translations in all 5 languages - Installed html2canvas and jspdf for export features - Applied consistent minimum width styling to all charts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
308 lines
11 KiB
TypeScript
308 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState } from 'react';
|
|
import { Card, CardContent, CardHeader, Box, Chip, Tabs, Tab, LinearProgress } from '@mui/material';
|
|
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 ?? 0,
|
|
lowerBound: point.confidenceInterval?.lower ?? 0,
|
|
})) ?? [];
|
|
|
|
const getChipColor = (direction: string) => {
|
|
switch (direction) {
|
|
case 'improving':
|
|
return 'success';
|
|
case 'declining':
|
|
return 'error';
|
|
default:
|
|
return 'default';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card sx={{ width: '100%' }}>
|
|
<CardHeader
|
|
title={
|
|
<Box display="flex" alignItems="center" justifyContent="space-between">
|
|
<Box display="flex" alignItems="center" gap={1}>
|
|
<ChartLine style={{ width: 20, height: 20 }} />
|
|
{activityType} Trend Analysis
|
|
</Box>
|
|
<Chip
|
|
label={
|
|
<Box display="flex" alignItems="center" gap={0.5}>
|
|
{getTrendIcon(currentTrend?.direction ?? 'stable')}
|
|
{currentTrend?.direction ?? 'No data'}
|
|
</Box>
|
|
}
|
|
color={getChipColor(currentTrend?.direction ?? 'stable')}
|
|
size="small"
|
|
/>
|
|
</Box>
|
|
}
|
|
/>
|
|
<CardContent>
|
|
<Box sx={{ mb: 3 }}>
|
|
{/* Timeframe Tabs */}
|
|
<Tabs
|
|
value={selectedTimeframe}
|
|
onChange={(e, newValue) => setSelectedTimeframe(newValue)}
|
|
variant="fullWidth"
|
|
>
|
|
<Tab value="short" label="Short (7 days)" />
|
|
<Tab value="medium" label="Medium (14 days)" />
|
|
<Tab value="long" label="Long (30 days)" />
|
|
</Tabs>
|
|
|
|
<Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
{/* Trend Metrics */}
|
|
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 2 }}>
|
|
<Box>
|
|
<Box display="flex" justifyContent="space-between" mb={1}>
|
|
<span style={{ fontSize: '0.875rem', color: '#666' }}>Change</span>
|
|
<span style={{ fontWeight: 500 }}>
|
|
{currentTrend?.changePercent != null ? (
|
|
`${currentTrend.changePercent > 0 ? '+' : ''}${currentTrend.changePercent.toFixed(1)}%`
|
|
) : 'N/A'}
|
|
</span>
|
|
</Box>
|
|
<LinearProgress
|
|
variant="determinate"
|
|
value={Math.abs(currentTrend?.changePercent ?? 0)}
|
|
sx={{ height: 8, borderRadius: 4 }}
|
|
/>
|
|
</Box>
|
|
<Box>
|
|
<Box display="flex" justifyContent="space-between" mb={1}>
|
|
<span style={{ fontSize: '0.875rem', color: '#666' }}>Confidence</span>
|
|
<span style={{ fontWeight: 500 }}>
|
|
{currentTrend?.confidence != null ? `${(currentTrend.confidence * 100).toFixed(0)}%` : 'N/A'}
|
|
</span>
|
|
</Box>
|
|
<LinearProgress
|
|
variant="determinate"
|
|
value={(currentTrend?.confidence ?? 0) * 100}
|
|
sx={{ height: 8, borderRadius: 4 }}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Statistical Details */}
|
|
<Box sx={{ p: 2, bgcolor: '#f9fafb', borderRadius: 1, display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 2, fontSize: '0.875rem' }}>
|
|
<Box>
|
|
<p style={{ color: '#666' }}>Slope</p>
|
|
<p style={{ fontWeight: 500 }}>{currentTrend?.slope != null ? currentTrend.slope.toFixed(3) : 'N/A'}</p>
|
|
</Box>
|
|
<Box>
|
|
<p style={{ color: '#666' }}>R² Score</p>
|
|
<p style={{ fontWeight: 500 }}>{currentTrend?.r2Score != null ? currentTrend.r2Score.toFixed(3) : 'N/A'}</p>
|
|
</Box>
|
|
<Box>
|
|
<p style={{ color: '#666' }}>Trend</p>
|
|
<p style={{ fontWeight: 500, textTransform: 'capitalize' }}>{currentTrend?.direction ?? 'N/A'}</p>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Prediction Chart */}
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
<Box display="flex" alignItems="center" gap={1} justifyContent="space-between">
|
|
<Box display="flex" alignItems="center" gap={1} sx={{ fontSize: '0.875rem', fontWeight: 500, color: '#374151' }}>
|
|
<Target style={{ width: 16, height: 16 }} />
|
|
7-Day Forecast
|
|
</Box>
|
|
<Chip
|
|
label={`${data.prediction?.confidence != null ? (data.prediction.confidence * 100).toFixed(0) : '0'}% confidence`}
|
|
size="small"
|
|
variant="outlined"
|
|
/>
|
|
</Box>
|
|
|
|
{chartData.length > 0 ? (
|
|
<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>
|
|
) : (
|
|
<Box sx={{ p: 3, textAlign: 'center', color: '#666' }}>
|
|
No prediction data available
|
|
</Box>
|
|
)}
|
|
|
|
{/* Prediction Factors */}
|
|
{data.prediction?.factors && data.prediction.factors.length > 0 && (
|
|
<Box sx={{ p: 2, bgcolor: '#eff6ff', borderRadius: 1, display: 'flex', flexDirection: 'column', gap: 1 }}>
|
|
<p style={{ fontSize: '0.75rem', fontWeight: 500, color: '#1e3a8a', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
|
Factors Influencing Prediction
|
|
</p>
|
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
|
{data.prediction.factors.map((factor, index) => (
|
|
<Chip key={index} label={factor} size="small" sx={{ fontSize: '0.75rem' }} />
|
|
))}
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
|
|
{/* Seasonal Patterns */}
|
|
{data.seasonalPatterns && data.seasonalPatterns.length > 0 && (
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
<Box display="flex" alignItems="center" gap={1} sx={{ fontSize: '0.875rem', fontWeight: 500, color: '#374151' }}>
|
|
<Calendar style={{ width: 16, height: 16 }} />
|
|
Seasonal Patterns Detected
|
|
</Box>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
|
{data.seasonalPatterns.map((pattern, index) => (
|
|
<Box key={index} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', p: 2, bgcolor: '#faf5ff', borderRadius: 1 }}>
|
|
<Box>
|
|
<p style={{ fontSize: '0.875rem', fontWeight: 500, color: '#581c87', textTransform: 'capitalize' }}>
|
|
{pattern.type} Pattern
|
|
</p>
|
|
<p style={{ fontSize: '0.75rem', color: '#7c3aed' }}>{pattern.pattern}</p>
|
|
</Box>
|
|
<Box sx={{ textAlign: 'right' }}>
|
|
<p style={{ fontSize: '0.75rem', color: '#9333ea' }}>Strength</p>
|
|
<p style={{ fontSize: '0.875rem', fontWeight: 500, color: '#581c87' }}>
|
|
{pattern.strength != null ? (pattern.strength * 100).toFixed(0) : '0'}%
|
|
</p>
|
|
</Box>
|
|
</Box>
|
|
))}
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
} |