import { useMemo, useState } from 'react'; import styles from './DashboardTab.module.css'; export interface PunchcardCell { weekday: number; hour: number; totalCount: number; failedCount: number; } interface PunchcardHeatmapProps { cells: PunchcardCell[]; timeRangeMs?: number; } type Mode = 'transactions' | 'errors'; const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; // Remap: backend DOW 0=Sun..6=Sat → display 0=Mon..6=Sun function toDisplayDay(dow: number): number { return dow === 0 ? 6 : dow - 1; } function transactionColor(ratio: number): string { if (ratio === 0) return 'var(--bg-inset)'; // Blue scale matching --running hue const alpha = 0.15 + ratio * 0.75; return `hsla(220, 65%, 50%, ${alpha.toFixed(2)})`; } function errorColor(ratio: number): string { if (ratio === 0) return 'var(--bg-inset)'; const alpha = 0.15 + ratio * 0.75; return `hsla(0, 65%, 50%, ${alpha.toFixed(2)})`; } const CELL = 11; const GAP = 2; const LABEL_W = 28; const LABEL_H = 14; const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1000; export function PunchcardHeatmap({ cells, timeRangeMs }: PunchcardHeatmapProps) { const [mode, setMode] = useState('transactions'); const insufficientData = timeRangeMs !== undefined && timeRangeMs < TWO_DAYS_MS; const { grid, maxVal } = useMemo(() => { const cellMap = new Map(); for (const c of cells) cellMap.set(`${toDisplayDay(c.weekday)}-${c.hour}`, c); let max = 0; const g: { day: number; hour: number; value: number }[] = []; 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; if (val > max) max = val; g.push({ day: d, hour: h, value: val }); } } return { grid: g, maxVal: Math.max(max, 1) }; }, [cells, mode]); const cols = 24; const rows = 7; const svgW = LABEL_W + cols * (CELL + GAP); const svgH = LABEL_H + rows * (CELL + GAP); if (insufficientData) { return (
Requires at least 2 days of data
); } return (
{/* Hour labels (top, every 4 hours) */} {[0, 4, 8, 12, 16, 20].map(h => ( {String(h).padStart(2, '0')} ))} {/* Day labels (left) */} {DAYS.map((day, i) => ( {day} ))} {/* Cells */} {grid.map(({ day, hour, value }) => { const ratio = value / maxVal; const fill = mode === 'errors' ? errorColor(ratio) : transactionColor(ratio); return ( {`${DAYS[day]} ${String(hour).padStart(2, '0')}:00 — ${value.toLocaleString()} ${mode}`} ); })}
); }