Host map One cell per machine, sized so a fleet fits on a screen. It answers how many and where, and it stops working past the point where a cell is smaller than a comfortable target. Tokens this needs: --card, --muted-foreground, --border, --scale-seq-1, --scale-seq-2, --scale-seq-3, --scale-seq-4, --scale-seq-5 The status, chart, scale, state and direction names are an extension, not a rename. shadcn has --destructive and five --chart-* and nothing else in this territory. ──────────────────────────────────────────────────────────────────────── // HostMap.tsx ──────────────────────────────────────────────────────────────────────── /** * One cell per machine, coloured by one metric, arranged by one grouping. * The grouping is what makes it diagnostic: hosts are placed by zone first, * so "one machine is hot" and "one whole zone is hot" look different, and * they are different incidents. */ export type Host = { id: string; zone: string; /** 0–1. null means no sample arrived, which is drawn as its own thing * rather than as idle. */ value: number | null; }; /** Five steps of the sequential ramp. Full class names, because Tailwind * only emits a class it can see written out. */ const RAMP = ["fill-scale-seq-1", "fill-scale-seq-2", "fill-scale-seq-3", "fill-scale-seq-4", "fill-scale-seq-5"]; const SWATCH = ["bg-scale-seq-1", "bg-scale-seq-2", "bg-scale-seq-3", "bg-scale-seq-4", "bg-scale-seq-5"]; const step = (v: number) => RAMP[Math.min(4, Math.floor(v * RAMP.length))]; /** Pointy-top hexagon. Every neighbour shares an edge, so a cluster of hot * cells reads as a blob rather than a diagonal the eye has to assemble. */ const hex = (cx: number, cy: number, r: number) => { const w = Math.sqrt(3) * r; return [[cx, cy - r], [cx + w / 2, cy - r / 2], [cx + w / 2, cy + r / 2], [cx, cy + r], [cx - w / 2, cy + r / 2], [cx - w / 2, cy - r / 2]] .map(([x, y]) => `${x.toFixed(1)},${y.toFixed(1)}`).join(" "); }; export function HostMap({ hosts, metric, onSelectHost, columns = 7, r = 13 }: { hosts: Host[]; /** What the colour means, for the legend and the cell tooltip. */ metric: string; /** A cell goes somewhere scoped to that host. */ onSelectHost: (host: Host) => void; columns?: number; r?: number; }) { const w = Math.sqrt(3) * r; const zones = [...new Set(hosts.map((h) => h.zone))]; const rows = Math.ceil(hosts.length / zones.length / columns); const width = w * (columns + 0.5) + 4; const height = r * (1.5 * rows + 0.5) + 4; return (
{zone} {Math.round(mean * 100)}% mean
open {picked.id} at {Math.round((picked.value ?? 0) * 100)}% CPU
)}