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) <noreply@anthropic.com>
This commit is contained in:
@@ -456,14 +456,9 @@ export default function DashboardL1() {
|
|||||||
onItemClick={(id) => navigate(`/dashboard/${id}`)}
|
onItemClick={(id) => navigate(`/dashboard/${id}`)}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
<div className={styles.punchcardStack}>
|
<Card title="7-Day Pattern">
|
||||||
<Card title="Transactions (7-day pattern)">
|
<PunchcardHeatmap cells={punchcardData ?? []} />
|
||||||
<PunchcardHeatmap cells={punchcardData ?? []} mode="transactions" />
|
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="Errors (7-day pattern)">
|
|
||||||
<PunchcardHeatmap cells={punchcardData ?? []} mode="errors" />
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -437,14 +437,9 @@ export default function DashboardL2() {
|
|||||||
onItemClick={(id) => navigate(`/dashboard/${appId}/${id}`)}
|
onItemClick={(id) => navigate(`/dashboard/${appId}/${id}`)}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
<div className={styles.punchcardStack}>
|
<Card title="7-Day Pattern">
|
||||||
<Card title="Transactions (7-day pattern)">
|
<PunchcardHeatmap cells={punchcardData ?? []} />
|
||||||
<PunchcardHeatmap cells={punchcardData ?? []} mode="transactions" />
|
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="Errors (7-day pattern)">
|
|
||||||
<PunchcardHeatmap cells={punchcardData ?? []} mode="errors" />
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -140,6 +140,37 @@
|
|||||||
gap: 16px;
|
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 */
|
/* Errors section */
|
||||||
.errorsSection {
|
.errorsSection {
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ScatterChart, Scatter, XAxis, YAxis, Tooltip,
|
ScatterChart, Scatter, XAxis, YAxis, Tooltip,
|
||||||
ResponsiveContainer, Rectangle,
|
ResponsiveContainer, Rectangle,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
import { rechartsTheme } from '@cameleer/design-system';
|
import { rechartsTheme } from '@cameleer/design-system';
|
||||||
|
import styles from './DashboardTab.module.css';
|
||||||
|
|
||||||
export interface PunchcardCell {
|
export interface PunchcardCell {
|
||||||
weekday: number;
|
weekday: number;
|
||||||
@@ -14,9 +15,10 @@ export interface PunchcardCell {
|
|||||||
|
|
||||||
interface PunchcardHeatmapProps {
|
interface PunchcardHeatmapProps {
|
||||||
cells: PunchcardCell[];
|
cells: PunchcardCell[];
|
||||||
mode: 'transactions' | 'errors';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Mode = 'transactions' | 'errors';
|
||||||
|
|
||||||
const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
|
|
||||||
function transactionColor(ratio: number): string {
|
function transactionColor(ratio: number): string {
|
||||||
@@ -43,7 +45,6 @@ function HeatmapCell(props: Record<string, unknown>) {
|
|||||||
cx: number; cy: number; payload: HeatmapPoint;
|
cx: number; cy: number; payload: HeatmapPoint;
|
||||||
};
|
};
|
||||||
if (!payload) return null;
|
if (!payload) return null;
|
||||||
// Cell size: chart area / grid divisions. Approximate from scatter positioning.
|
|
||||||
const cellW = 32;
|
const cellW = 32;
|
||||||
const cellH = 10;
|
const cellH = 10;
|
||||||
return (
|
return (
|
||||||
@@ -66,16 +67,14 @@ function formatHour(value: number): string {
|
|||||||
return String(value).padStart(2, '0');
|
return String(value).padStart(2, '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PunchcardHeatmap({ cells, mode }: PunchcardHeatmapProps) {
|
function buildGrid(cells: PunchcardCell[], mode: Mode): HeatmapPoint[] {
|
||||||
const data: HeatmapPoint[] = useMemo(() => {
|
|
||||||
const values = cells.map(c => mode === 'errors' ? c.failedCount : c.totalCount);
|
const values = cells.map(c => mode === 'errors' ? c.failedCount : c.totalCount);
|
||||||
const maxVal = Math.max(...values, 1);
|
const maxVal = Math.max(...values, 1);
|
||||||
|
|
||||||
// Build full 7x24 grid
|
|
||||||
const points: HeatmapPoint[] = [];
|
|
||||||
const cellMap = new Map<string, PunchcardCell>();
|
const cellMap = new Map<string, PunchcardCell>();
|
||||||
for (const c of cells) cellMap.set(`${c.weekday}-${c.hour}`, c);
|
for (const c of cells) cellMap.set(`${c.weekday}-${c.hour}`, c);
|
||||||
|
|
||||||
|
const points: HeatmapPoint[] = [];
|
||||||
for (let d = 0; d < 7; d++) {
|
for (let d = 0; d < 7; d++) {
|
||||||
for (let h = 0; h < 24; h++) {
|
for (let h = 0; h < 24; h++) {
|
||||||
const cell = cellMap.get(`${d}-${h}`);
|
const cell = cellMap.get(`${d}-${h}`);
|
||||||
@@ -90,10 +89,30 @@ export function PunchcardHeatmap({ cells, mode }: PunchcardHeatmapProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return points;
|
return points;
|
||||||
}, [cells, mode]);
|
}
|
||||||
|
|
||||||
|
export function PunchcardHeatmap({ cells }: PunchcardHeatmapProps) {
|
||||||
|
const [mode, setMode] = useState<Mode>('transactions');
|
||||||
|
|
||||||
|
const data = useMemo(() => buildGrid(cells, mode), [cells, mode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<div>
|
||||||
|
<div className={styles.toggleRow}>
|
||||||
|
<button
|
||||||
|
className={`${styles.toggleBtn} ${mode === 'transactions' ? styles.toggleActive : ''}`}
|
||||||
|
onClick={() => setMode('transactions')}
|
||||||
|
>
|
||||||
|
Transactions
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.toggleBtn} ${mode === 'errors' ? styles.toggleActive : ''}`}
|
||||||
|
onClick={() => setMode('errors')}
|
||||||
|
>
|
||||||
|
Errors
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ResponsiveContainer width="100%" height={280}>
|
||||||
<ScatterChart margin={{ top: 10, right: 10, bottom: 10, left: 10 }}>
|
<ScatterChart margin={{ top: 10, right: 10, bottom: 10, left: 10 }}>
|
||||||
<XAxis
|
<XAxis
|
||||||
type="number"
|
type="number"
|
||||||
@@ -131,5 +150,6 @@ export function PunchcardHeatmap({ cells, mode }: PunchcardHeatmapProps) {
|
|||||||
/>
|
/>
|
||||||
</ScatterChart>
|
</ScatterChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user