diff --git a/backend/routes/clustering.py b/backend/routes/clustering.py index b775608..de40c26 100644 --- a/backend/routes/clustering.py +++ b/backend/routes/clustering.py @@ -199,7 +199,7 @@ def _run_clustering_job(k: int, hours: int) -> None: for i, name in enumerate(FEATURE_NAMES) ] - radius = max(12, min(80, int(math.sqrt(ip_count) * 2))) + radius = max(8, min(30, int(math.log1p(ip_count) * 2.2))) sample_rows = sorted(cluster_rows[j], key=lambda r: float(r.get("hits") or 0), reverse=True)[:8] sample_ips = [r["ip"] for r in sample_rows] diff --git a/frontend/src/components/ClusteringView.tsx b/frontend/src/components/ClusteringView.tsx index dd5f8bd..89989a2 100644 --- a/frontend/src/components/ClusteringView.tsx +++ b/frontend/src/components/ClusteringView.tsx @@ -111,8 +111,8 @@ export default function ClusteringView() { // Viewport deck.gl — centré à [WORLD/2, WORLD/2] const [viewState, setViewState] = useState({ target: [WORLD / 2, WORLD / 2, 0] as [number, number, number], - zoom: 0, - minZoom: -4, + zoom: -0.5, // montre légèrement plus que le monde [0,WORLD] + minZoom: -3, maxZoom: 6, }); @@ -138,10 +138,18 @@ 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 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 canvasH = window.innerHeight - 60; setViewState(v => ({ ...v, target: [(minX + maxX) / 2, (minY + maxY) / 2, 0], - zoom: Math.log2(Math.min(800 / (maxX - minX + 1), 600 / (maxY - minY + 1))) - 1, + zoom: Math.min( + Math.log2(canvasW / fitW), + Math.log2(canvasH / fitH), + ), })); } }