From 4a91ca07743d98f819f8be19bea9210e30d05b5f Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:45:22 +0200 Subject: [PATCH] feat: consolidate punchcard heatmaps into single toggle component Replace two separate Transaction/Error punchcard cards with a single card containing a Transactions/Errors toggle. Uses internal state to switch between modes without remounting the chart. Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/src/pages/DashboardTab/DashboardL1.tsx | 11 +- ui/src/pages/DashboardTab/DashboardL2.tsx | 11 +- .../DashboardTab/DashboardTab.module.css | 31 ++++ .../pages/DashboardTab/PunchcardHeatmap.tsx | 146 ++++++++++-------- 4 files changed, 120 insertions(+), 79 deletions(-) diff --git a/ui/src/pages/DashboardTab/DashboardL1.tsx b/ui/src/pages/DashboardTab/DashboardL1.tsx index d091eaff..ed1fc556 100644 --- a/ui/src/pages/DashboardTab/DashboardL1.tsx +++ b/ui/src/pages/DashboardTab/DashboardL1.tsx @@ -456,14 +456,9 @@ export default function DashboardL1() { onItemClick={(id) => navigate(`/dashboard/${id}`)} /> -
- - - - - - -
+ + + )} diff --git a/ui/src/pages/DashboardTab/DashboardL2.tsx b/ui/src/pages/DashboardTab/DashboardL2.tsx index d16b65d7..702d8076 100644 --- a/ui/src/pages/DashboardTab/DashboardL2.tsx +++ b/ui/src/pages/DashboardTab/DashboardL2.tsx @@ -437,14 +437,9 @@ export default function DashboardL2() { onItemClick={(id) => navigate(`/dashboard/${appId}/${id}`)} /> -
- - - - - - -
+ + + )} diff --git a/ui/src/pages/DashboardTab/DashboardTab.module.css b/ui/src/pages/DashboardTab/DashboardTab.module.css index 41a1b400..f9f9abe2 100644 --- a/ui/src/pages/DashboardTab/DashboardTab.module.css +++ b/ui/src/pages/DashboardTab/DashboardTab.module.css @@ -140,6 +140,37 @@ gap: 16px; } +/* Toggle button row */ +.toggleRow { + display: flex; + gap: 2px; + padding: 0 12px 4px; +} + +.toggleBtn { + padding: 3px 10px; + font-size: 11px; + font-family: var(--font-mono); + color: var(--text-muted); + background: transparent; + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + cursor: pointer; + transition: all 0.15s; +} + +.toggleBtn:hover { + color: var(--text-primary); + border-color: var(--border); +} + +.toggleActive { + color: var(--text-primary); + background: var(--bg-inset); + border-color: var(--border); + font-weight: 600; +} + /* Errors section */ .errorsSection { background: var(--bg-surface); diff --git a/ui/src/pages/DashboardTab/PunchcardHeatmap.tsx b/ui/src/pages/DashboardTab/PunchcardHeatmap.tsx index abeacff7..345440bb 100644 --- a/ui/src/pages/DashboardTab/PunchcardHeatmap.tsx +++ b/ui/src/pages/DashboardTab/PunchcardHeatmap.tsx @@ -1,9 +1,10 @@ -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { ScatterChart, Scatter, XAxis, YAxis, Tooltip, ResponsiveContainer, Rectangle, } from 'recharts'; import { rechartsTheme } from '@cameleer/design-system'; +import styles from './DashboardTab.module.css'; export interface PunchcardCell { weekday: number; @@ -14,9 +15,10 @@ export interface PunchcardCell { interface PunchcardHeatmapProps { cells: PunchcardCell[]; - mode: 'transactions' | 'errors'; } +type Mode = 'transactions' | 'errors'; + const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; function transactionColor(ratio: number): string { @@ -43,7 +45,6 @@ function HeatmapCell(props: Record) { 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 ( @@ -66,70 +67,89 @@ 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); +function buildGrid(cells: PunchcardCell[], mode: Mode): HeatmapPoint[] { + 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); + 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), - }); - } + const points: HeatmapPoint[] = []; + 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 points; +} + +export function PunchcardHeatmap({ cells }: PunchcardHeatmapProps) { + const [mode, setMode] = useState('transactions'); + + const data = useMemo(() => buildGrid(cells, mode), [cells, mode]); return ( - - - - - { - const p = entry.payload; - if (!p) return ''; - return [`${p.value.toLocaleString()} ${mode}`, `${DAYS[p.weekday]} ${formatHour(p.hour)}:00`]; - }} - labelFormatter={() => ''} - /> - )} />} - isAnimationActive={false} - /> - - +
+
+ + +
+ + + + + { + const p = entry.payload; + if (!p) return ''; + return [`${p.value.toLocaleString()} ${mode}`, `${DAYS[p.weekday]} ${formatHour(p.hour)}:00`]; + }} + labelFormatter={() => ''} + /> + )} />} + isAnimationActive={false} + /> + + +
); }