import { useMemo, useState } from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import type { HaystackCategory, HaystackPoint } from '../../types/labeleval'; import { SectionCard, fmtInt } from './shared'; const WIDTH = 700; const HEIGHT = 420; const PAD = 24; const CATEGORY_STYLE: Record = { normal: { color: '#5c6773', label: 'Normal' }, anomaly: { color: '#ffa726', label: 'Anomaly' }, rare_event: { color: '#ef5350', label: 'Rare event' }, false_positive: { color: '#ab47bc', label: 'False positive' }, false_negative: { color: '#42a5f5', label: 'False negative' }, verified: { color: '#66bb6a', label: 'Verified' }, }; const CATEGORY_ORDER: HaystackCategory[] = [ 'normal', 'verified', 'false_positive', 'false_negative', 'anomaly', 'rare_event', ]; export default function HaystackVisualizer({ points, onPointClick, }: { points: HaystackPoint[]; onPointClick: (point: HaystackPoint) => void; }) { const [hovered, setHovered] = useState(null); const { scaled, counts } = useMemo(() => { const counts = new Map(); for (const p of points) counts.set(p.category, (counts.get(p.category) ?? 0) + 1); if (points.length === 0) return { scaled: [] as Array, counts }; let minX = Infinity; let maxX = -Infinity; let minY = Infinity; let maxY = -Infinity; for (const p of points) { if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x; if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y; } const spanX = maxX - minX || 1; const spanY = maxY - minY || 1; const scaled = points.map((p) => ({ ...p, sx: PAD + ((p.x - minX) / spanX) * (WIDTH - 2 * PAD), sy: HEIGHT - PAD - ((p.y - minY) / spanY) * (HEIGHT - 2 * PAD), })); // Draw interesting categories on top of the normal haystack. scaled.sort((a, b) => CATEGORY_ORDER.indexOf(a.category) - CATEGORY_ORDER.indexOf(b.category)); return { scaled, counts }; }, [points]); return ( {/* subtle grid */} {Array.from({ length: 7 }, (_, i) => ( ))} {Array.from({ length: 5 }, (_, i) => ( ))} {scaled.map((p) => { const style = CATEGORY_STYLE[p.category]; const interesting = p.category !== 'normal'; const common = { cursor: 'pointer', onClick: () => onPointClick(p), onMouseEnter: () => setHovered(p), onMouseLeave: () => setHovered(null), }; if (p.category === 'rare_event') { const r = 6; return ( ); } return ( ); })} {points.length === 0 ? ( No haystack points yet — run the evaluation pipeline. ) : null} {hovered ? ( {hovered.id} {hovered.class_name} · {CATEGORY_STYLE[hovered.category].label} · score{' '} {hovered.anomaly_score.toFixed(3)} · {hovered.frame_id} ) : null} LEGEND {CATEGORY_ORDER.map((cat) => { const style = CATEGORY_STYLE[cat]; return ( {cat === 'rare_event' ? ( ) : ( )} {style.label} {fmtInt(counts.get(cat) ?? 0)} ); })} Click any point to open its full evidence panel. ); }