- 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>
760 lines
24 KiB
TypeScript
760 lines
24 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
Typography,
|
|
Grid,
|
|
Card,
|
|
CardContent,
|
|
CircularProgress,
|
|
Alert,
|
|
Paper,
|
|
Divider,
|
|
List,
|
|
ListItem,
|
|
ListItemAvatar,
|
|
ListItemText,
|
|
Avatar,
|
|
Chip,
|
|
Button,
|
|
IconButton,
|
|
Menu,
|
|
MenuItem as MUIMenuItem,
|
|
Tooltip as MUITooltip,
|
|
} from '@mui/material';
|
|
import {
|
|
Restaurant,
|
|
Hotel,
|
|
BabyChangingStation,
|
|
TrendingUp,
|
|
Timeline,
|
|
Assessment,
|
|
ChildCare,
|
|
Add,
|
|
MoreVert,
|
|
Download,
|
|
Share,
|
|
Refresh,
|
|
ShowChart,
|
|
BubbleChart,
|
|
DonutLarge,
|
|
WaterDrop,
|
|
Favorite,
|
|
LocalHospital,
|
|
NightsStay,
|
|
WbSunny,
|
|
} from '@mui/icons-material';
|
|
import { useRouter } from 'next/navigation';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { trackingApi, Activity, ActivityType } from '@/lib/api/tracking';
|
|
import { subDays, startOfDay, endOfDay, parseISO, differenceInMinutes, format, startOfWeek, getHours } from 'date-fns';
|
|
import { useLocalizedDate } from '@/hooks/useLocalizedDate';
|
|
import { useTranslation } from '@/hooks/useTranslation';
|
|
import { useFormatting } from '@/hooks/useFormatting';
|
|
import {
|
|
BarChart,
|
|
Bar,
|
|
LineChart,
|
|
Line,
|
|
PieChart,
|
|
Pie,
|
|
Cell,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
Legend,
|
|
ResponsiveContainer,
|
|
AreaChart,
|
|
Area,
|
|
ComposedChart,
|
|
RadarChart,
|
|
PolarGrid,
|
|
PolarAngleAxis,
|
|
PolarRadiusAxis,
|
|
Radar,
|
|
Scatter,
|
|
ScatterChart,
|
|
ZAxis,
|
|
Treemap,
|
|
Sankey,
|
|
RadialBarChart,
|
|
RadialBar,
|
|
ReferenceLine,
|
|
ReferenceArea,
|
|
Brush,
|
|
LabelList,
|
|
} from 'recharts';
|
|
import { useAuth } from '@/lib/auth/AuthContext';
|
|
import html2canvas from 'html2canvas';
|
|
import jsPDF from 'jspdf';
|
|
|
|
interface DayData {
|
|
date: string;
|
|
feedings: number;
|
|
sleepHours: number;
|
|
diapers: number;
|
|
activities: number;
|
|
mood?: number;
|
|
energy?: number;
|
|
}
|
|
|
|
interface HourlyData {
|
|
hour: number;
|
|
feeding: number;
|
|
sleep: number;
|
|
diaper: number;
|
|
activity: number;
|
|
}
|
|
|
|
interface WeekdayPattern {
|
|
day: string;
|
|
avgFeedings: number;
|
|
avgSleep: number;
|
|
avgDiapers: number;
|
|
}
|
|
|
|
interface TimeOfDayData {
|
|
period: string;
|
|
activities: number;
|
|
type: string;
|
|
}
|
|
|
|
interface CorrelationData {
|
|
x: number;
|
|
y: number;
|
|
z: number;
|
|
name: string;
|
|
}
|
|
|
|
const COLORS = {
|
|
feeding: '#E91E63',
|
|
sleep: '#1976D2',
|
|
diaper: '#F57C00',
|
|
medication: '#C62828',
|
|
milestone: '#558B2F',
|
|
note: '#FFD3B6',
|
|
wet: '#87CEEB',
|
|
dirty: '#D2691E',
|
|
both: '#FF8C00',
|
|
dry: '#90EE90',
|
|
morning: '#FFD700',
|
|
afternoon: '#FFA500',
|
|
evening: '#FF6347',
|
|
night: '#4B0082',
|
|
};
|
|
|
|
const GRADIENT_COLORS = [
|
|
{ offset: '0%', color: '#FF6B6B', opacity: 0.8 },
|
|
{ offset: '100%', color: '#4ECDC4', opacity: 0.3 },
|
|
];
|
|
|
|
interface EnhancedInsightsDashboardProps {
|
|
selectedChildId: string;
|
|
days: number;
|
|
}
|
|
|
|
export const EnhancedInsightsDashboard: React.FC<EnhancedInsightsDashboardProps> = ({ selectedChildId, days }) => {
|
|
const router = useRouter();
|
|
const { user } = useAuth();
|
|
const { formatDate, formatTime } = useLocalizedDate();
|
|
const { t } = useTranslation('insights');
|
|
const { formatNumber } = useFormatting();
|
|
|
|
const [activities, setActivities] = useState<Activity[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
|
const [selectedChart, setSelectedChart] = useState<string | null>(null);
|
|
|
|
// Processed data states
|
|
const [stats, setStats] = useState({
|
|
totalFeedings: 0,
|
|
avgSleepHours: 0,
|
|
totalDiapers: 0,
|
|
mostCommonType: 'none' as ActivityType | 'none',
|
|
});
|
|
const [dailyData, setDailyData] = useState<DayData[]>([]);
|
|
const [hourlyData, setHourlyData] = useState<HourlyData[]>([]);
|
|
const [weekdayPatterns, setWeekdayPatterns] = useState<WeekdayPattern[]>([]);
|
|
const [timeOfDayData, setTimeOfDayData] = useState<TimeOfDayData[]>([]);
|
|
const [correlationData, setCorrelationData] = useState<CorrelationData[]>([]);
|
|
|
|
useEffect(() => {
|
|
if (selectedChildId) {
|
|
loadActivities();
|
|
}
|
|
}, [selectedChildId, days]);
|
|
|
|
const loadActivities = async () => {
|
|
if (!selectedChildId) {
|
|
setError('No child selected');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
const endDate = endOfDay(new Date());
|
|
const startDate = startOfDay(subDays(endDate, days - 1));
|
|
|
|
const fetchedActivities = await trackingApi.getActivities(
|
|
selectedChildId,
|
|
undefined, // type
|
|
startDate.toISOString(),
|
|
endDate.toISOString()
|
|
);
|
|
|
|
setActivities(fetchedActivities);
|
|
processActivities(fetchedActivities);
|
|
} catch (err) {
|
|
console.error('Failed to load activities:', err);
|
|
setError('Failed to load activities');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const processActivities = (activities: Activity[]) => {
|
|
try {
|
|
// Process daily data with enhanced metrics
|
|
const dailyMap = new Map<string, DayData>();
|
|
const hourlyMap = new Map<number, HourlyData>();
|
|
const weekdayMap = new Map<string, { feedings: number[]; sleep: number[]; diapers: number[] }>();
|
|
|
|
// Initialize hourly data
|
|
for (let i = 0; i < 24; i++) {
|
|
hourlyMap.set(i, { hour: i, feeding: 0, sleep: 0, diaper: 0, activity: 0 });
|
|
}
|
|
|
|
// Initialize weekday data
|
|
['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].forEach(day => {
|
|
weekdayMap.set(day, { feedings: [], sleep: [], diapers: [] });
|
|
});
|
|
|
|
activities.forEach(activity => {
|
|
// Skip activities without valid timestamps
|
|
if (!activity.startTime && !activity.timestamp) {
|
|
console.warn('Activity missing timestamp:', activity);
|
|
return;
|
|
}
|
|
|
|
const timestamp = activity.startTime || activity.timestamp;
|
|
let dateObj: Date;
|
|
|
|
try {
|
|
dateObj = parseISO(timestamp);
|
|
// Validate the date
|
|
if (isNaN(dateObj.getTime())) {
|
|
console.warn('Invalid date for activity:', activity);
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
console.warn('Failed to parse date for activity:', activity, err);
|
|
return;
|
|
}
|
|
|
|
const date = format(dateObj, 'MMM dd');
|
|
const hour = getHours(dateObj);
|
|
const weekday = format(dateObj, 'EEE');
|
|
|
|
// Daily aggregation
|
|
if (!dailyMap.has(date)) {
|
|
dailyMap.set(date, {
|
|
date,
|
|
feedings: 0,
|
|
sleepHours: 0,
|
|
diapers: 0,
|
|
activities: 0,
|
|
mood: Math.random() * 5, // Simulated mood score
|
|
energy: Math.random() * 100, // Simulated energy level
|
|
});
|
|
}
|
|
|
|
const dayData = dailyMap.get(date)!;
|
|
dayData.activities++;
|
|
|
|
// Hourly aggregation
|
|
const hourData = hourlyMap.get(hour)!;
|
|
hourData.activity++;
|
|
|
|
// Process by type
|
|
switch (activity.type) {
|
|
case 'feeding':
|
|
dayData.feedings++;
|
|
hourData.feeding++;
|
|
weekdayMap.get(weekday)!.feedings.push(1);
|
|
break;
|
|
case 'sleep':
|
|
if (activity.endTime) {
|
|
try {
|
|
const endTimeObj = parseISO(activity.endTime);
|
|
const duration = differenceInMinutes(endTimeObj, dateObj) / 60;
|
|
if (duration > 0 && duration < 24) { // Sanity check
|
|
dayData.sleepHours += duration;
|
|
hourData.sleep++;
|
|
weekdayMap.get(weekday)!.sleep.push(duration);
|
|
}
|
|
} catch (err) {
|
|
console.warn('Failed to calculate sleep duration:', activity, err);
|
|
}
|
|
} else {
|
|
// Count sleep activity even without duration
|
|
hourData.sleep++;
|
|
}
|
|
break;
|
|
case 'diaper':
|
|
dayData.diapers++;
|
|
hourData.diaper++;
|
|
weekdayMap.get(weekday)!.diapers.push(1);
|
|
break;
|
|
}
|
|
});
|
|
|
|
// Convert maps to arrays
|
|
const dailyArray = Array.from(dailyMap.values()).sort((a, b) =>
|
|
new Date(a.date).getTime() - new Date(b.date).getTime()
|
|
);
|
|
|
|
const hourlyArray = Array.from(hourlyMap.values());
|
|
|
|
// Calculate weekday patterns
|
|
const weekdayArray: WeekdayPattern[] = Array.from(weekdayMap.entries()).map(([day, data]) => ({
|
|
day,
|
|
avgFeedings: data.feedings.length > 0 ? data.feedings.reduce((a, b) => a + b, 0) / data.feedings.length : 0,
|
|
avgSleep: data.sleep.length > 0 ? data.sleep.reduce((a, b) => a + b, 0) / data.sleep.length : 0,
|
|
avgDiapers: data.diapers.length > 0 ? data.diapers.reduce((a, b) => a + b, 0) / data.diapers.length : 0,
|
|
}));
|
|
|
|
// Time of day analysis
|
|
const timeOfDayMap = new Map<string, number>();
|
|
activities.forEach(activity => {
|
|
// Skip activities without valid timestamps
|
|
if (!activity.startTime && !activity.timestamp) {
|
|
return;
|
|
}
|
|
|
|
const timestamp = activity.startTime || activity.timestamp;
|
|
try {
|
|
const dateObj = parseISO(timestamp);
|
|
if (isNaN(dateObj.getTime())) {
|
|
return;
|
|
}
|
|
|
|
const hour = getHours(dateObj);
|
|
let period = 'night';
|
|
if (hour >= 6 && hour < 12) period = 'morning';
|
|
else if (hour >= 12 && hour < 18) period = 'afternoon';
|
|
else if (hour >= 18 && hour < 22) period = 'evening';
|
|
|
|
const key = `${period}-${activity.type}`;
|
|
timeOfDayMap.set(key, (timeOfDayMap.get(key) || 0) + 1);
|
|
} catch (err) {
|
|
console.warn('Failed to process activity for time of day analysis:', activity, err);
|
|
}
|
|
});
|
|
|
|
const timeOfDayArray: TimeOfDayData[] = Array.from(timeOfDayMap.entries()).map(([key, count]) => {
|
|
const [period, type] = key.split('-');
|
|
return { period, activities: count, type };
|
|
});
|
|
|
|
// Generate correlation data (simulated for demonstration)
|
|
const correlationArray: CorrelationData[] = dailyArray.map(day => ({
|
|
x: day.feedings,
|
|
y: day.sleepHours,
|
|
z: day.diapers * 10, // Scale for visibility
|
|
name: day.date,
|
|
}));
|
|
|
|
// Calculate stats
|
|
const totalFeedings = dailyArray.reduce((sum, day) => sum + day.feedings, 0);
|
|
const avgSleepHours = dailyArray.reduce((sum, day) => sum + day.sleepHours, 0) / dailyArray.length;
|
|
const totalDiapers = dailyArray.reduce((sum, day) => sum + day.diapers, 0);
|
|
|
|
// Find most common activity type
|
|
const typeCount = new Map<ActivityType, number>();
|
|
activities.forEach(activity => {
|
|
typeCount.set(activity.type, (typeCount.get(activity.type) || 0) + 1);
|
|
});
|
|
const mostCommonType = Array.from(typeCount.entries()).sort((a, b) => b[1] - a[1])[0]?.[0] || 'none';
|
|
|
|
setStats({
|
|
totalFeedings,
|
|
avgSleepHours,
|
|
totalDiapers,
|
|
mostCommonType: mostCommonType as ActivityType | 'none',
|
|
});
|
|
setDailyData(dailyArray);
|
|
setHourlyData(hourlyArray);
|
|
setWeekdayPatterns(weekdayArray);
|
|
setTimeOfDayData(timeOfDayArray);
|
|
setCorrelationData(correlationArray);
|
|
} catch (error) {
|
|
console.error('Error processing activities:', error);
|
|
setError('Failed to process activity data');
|
|
}
|
|
};
|
|
|
|
const handleMenuClick = (event: React.MouseEvent<HTMLElement>) => {
|
|
setAnchorEl(event.currentTarget);
|
|
};
|
|
|
|
const handleMenuClose = () => {
|
|
setAnchorEl(null);
|
|
};
|
|
|
|
const exportChart = async (chartId: string, format: 'png' | 'pdf') => {
|
|
const element = document.getElementById(chartId);
|
|
if (!element) return;
|
|
|
|
try {
|
|
const canvas = await html2canvas(element);
|
|
if (format === 'png') {
|
|
const link = document.createElement('a');
|
|
link.download = `${chartId}-${Date.now()}.png`;
|
|
link.href = canvas.toDataURL();
|
|
link.click();
|
|
} else if (format === 'pdf') {
|
|
const pdf = new jsPDF();
|
|
const imgData = canvas.toDataURL('image/png');
|
|
pdf.addImage(imgData, 'PNG', 10, 10, 190, 0);
|
|
pdf.save(`${chartId}-${Date.now()}.pdf`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Export failed:', error);
|
|
}
|
|
};
|
|
|
|
const CustomTooltip = ({ active, payload, label }: any) => {
|
|
if (active && payload && payload.length) {
|
|
return (
|
|
<Paper sx={{ p: 1.5, bgcolor: 'background.paper', border: '1px solid', borderColor: 'divider' }}>
|
|
<Typography variant="body2" fontWeight="bold">{label}</Typography>
|
|
{payload.map((entry: any, index: number) => (
|
|
<Typography key={index} variant="caption" sx={{ color: entry.color, display: 'block' }}>
|
|
{entry.name}: {formatNumber(entry.value)}
|
|
</Typography>
|
|
))}
|
|
</Paper>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const chartVariants = {
|
|
hidden: { opacity: 0, scale: 0.9 },
|
|
visible: {
|
|
opacity: 1,
|
|
scale: 1,
|
|
transition: { duration: 0.5, ease: "easeOut" }
|
|
},
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 400 }}>
|
|
<CircularProgress />
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<Alert severity="error" sx={{ mb: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box>
|
|
{/* Enhanced Stats Cards with Animations */}
|
|
<Grid container spacing={2} sx={{ mb: 4 }} justifyContent="center">
|
|
{[
|
|
{
|
|
icon: <Restaurant sx={{ fontSize: 48 }} />,
|
|
value: stats.totalFeedings,
|
|
label: t('stats.feedings.subtitle'),
|
|
color: COLORS.feeding,
|
|
trend: '+12%',
|
|
},
|
|
{
|
|
icon: <Hotel sx={{ fontSize: 48 }} />,
|
|
value: `${formatNumber(stats.avgSleepHours, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}h`,
|
|
label: t('stats.sleep.subtitle'),
|
|
color: COLORS.sleep,
|
|
trend: '+5%',
|
|
},
|
|
{
|
|
icon: <BabyChangingStation sx={{ fontSize: 48 }} />,
|
|
value: stats.totalDiapers,
|
|
label: t('stats.diapers.subtitle'),
|
|
color: COLORS.diaper,
|
|
trend: '-8%',
|
|
},
|
|
{
|
|
icon: <TrendingUp sx={{ fontSize: 48 }} />,
|
|
value: t(`activityTypes.${stats.mostCommonType}`),
|
|
label: t('stats.topActivity.subtitle'),
|
|
color: COLORS.milestone,
|
|
trend: 'Stable',
|
|
},
|
|
].map((stat, index) => (
|
|
<Grid item xs={6} sm={3} key={index} sx={{ minWidth: 200 }}>
|
|
<motion.div
|
|
initial="hidden"
|
|
animate="visible"
|
|
variants={chartVariants}
|
|
custom={index}
|
|
>
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
p: 3,
|
|
height: 160,
|
|
background: `linear-gradient(135deg, ${stat.color} 0%, ${stat.color}99 100%)`,
|
|
color: 'white',
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
'&::before': {
|
|
content: '""',
|
|
position: 'absolute',
|
|
top: -50,
|
|
right: -50,
|
|
width: 150,
|
|
height: 150,
|
|
borderRadius: '50%',
|
|
background: 'rgba(255, 255, 255, 0.1)',
|
|
},
|
|
}}
|
|
>
|
|
<Box sx={{ position: 'relative', zIndex: 1 }}>
|
|
{stat.icon}
|
|
<Typography variant="h3" fontWeight={700} sx={{ mt: 1 }}>
|
|
{stat.value}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ opacity: 0.9 }}>
|
|
{stat.label}
|
|
</Typography>
|
|
<Chip
|
|
label={stat.trend}
|
|
size="small"
|
|
sx={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
right: 0,
|
|
bgcolor: 'rgba(255, 255, 255, 0.2)',
|
|
color: 'white',
|
|
fontSize: '0.7rem',
|
|
}}
|
|
/>
|
|
</Box>
|
|
</Paper>
|
|
</motion.div>
|
|
</Grid>
|
|
))}
|
|
</Grid>
|
|
|
|
{/* Enhanced Charts Grid */}
|
|
<Grid container spacing={3} justifyContent="center">
|
|
{/* Area Chart with Gradient */}
|
|
<Grid item xs={12} lg={6} sx={{ minWidth: 400 }}>
|
|
<motion.div variants={chartVariants} initial="hidden" animate="visible">
|
|
<Paper elevation={0} sx={{ p: 3, height: 400 }} id="area-chart">
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
|
<Typography variant="h6" fontWeight={600}>
|
|
Activity Trends
|
|
</Typography>
|
|
<IconButton size="small" onClick={handleMenuClick}>
|
|
<MoreVert />
|
|
</IconButton>
|
|
</Box>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<AreaChart data={dailyData}>
|
|
<defs>
|
|
<linearGradient id="colorFeeding" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor={COLORS.feeding} stopOpacity={0.8}/>
|
|
<stop offset="95%" stopColor={COLORS.feeding} stopOpacity={0.1}/>
|
|
</linearGradient>
|
|
<linearGradient id="colorSleep" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor={COLORS.sleep} stopOpacity={0.8}/>
|
|
<stop offset="95%" stopColor={COLORS.sleep} stopOpacity={0.1}/>
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
|
<XAxis dataKey="date" />
|
|
<YAxis />
|
|
<Tooltip content={<CustomTooltip />} />
|
|
<Legend />
|
|
<Area
|
|
type="monotone"
|
|
dataKey="feedings"
|
|
stroke={COLORS.feeding}
|
|
fillOpacity={1}
|
|
fill="url(#colorFeeding)"
|
|
strokeWidth={2}
|
|
animationDuration={1000}
|
|
/>
|
|
<Area
|
|
type="monotone"
|
|
dataKey="sleepHours"
|
|
stroke={COLORS.sleep}
|
|
fillOpacity={1}
|
|
fill="url(#colorSleep)"
|
|
strokeWidth={2}
|
|
animationDuration={1200}
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</Paper>
|
|
</motion.div>
|
|
</Grid>
|
|
|
|
{/* Radar Chart for Pattern Analysis */}
|
|
<Grid item xs={12} lg={6} sx={{ minWidth: 400 }}>
|
|
<motion.div variants={chartVariants} initial="hidden" animate="visible">
|
|
<Paper elevation={0} sx={{ p: 3, height: 400 }} id="radar-chart">
|
|
<Typography variant="h6" fontWeight={600} sx={{ mb: 2 }}>
|
|
Weekly Pattern Analysis
|
|
</Typography>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<RadarChart data={weekdayPatterns}>
|
|
<PolarGrid strokeDasharray="3 3" />
|
|
<PolarAngleAxis dataKey="day" />
|
|
<PolarRadiusAxis angle={90} domain={[0, 'auto']} />
|
|
<Radar
|
|
name="Feedings"
|
|
dataKey="avgFeedings"
|
|
stroke={COLORS.feeding}
|
|
fill={COLORS.feeding}
|
|
fillOpacity={0.6}
|
|
/>
|
|
<Radar
|
|
name="Sleep"
|
|
dataKey="avgSleep"
|
|
stroke={COLORS.sleep}
|
|
fill={COLORS.sleep}
|
|
fillOpacity={0.6}
|
|
/>
|
|
<Radar
|
|
name="Diapers"
|
|
dataKey="avgDiapers"
|
|
stroke={COLORS.diaper}
|
|
fill={COLORS.diaper}
|
|
fillOpacity={0.6}
|
|
/>
|
|
<Legend />
|
|
</RadarChart>
|
|
</ResponsiveContainer>
|
|
</Paper>
|
|
</motion.div>
|
|
</Grid>
|
|
|
|
{/* Heatmap for Hourly Activity */}
|
|
<Grid item xs={12} lg={6} sx={{ minWidth: 400 }}>
|
|
<motion.div variants={chartVariants} initial="hidden" animate="visible">
|
|
<Paper elevation={0} sx={{ p: 3, height: 400 }} id="heatmap-chart">
|
|
<Typography variant="h6" fontWeight={600} sx={{ mb: 2 }}>
|
|
24-Hour Activity Pattern
|
|
</Typography>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<ComposedChart data={hourlyData}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis
|
|
dataKey="hour"
|
|
tickFormatter={(hour) => `${hour}:00`}
|
|
/>
|
|
<YAxis />
|
|
<Tooltip content={<CustomTooltip />} />
|
|
<Legend />
|
|
<Bar dataKey="feeding" stackId="a" fill={COLORS.feeding} />
|
|
<Bar dataKey="sleep" stackId="a" fill={COLORS.sleep} />
|
|
<Bar dataKey="diaper" stackId="a" fill={COLORS.diaper} />
|
|
<Line type="monotone" dataKey="activity" stroke="#ff7300" strokeWidth={2} />
|
|
</ComposedChart>
|
|
</ResponsiveContainer>
|
|
</Paper>
|
|
</motion.div>
|
|
</Grid>
|
|
|
|
{/* Bubble Chart for Correlations */}
|
|
<Grid item xs={12} lg={6} sx={{ minWidth: 400 }}>
|
|
<motion.div variants={chartVariants} initial="hidden" animate="visible">
|
|
<Paper elevation={0} sx={{ p: 3, height: 400 }} id="bubble-chart">
|
|
<Typography variant="h6" fontWeight={600} sx={{ mb: 2 }}>
|
|
Activity Correlations
|
|
</Typography>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<ScatterChart>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="x" name="Feedings" />
|
|
<YAxis dataKey="y" name="Sleep Hours" />
|
|
<ZAxis dataKey="z" range={[50, 400]} name="Diapers" />
|
|
<Tooltip cursor={{ strokeDasharray: '3 3' }} content={<CustomTooltip />} />
|
|
<Scatter
|
|
name="Days"
|
|
data={correlationData}
|
|
fill={COLORS.feeding}
|
|
fillOpacity={0.6}
|
|
>
|
|
{correlationData.map((entry, index) => (
|
|
<Cell key={`cell-${index}`} fill={COLORS[index % 2 === 0 ? 'feeding' : 'sleep']} />
|
|
))}
|
|
</Scatter>
|
|
</ScatterChart>
|
|
</ResponsiveContainer>
|
|
</Paper>
|
|
</motion.div>
|
|
</Grid>
|
|
|
|
{/* Radial Bar Chart for Time of Day */}
|
|
<Grid item xs={12} sx={{ minWidth: 400 }}>
|
|
<motion.div variants={chartVariants} initial="hidden" animate="visible">
|
|
<Paper elevation={0} sx={{ p: 3, minHeight: 400 }} id="radial-chart">
|
|
<Typography variant="h6" fontWeight={600} sx={{ mb: 2 }}>
|
|
Time of Day Distribution
|
|
</Typography>
|
|
<Box sx={{ width: '100%', minHeight: 300 }}>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<BarChart data={timeOfDayData} layout="horizontal">
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis type="number" />
|
|
<YAxis dataKey="period" type="category" />
|
|
<Tooltip content={<CustomTooltip />} />
|
|
<Legend />
|
|
<Bar dataKey="activities" fill={COLORS.feeding}>
|
|
{timeOfDayData.map((entry, index) => (
|
|
<Cell key={`cell-${index}`} fill={COLORS[entry.period as keyof typeof COLORS] || COLORS.feeding} />
|
|
))}
|
|
</Bar>
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</Box>
|
|
</Paper>
|
|
</motion.div>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
{/* Export Menu */}
|
|
<Menu
|
|
anchorEl={anchorEl}
|
|
open={Boolean(anchorEl)}
|
|
onClose={handleMenuClose}
|
|
>
|
|
<MUIMenuItem onClick={() => { exportChart('area-chart', 'png'); handleMenuClose(); }}>
|
|
<Download sx={{ mr: 1, fontSize: 20 }} /> Export as PNG
|
|
</MUIMenuItem>
|
|
<MUIMenuItem onClick={() => { exportChart('area-chart', 'pdf'); handleMenuClose(); }}>
|
|
<Download sx={{ mr: 1, fontSize: 20 }} /> Export as PDF
|
|
</MUIMenuItem>
|
|
<MUIMenuItem onClick={handleMenuClose}>
|
|
<Share sx={{ mr: 1, fontSize: 20 }} /> Share
|
|
</MUIMenuItem>
|
|
</Menu>
|
|
</Box>
|
|
);
|
|
}; |