- Add placeholderData to useRouteMetrics and usePunchcard hooks so data stays stable between refetches instead of going undefined → flicker - Disable Recharts animation on Treemap (isAnimationActive=false) - Make .content scrollable (overflow-y: auto, flex: 1, min-height: 0) so charts below the fold are accessible Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
130 lines
3.7 KiB
TypeScript
130 lines
3.7 KiB
TypeScript
import { useCallback } from 'react';
|
|
import { Treemap as RechartsTreemap, ResponsiveContainer, Tooltip } from 'recharts';
|
|
import { rechartsTheme } from '@cameleer/design-system';
|
|
|
|
export interface TreemapItem {
|
|
id: string;
|
|
label: string;
|
|
value: number;
|
|
/** 0-100, drives green→yellow→red color */
|
|
slaCompliance: number;
|
|
}
|
|
|
|
interface TreemapProps {
|
|
items: TreemapItem[];
|
|
onItemClick?: (id: string) => void;
|
|
}
|
|
|
|
function slaColor(pct: number): string {
|
|
if (pct >= 99) return 'hsl(120, 45%, 85%)';
|
|
if (pct >= 97) return 'hsl(90, 45%, 85%)';
|
|
if (pct >= 95) return 'hsl(60, 50%, 85%)';
|
|
if (pct >= 90) return 'hsl(30, 55%, 85%)';
|
|
return 'hsl(0, 55%, 85%)';
|
|
}
|
|
|
|
function slaBorderColor(pct: number): string {
|
|
if (pct >= 99) return 'hsl(120, 40%, 45%)';
|
|
if (pct >= 97) return 'hsl(90, 40%, 50%)';
|
|
if (pct >= 95) return 'hsl(60, 45%, 45%)';
|
|
if (pct >= 90) return 'hsl(30, 50%, 45%)';
|
|
return 'hsl(0, 50%, 45%)';
|
|
}
|
|
|
|
function slaTextColor(pct: number): string {
|
|
if (pct >= 95) return 'hsl(120, 20%, 25%)';
|
|
return 'hsl(0, 40%, 30%)';
|
|
}
|
|
|
|
/** Custom cell renderer for the Recharts Treemap */
|
|
function CustomCell(props: Record<string, unknown>) {
|
|
const { x, y, width, height, name, slaCompliance, onItemClick } = props as {
|
|
x: number; y: number; width: number; height: number;
|
|
name: string; slaCompliance: number; onItemClick?: (id: string) => void;
|
|
};
|
|
|
|
const w = width ?? 0;
|
|
const h = height ?? 0;
|
|
if (w < 2 || h < 2) return null;
|
|
|
|
const showLabel = w > 40 && h > 20;
|
|
const showSla = w > 60 && h > 34;
|
|
const sla = slaCompliance ?? 100;
|
|
|
|
return (
|
|
<g
|
|
onClick={() => onItemClick?.(name)}
|
|
style={{ cursor: onItemClick ? 'pointer' : 'default' }}
|
|
>
|
|
<rect
|
|
x={x + 1} y={y + 1} width={w - 2} height={h - 2}
|
|
rx={3}
|
|
fill={slaColor(sla)}
|
|
stroke={slaBorderColor(sla)}
|
|
strokeWidth={1}
|
|
/>
|
|
{showLabel && (
|
|
<text
|
|
x={x + 5} y={y + 15}
|
|
fill={slaTextColor(sla)}
|
|
fontSize={11} fontWeight={600}
|
|
style={{ pointerEvents: 'none' }}
|
|
>
|
|
{name.length > w / 6.5 ? name.slice(0, Math.floor(w / 6.5)) + '\u2026' : name}
|
|
</text>
|
|
)}
|
|
{showSla && (
|
|
<text
|
|
x={x + 5} y={y + 28}
|
|
fill={slaTextColor(sla)}
|
|
fontSize={10} fontWeight={400}
|
|
style={{ pointerEvents: 'none' }}
|
|
>
|
|
{sla.toFixed(1)}% SLA
|
|
</text>
|
|
)}
|
|
</g>
|
|
);
|
|
}
|
|
|
|
export function Treemap({ items, onItemClick }: TreemapProps) {
|
|
// Recharts Treemap expects { name, size, ...extra }
|
|
const data = items.map(i => ({
|
|
name: i.label,
|
|
size: i.value,
|
|
slaCompliance: i.slaCompliance,
|
|
}));
|
|
|
|
const renderContent = useCallback(
|
|
(props: Record<string, unknown>) => <CustomCell {...props} onItemClick={onItemClick} />,
|
|
[onItemClick],
|
|
);
|
|
|
|
if (items.length === 0) {
|
|
return <div style={{ color: 'var(--text-muted)', textAlign: 'center', padding: '2rem' }}>No data</div>;
|
|
}
|
|
|
|
return (
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<RechartsTreemap
|
|
data={data}
|
|
dataKey="size"
|
|
nameKey="name"
|
|
stroke="none"
|
|
content={renderContent}
|
|
isAnimationActive={false}
|
|
>
|
|
<Tooltip
|
|
contentStyle={rechartsTheme.tooltip.contentStyle}
|
|
labelStyle={rechartsTheme.tooltip.labelStyle}
|
|
itemStyle={rechartsTheme.tooltip.itemStyle}
|
|
formatter={(value: number, _name: string, entry: { payload?: { slaCompliance?: number } }) => {
|
|
const sla = entry.payload?.slaCompliance ?? 0;
|
|
return [`${value.toLocaleString()} exchanges · ${sla.toFixed(1)}% SLA`];
|
|
}}
|
|
/>
|
|
</RechartsTreemap>
|
|
</ResponsiveContainer>
|
|
);
|
|
}
|