import { useCallback, useMemo, useState } from 'react'; import { ScatterChart, Scatter, XAxis, YAxis, Tooltip, Cell, ResponsiveContainer } from 'recharts'; const CLUSTER_COLORS = [ '#f59e0b', '#3b82f6', '#22c55e', '#ef4444', '#a855f7', '#06b6d4', '#f97316', '#ec4899', '#84cc16', '#14b8a6', ]; const ANOMALY_COLOR = '#4b4b4b'; const MAX_POINTS = 500; function stratifiedSample(clusterPoints, anomalyPoints, max) { const total = clusterPoints.length + anomalyPoints.length; if (total <= max) return { points: [...clusterPoints, ...anomalyPoints], sampled: false }; const budgetForClusters = max - anomalyPoints.length; const ratio = budgetForClusters / clusterPoints.length; const sampledClusters = ratio >= 1 ? clusterPoints : clusterPoints.filter(() => Math.random() < ratio); return { points: [...sampledClusters, ...anomalyPoints], sampled: true, }; } export default function ScatterPlot({ clusters, anomalies }) { const [hiddenClusters, setHiddenClusters] = useState(new Set()); const safeClusters = useMemo(() => Array.isArray(clusters) ? clusters : [], [clusters]); const safeAnomalies = useMemo(() => Array.isArray(anomalies) ? anomalies : [], [anomalies]); function toggleCluster(id) { setHiddenClusters(prev => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); } const clusterPoints = useMemo(() => safeClusters .filter(c => !hiddenClusters.has(c.id)) .flatMap(c => (c.points ?? []).map(p => ({ ...p, clusterId: c.id, clusterLabel: c.label, color: CLUSTER_COLORS[c.id % CLUSTER_COLORS.length], isAnomaly: false, })) ), [safeClusters, hiddenClusters]); const anomalyPoints = useMemo(() => safeAnomalies.map(p => ({ ...p, clusterId: -1, clusterLabel: 'Anomaly', color: ANOMALY_COLOR, isAnomaly: true, })), [safeAnomalies]); const { points: allPoints, sampled } = useMemo( () => stratifiedSample(clusterPoints, anomalyPoints, MAX_POINTS), [clusterPoints, anomalyPoints] ); const totalPoints = clusterPoints.length + anomalyPoints.length; const CustomTooltip = useCallback(({ active, payload }) => { if (!active || !payload?.length) return null; const d = payload[0].payload; return (
{d.isAnomaly ? '⚠ ANOMALY' : d.clusterLabel}
{d.raw ?
{d.raw}
:
{d.error_type} · {d.module}
}
); }, []); return (
CLUSTER MAP — UMAP 2D PROJECTION {sampled && ( showing {allPoints.length} of {totalPoints} points )}
{safeClusters.map(c => (
toggleCluster(c.id)} style={{ cursor: 'pointer', opacity: hiddenClusters.has(c.id) ? 0.3 : 1, transition: 'opacity 0.2s', }} >
{c.label}
))} {safeAnomalies.length > 0 && (
Anomalies
)}
} /> {allPoints.map((point, i) => ( ))}
); }