diff --git a/frontend/src/components/ClusteringView.tsx b/frontend/src/components/ClusteringView.tsx index 3b9bebe..fdf2e93 100644 --- a/frontend/src/components/ClusteringView.tsx +++ b/frontend/src/components/ClusteringView.tsx @@ -93,10 +93,21 @@ function hexToRgba(hex: string, alpha = 255): [number, number, number, number] { // ─── Composant principal ────────────────────────────────────────────────────── +// Persistence des paramètres dans localStorage +const LS_KEY = 'soc_clustering_params'; +function loadParams() { + try { + const s = localStorage.getItem(LS_KEY); + if (s) return JSON.parse(s) as { k: number; hours: number; sensitivity: number }; + } catch { /* ignore */ } + return { k: 20, hours: 24, sensitivity: 1.0 }; +} + export default function ClusteringView() { - const [k, setK] = useState(20); - const [hours, setHours] = useState(24); - const [sensitivity, setSensitivity] = useState(1.0); + const init = loadParams(); + const [k, setK] = useState(init.k); + const [hours, setHours] = useState(init.hours); + const [sensitivity, setSensitivity] = useState(init.sensitivity); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [computing, setComputing] = useState(false); @@ -117,9 +128,15 @@ export default function ClusteringView() { maxZoom: 6, }); + // ── Persistence des paramètres ────────────────────────────────────────── + useEffect(() => { + localStorage.setItem(LS_KEY, JSON.stringify({ k, hours, sensitivity })); + }, [k, hours, sensitivity]); + // ── Chargement / polling ───────────────────────────────────────────────── const fetchClusters = useCallback(async (force = false) => { + if (pollRef.current) { clearTimeout(pollRef.current); pollRef.current = null; } setLoading(true); setError(null); try { @@ -128,7 +145,7 @@ export default function ClusteringView() { }); if (res.data.status === 'computing' || res.data.status === 'idle') { setComputing(true); - // Polling + // Polling toutes les 3s pollRef.current = setTimeout(() => fetchClusters(), 3000); } else { setComputing(false); @@ -139,10 +156,10 @@ export default function ClusteringView() { const ys = res.data.nodes.map(n => toWorld(n.pca_y)); const minX = Math.min(...xs), maxX = Math.max(...xs); const minY = Math.min(...ys), maxY = Math.max(...ys); - const pad = 0.18; // 18% de marge de chaque côté + const pad = 0.18; const fitW = (maxX - minX) * (1 + 2 * pad) || WORLD; const fitH = (maxY - minY) * (1 + 2 * pad) || WORLD; - const canvasW = window.innerWidth - 288 - (selected ? 384 : 0); // panel + sidebar + const canvasW = window.innerWidth - 288 - (selected ? 384 : 0); const canvasH = window.innerHeight - 60; setViewState(v => ({ ...v, @@ -160,7 +177,7 @@ export default function ClusteringView() { } finally { setLoading(false); } - }, [k, hours]); + }, [k, hours, sensitivity]); // sensitivity inclus pour éviter la stale closure useEffect(() => { fetchClusters(); @@ -387,9 +404,9 @@ export default function ClusteringView() { Afficher les arêtes @@ -442,12 +459,12 @@ export default function ClusteringView() { {/* ── Canvas WebGL (deck.gl) ── */}
- {/* Animation de calcul — remplace le canvas pendant le traitement */} - {(computing || loading) && ( -
+ {/* Animation de calcul — REMPLACE DeckGL (le canvas WebGL ignore z-index) */} + {(computing || loading) ? ( +
{/* Noeuds pulsants animés */}
- {/* Anneau tournant */} + {/* Anneaux tournants */}
@@ -457,9 +474,8 @@ export default function ClusteringView() { {/* Noeuds orbitaux représentant les clusters */} {([0,1,2,3,4,5,6,7] as const).map((i) => { const angle = (i / 8) * 2 * Math.PI; - const r = 88; - const x = 50 + (r / 1.12) * Math.cos(angle); - const y = 50 + (r / 1.12) * Math.sin(angle); + const x = 50 + 39 * Math.cos(angle); + const y = 50 + 39 * Math.sin(angle); const colors = ['#dc2626','#f97316','#eab308','#22c55e','#3b82f6','#8b5cf6','#ec4899','#14b8a6']; return (

Clustering en cours…

-

K-means++ · 31 features · toutes les IPs

+

+ K-means++ · 31 features · {Math.round(k * sensitivity)} clusters · toutes les IPs +

Mise à jour automatique toutes les 3 secondes

- )} - - {/* Message état vide */} - {!data && !loading && !computing && ( + ) : !data ? ( + /* État vide initial */
🔬 Cliquez sur Recalculer pour démarrer
- )} - - setViewState(vs as typeof viewState)} - layers={layers as any} - style={{ width: '100%', height: '100%' }} - controller={true} - > - {/* Légende overlay */} -
-
- {[['#dc2626', 'CRITICAL'], ['#f97316', 'HIGH'], ['#eab308', 'MEDIUM'], ['#22c55e', 'LOW']].map(([c, l]) => ( -
- - {l} -
- ))} + ) : ( + /* Canvas WebGL — monté seulement quand il y a des données */ + setViewState(vs as typeof viewState)} + layers={layers as any} + style={{ width: '100%', height: '100%' }} + controller={true} + > + {/* Légende overlay */} +
+
+ {[['#dc2626', 'CRITICAL'], ['#f97316', 'HIGH'], ['#eab308', 'MEDIUM'], ['#22c55e', 'LOW']].map(([c, l]) => ( +
+ + {l} +
+ ))} +
-
- {/* Tooltip zoom hint */} -
-
Scroll pour zoomer · Drag pour déplacer · Click sur un cluster
-
- + {/* Tooltip zoom hint */} +
+
Scroll pour zoomer · Drag pour déplacer · Click sur un cluster
+
+ + )}
{/* ── Sidebar droite (sélection) ── */}