feat: Add enhanced analytics dashboard with advanced visualizations
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

- 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>
This commit is contained in:
2025-10-06 13:17:20 +00:00
parent b0ac2f71df
commit 19e50a3b75
13 changed files with 1317 additions and 223 deletions

View File

@@ -1,10 +1,7 @@
'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 { Card, CardContent, CardHeader, Box, Chip, Tabs, Tab, LinearProgress } from '@mui/material';
import {
LineChart,
Line,
@@ -101,171 +98,209 @@ export function TrendAnalysisChart({ data, activityType, loading, error }: Trend
const currentTrend = getTrendData();
// Prepare chart data for predictions
const chartData = data.prediction.next7Days.map((point, index) => ({
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,
}));
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 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>
<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>
<TabsContent value={selectedTimeframe} className="space-y-4 mt-4">
<Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* 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)}%
<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>
</div>
<Progress
value={Math.abs(currentTrend.changePercent)}
className="h-2"
</Box>
<LinearProgress
variant="determinate"
value={Math.abs(currentTrend?.changePercent ?? 0)}
sx={{ height: 8, borderRadius: 4 }}
/>
</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>
</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 */}
<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>
<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 */}
<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>
<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>
<ResponsiveContainer width="100%" height={250}>
<AreaChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="day" />
<YAxis />
<Tooltip />
<Legend />
{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"
/>
{/* 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>
{/* 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.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">
{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>
<div className="flex flex-wrap gap-2">
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{data.prediction.factors.map((factor, index) => (
<Badge key={index} variant="secondary" className="text-xs">
{factor}
</Badge>
<Chip key={index} label={factor} size="small" sx={{ fontSize: '0.75rem' }} />
))}
</div>
</div>
</Box>
</Box>
)}
</div>
</Box>
{/* 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" />
<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
</h4>
<div className="space-y-2">
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{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">
<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 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 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>
</div>
</div>
</Box>
</Box>
))}
</div>
</div>
</Box>
</Box>
)}
</CardContent>
</Card>