Spaces:
Sleeping
Sleeping
Download frontend/src/components/ScatterPlot.js from Ilaa-1505/Error-Clustering-System: direct link, hf CLI and curl.
- Browser
- Download file 6.72 kB
-
https://huggingface.co/spaces/Ilaa-1505/Error-Clustering-System/resolve/main/frontend/src/components/ScatterPlot.js
- Command line
-
hf download hf://spaces/Ilaa-1505/Error-Clustering-System/frontend/src/components/ScatterPlot.js
-
curl -L -o ScatterPlot.js https://huggingface.co/spaces/Ilaa-1505/Error-Clustering-System/resolve/main/frontend/src/components/ScatterPlot.js
6.72 kB
| 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 ( | |
| <div className="tooltip-box"> | |
| <div style={{ color: d.color, marginBottom: 4, letterSpacing: '0.1em' }}> | |
| {d.isAnomaly ? '⚠ ANOMALY' : d.clusterLabel} | |
| </div> | |
| {d.raw | |
| ? <div style={{ color: 'var(--text-mid)' }}>{d.raw}</div> | |
| : <div style={{ color: 'var(--text-dim)' }}>{d.error_type} · {d.module}</div> | |
| } | |
| </div> | |
| ); | |
| }, []); | |
| return ( | |
| <div className="scatter-wrapper fade-in"> | |
| <div className="scatter-header"> | |
| <span className="scatter-title">CLUSTER MAP — UMAP 2D PROJECTION</span> | |
| {sampled && ( | |
| <span style={{ | |
| fontSize: 10, | |
| fontFamily: 'var(--font-mono)', | |
| color: 'var(--text-dim)', | |
| marginLeft: 12, | |
| }}> | |
| showing {allPoints.length} of {totalPoints} points | |
| </span> | |
| )} | |
| <div className="scatter-legend"> | |
| {safeClusters.map(c => ( | |
| <div | |
| key={c.id} | |
| className="legend-item" | |
| onClick={() => toggleCluster(c.id)} | |
| style={{ | |
| cursor: 'pointer', | |
| opacity: hiddenClusters.has(c.id) ? 0.3 : 1, | |
| transition: 'opacity 0.2s', | |
| }} | |
| > | |
| <div | |
| className="legend-dot" | |
| style={{ background: CLUSTER_COLORS[c.id % CLUSTER_COLORS.length] }} | |
| /> | |
| {c.label} | |
| </div> | |
| ))} | |
| {safeAnomalies.length > 0 && ( | |
| <div className="legend-item"> | |
| <div className="legend-dot" style={{ background: ANOMALY_COLOR }} /> | |
| Anomalies | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| <ResponsiveContainer width="100%" height={400}> | |
| <ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}> | |
| <XAxis | |
| dataKey="x" | |
| type="number" | |
| domain={['auto', 'auto']} | |
| tick={{ fill: 'var(--text-dim)', fontSize: 10 }} | |
| axisLine={{ stroke: 'var(--border)' }} | |
| tickLine={false} | |
| label={{ value: 'UMAP-1', position: 'insideBottom', offset: -10, fill: 'var(--text-dim)', fontSize: 9 }} | |
| /> | |
| <YAxis | |
| dataKey="y" | |
| type="number" | |
| domain={['auto', 'auto']} | |
| tick={{ fill: 'var(--text-dim)', fontSize: 10 }} | |
| axisLine={{ stroke: 'var(--border)' }} | |
| tickLine={false} | |
| label={{ value: 'UMAP-2', angle: -90, position: 'insideLeft', fill: 'var(--text-dim)', fontSize: 9 }} | |
| /> | |
| <Tooltip content={<CustomTooltip />} /> | |
| <Scatter | |
| data={allPoints} | |
| isAnimationActive={allPoints.length < 200} | |
| > | |
| {allPoints.map((point, i) => ( | |
| <Cell | |
| key={i} | |
| fill={point.color} | |
| opacity={point.isAnomaly ? 0.5 : 0.85} | |
| r={point.isAnomaly ? 4 : 6} | |
| /> | |
| ))} | |
| </Scatter> | |
| </ScatterChart> | |
| </ResponsiveContainer> | |
| </div> | |
| ); | |
| } |