feat: add treemap and punchcard heatmap to dashboard L1/L2 (#94)
Treemap: rectangle area = transaction volume, color = SLA compliance (green→red). Shows apps at L1, routes at L2. Click navigates deeper. Punchcard heatmap: 7-day rolling weekday x 24-hour grid showing transaction volume and error patterns. Two side-by-side views (transactions + errors) reveal temporal clustering. Backend: new GET /search/stats/punchcard endpoint aggregating stats_1m_all/app by DOW x hour over rolling 7 days. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,8 +18,12 @@ import {
|
||||
useTimeseriesByRoute,
|
||||
useTopErrors,
|
||||
useAppSettings,
|
||||
usePunchcard,
|
||||
} from '../../api/queries/dashboard';
|
||||
import type { TopError } from '../../api/queries/dashboard';
|
||||
import { Treemap } from './Treemap';
|
||||
import type { TreemapItem } from './Treemap';
|
||||
import { PunchcardHeatmap } from './PunchcardHeatmap';
|
||||
import type { RouteMetrics } from '../../api/types';
|
||||
import {
|
||||
trendArrow,
|
||||
@@ -272,28 +276,34 @@ export default function DashboardL2() {
|
||||
const { data: metrics } = useRouteMetrics(timeFrom, timeTo, appId);
|
||||
const { data: timeseriesByRoute } = useTimeseriesByRoute(timeFrom, timeTo, appId);
|
||||
const { data: errors } = useTopErrors(timeFrom, timeTo, appId);
|
||||
const { data: punchcardData } = usePunchcard(appId);
|
||||
const { data: appSettings } = useAppSettings(appId);
|
||||
|
||||
const slaThresholdMs = appSettings?.slaThresholdMs ?? 300;
|
||||
|
||||
// Route performance table rows
|
||||
const routeRows: RouteRow[] = useMemo(() =>
|
||||
(metrics || []).map((m: RouteMetrics) => {
|
||||
const sla = m.p99DurationMs <= slaThresholdMs
|
||||
? 99.9
|
||||
: Math.max(0, 100 - ((m.p99DurationMs - slaThresholdMs) / slaThresholdMs) * 10);
|
||||
return {
|
||||
id: m.routeId,
|
||||
routeId: m.routeId,
|
||||
exchangeCount: m.exchangeCount,
|
||||
successRate: m.successRate,
|
||||
avgDurationMs: m.avgDurationMs,
|
||||
p99DurationMs: m.p99DurationMs,
|
||||
slaCompliance: sla,
|
||||
sparkline: m.sparkline ?? [],
|
||||
};
|
||||
}),
|
||||
[metrics, slaThresholdMs],
|
||||
(metrics || []).map((m: RouteMetrics) => ({
|
||||
id: m.routeId,
|
||||
routeId: m.routeId,
|
||||
exchangeCount: m.exchangeCount,
|
||||
successRate: m.successRate,
|
||||
avgDurationMs: m.avgDurationMs,
|
||||
p99DurationMs: m.p99DurationMs,
|
||||
slaCompliance: (m as Record<string, unknown>).slaCompliance as number ?? -1,
|
||||
sparkline: m.sparkline ?? [],
|
||||
})),
|
||||
[metrics],
|
||||
);
|
||||
|
||||
// Treemap items: one per route, sized by exchange count, colored by SLA
|
||||
const treemapItems: TreemapItem[] = useMemo(
|
||||
() => routeRows.map(r => ({
|
||||
id: r.routeId, label: r.routeId,
|
||||
value: r.exchangeCount,
|
||||
slaCompliance: r.slaCompliance >= 0 ? r.slaCompliance : 100,
|
||||
})),
|
||||
[routeRows],
|
||||
);
|
||||
|
||||
// KPI sparklines from timeseries
|
||||
@@ -416,6 +426,30 @@ export default function DashboardL2() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Treemap: route volume vs SLA compliance */}
|
||||
{treemapItems.length > 0 && (
|
||||
<Card title="Route Volume vs SLA Compliance">
|
||||
<Treemap
|
||||
items={treemapItems}
|
||||
width={800}
|
||||
height={250}
|
||||
onItemClick={(id) => navigate(`/dashboard/${appId}/${id}`)}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Punchcard heatmaps: transactions and errors by weekday x hour */}
|
||||
{(punchcardData?.length ?? 0) > 0 && (
|
||||
<div className={styles.chartGrid}>
|
||||
<Card title="Transaction Volume (7-day pattern)">
|
||||
<PunchcardHeatmap cells={punchcardData!} mode="transactions" width={380} height={300} />
|
||||
</Card>
|
||||
<Card title="Error Volume (7-day pattern)">
|
||||
<PunchcardHeatmap cells={punchcardData!} mode="errors" width={380} height={300} />
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user