diff --git a/package.json b/package.json index 46c23ff1..b83a6b45 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3.3.5", + "@playwright/test": "^1.54.0", "@tailwindcss/postcss": "^4.0.0", "@tailwindcss/vite": "^4.2.0", "@testing-library/dom": "^10.4.1", @@ -114,8 +115,9 @@ "postcss": "^8.5.6", "prettier": "^2.8.8", "tailwindcss": "^4.0.0", + "tsx": "^4.20.3", "typescript": "^5.8.3", "vite": "^5.4.19", "vitest": "^2.1.9" } -} +} \ No newline at end of file diff --git a/src/components/charts/InteractiveChart.tsx b/src/components/charts/InteractiveChart.tsx new file mode 100644 index 00000000..75850ef7 --- /dev/null +++ b/src/components/charts/InteractiveChart.tsx @@ -0,0 +1,135 @@ +import React, { useMemo } from 'react'; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + AreaChart, + Area, + BarChart, + Bar, + Legend +} from 'recharts'; + +export type ChartType = 'line' | 'area' | 'bar'; + +interface InteractiveChartProps { + data: any[]; + xKey: string; + yKeys: { key: string; color: string; name?: string }[]; + type?: ChartType; + height?: number | string; + title?: string; + syncId?: string; + className?: string; +} + +export const InteractiveChart: React.FC = ({ + data, + xKey, + yKeys, + type = 'line', + height = 300, + title, + syncId, + className = '', +}) => { + // Optimization for large datasets (10k+ points): + // If data is very large, we can sample it or simplify it before rendering + // But Recharts handles a few thousand points well. Let's add a simple sampling if data > 1000 + const optimizedData = useMemo(() => { + if (!data || data.length <= 1000) return data; + const factor = Math.ceil(data.length / 1000); + return data.filter((_, index) => index % factor === 0); + }, [data]); + + const renderChart = () => { + switch (type) { + case 'area': + return ( + + + + + + + {yKeys.map((yConfig) => ( + + ))} + + ); + case 'bar': + return ( + + + + + + + {yKeys.map((yConfig) => ( + + ))} + + ); + case 'line': + default: + return ( + + + + + + + {yKeys.map((yConfig) => ( + + ))} + + ); + } + }; + + return ( +
+ {title &&

{title}

} +
+ + {renderChart()} + +
+
+ ); +}; diff --git a/src/hooks/useRealTimeAnalytics.ts b/src/hooks/useRealTimeAnalytics.ts new file mode 100644 index 00000000..00163173 --- /dev/null +++ b/src/hooks/useRealTimeAnalytics.ts @@ -0,0 +1,44 @@ +import { useState, useEffect, useCallback } from 'react'; + +export interface AnalyticsDataPoint { + timestamp: string; + value: number; + category?: string; +} + +export const useRealTimeAnalytics = (initialData: AnalyticsDataPoint[] = []) => { + const [data, setData] = useState(initialData); + const [isConnected, setIsConnected] = useState(false); + + // In a real application, this would connect to a real WebSocket endpoint + // For the sake of this frontend implementation, we simulate the WebSocket stream + useEffect(() => { + // Simulate WebSocket connection + setIsConnected(true); + + const interval = setInterval(() => { + setData((prevData) => { + const newDataPoint: AnalyticsDataPoint = { + timestamp: new Date().toISOString(), + value: Math.floor(Math.random() * 100) + 10, + category: ['engagement', 'learning', 'performance'][Math.floor(Math.random() * 3)], + }; + + // Keep the last 50 points to avoid memory issues while demonstrating streaming + const updatedData = [...prevData, newDataPoint]; + return updatedData.length > 50 ? updatedData.slice(updatedData.length - 50) : updatedData; + }); + }, 2000); // 2 seconds update as per requirements + + return () => { + clearInterval(interval); + setIsConnected(false); + }; + }, []); + + const addDataPoint = useCallback((point: AnalyticsDataPoint) => { + setData((prev) => [...prev, point]); + }, []); + + return { data, isConnected, addDataPoint }; +};