/** * Live Chart Component * * Displays real-time data as a simple line chart. */ import React, { useMemo } from 'react'; import classNames from 'classnames'; import { useSocket } from '../../lib/realtime/useSocket'; import type { TransparencyEventType } from '../../lib/realtime/types'; interface LiveChartProps { eventTypes?: TransparencyEventType[]; dataKey?: string; title?: string; color?: string; height?: number; maxDataPoints?: number; showGrid?: boolean; className?: string; } /** * Simple SVG line chart for real-time data */ export function LiveChart({ eventTypes = ['system.metric'], dataKey = 'value', title = 'Live Data', color = '#3B82F6', height = 120, maxDataPoints = 30, showGrid = true, className, }: LiveChartProps) { const { events } = useSocket({ eventTypes, maxEvents: maxDataPoints, }); // Extract data points const dataPoints = useMemo(() => { return events .filter((e) => e.data && typeof e.data[dataKey] === 'number') .map((e) => ({ value: e.data[dataKey] as number, timestamp: new Date(e.timestamp).getTime(), })) .reverse() .slice(-maxDataPoints); }, [events, dataKey, maxDataPoints]); // Calculate chart dimensions const chartWidth = 400; const chartHeight = height - 40; const padding = { top: 10, right: 10, bottom: 20, left: 40 }; const innerWidth = chartWidth - padding.left - padding.right; const innerHeight = chartHeight - padding.top - padding.bottom; // Calculate scales const { minValue, maxValue, points, pathD } = useMemo(() => { if (dataPoints.length === 0) { return { minValue: 0, maxValue: 100, points: [], pathD: '' }; } const values = dataPoints.map((d) => d.value); const min = Math.min(...values); const max = Math.max(...values); const range = max - min || 1; const pts = dataPoints.map((d, i) => ({ x: padding.left + (i / Math.max(1, dataPoints.length - 1)) * innerWidth, y: padding.top + innerHeight - ((d.value - min) / range) * innerHeight, })); const d = pts.length > 0 ? `M ${pts.map((p) => `${p.x},${p.y}`).join(' L ')}` : ''; return { minValue: min, maxValue: max, points: pts, pathD: d }; }, [dataPoints, innerWidth, innerHeight, padding]); // Latest value const latestValue = dataPoints.length > 0 ? dataPoints[dataPoints.length - 1].value : null; return (
{/* Header */}

{title}

{latestValue !== null && ( {latestValue.toFixed(1)} )}
{/* Chart */} {/* Grid */} {showGrid && ( {/* Horizontal grid lines */} {[0, 0.25, 0.5, 0.75, 1].map((ratio) => ( ))} {/* Vertical grid lines */} {[0, 0.5, 1].map((ratio) => ( ))} )} {/* Y-axis labels */} {maxValue.toFixed(0)} {minValue.toFixed(0)} {/* Line path */} {pathD && ( <> {/* Gradient area */} )} {/* Data points */} {points.map((p, i) => ( ))} {/* No data message */} {dataPoints.length === 0 && ( Waiting for data... )}
); } /** * Event count chart - shows event frequency over time */ export function EventCountChart({ className, }: { className?: string; }) { const { events } = useSocket({ maxEvents: 100 }); // Group events by minute const countsByMinute = useMemo(() => { const counts: Record = {}; const now = Date.now(); // Initialize last 10 minutes for (let i = 0; i < 10; i++) { const minute = Math.floor((now - i * 60000) / 60000); counts[minute] = 0; } // Count events events.forEach((e) => { const minute = Math.floor(new Date(e.timestamp).getTime() / 60000); if (counts[minute] !== undefined) { counts[minute]++; } }); return Object.entries(counts) .sort(([a], [b]) => Number(a) - Number(b)) .map(([, count]) => count); }, [events]); const maxCount = Math.max(...countsByMinute, 1); return (

Events per Minute

{countsByMinute.map((count, i) => (
))}
10m ago Now
); } export default LiveChart;