Files
cameleer-server/ui/src/pages/DashboardTab/Treemap.tsx
hsiegeln 52c22f1eb9
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m3s
CI / docker (push) Successful in 54s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
fix: dashboard flickering on poll, animation replay, and scroll
- 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>
2026-03-30 15:42:02 +02:00

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>
);
}