import { useMemo } from 'react'; import { ScatterChart, Scatter, XAxis, YAxis, Tooltip, ResponsiveContainer, Rectangle, } from 'recharts'; import { rechartsTheme } from '@cameleer/design-system'; export interface PunchcardCell { weekday: number; hour: number; totalCount: number; failedCount: number; } interface PunchcardHeatmapProps { cells: PunchcardCell[]; mode: 'transactions' | 'errors'; } const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; function transactionColor(ratio: number): string { if (ratio === 0) return 'var(--bg-inset)'; const lightness = 90 - ratio * 55; return `hsl(220, 60%, ${Math.round(lightness)}%)`; } function errorColor(ratio: number): string { if (ratio === 0) return 'var(--bg-inset)'; const lightness = 90 - ratio * 55; return `hsl(0, 65%, ${Math.round(lightness)}%)`; } interface HeatmapPoint { weekday: number; hour: number; value: number; fill: string; } function HeatmapCell(props: Record) { const { cx, cy, payload } = props as { cx: number; cy: number; payload: HeatmapPoint; }; if (!payload) return null; // Cell size: chart area / grid divisions. Approximate from scatter positioning. const cellW = 32; const cellH = 10; return ( ); } function formatDay(value: number): string { return DAYS[value] ?? ''; } function formatHour(value: number): string { return String(value).padStart(2, '0'); } export function PunchcardHeatmap({ cells, mode }: PunchcardHeatmapProps) { const data: HeatmapPoint[] = useMemo(() => { const values = cells.map(c => mode === 'errors' ? c.failedCount : c.totalCount); const maxVal = Math.max(...values, 1); // Build full 7x24 grid const points: HeatmapPoint[] = []; const cellMap = new Map(); for (const c of cells) cellMap.set(`${c.weekday}-${c.hour}`, c); for (let d = 0; d < 7; d++) { for (let h = 0; h < 24; h++) { const cell = cellMap.get(`${d}-${h}`); const val = cell ? (mode === 'errors' ? cell.failedCount : cell.totalCount) : 0; const ratio = maxVal > 0 ? val / maxVal : 0; points.push({ weekday: d, hour: h, value: val, fill: mode === 'errors' ? errorColor(ratio) : transactionColor(ratio), }); } } return points; }, [cells, mode]); return ( { const p = entry.payload; if (!p) return ''; return [`${p.value.toLocaleString()} ${mode}`, `${DAYS[p.weekday]} ${formatHour(p.hour)}:00`]; }} labelFormatter={() => ''} /> ); }