fix: bulles plus petites + viewport auto-fit avec padding 18%

- Backend: radius = log1p(ip_count)*2.2 au lieu de sqrt*2 (max 30px vs 80px)
  ex: 60K IPs → 24px, 1K IPs → 15px, 100 IPs → 10px
- Frontend: zoom initial -0.5 (vue dézoomée par défaut)
- Fit viewport basé sur dimensions réelles canvas - panneaux latéraux
- Padding 18% autour de l'étendue des données pour éviter le débord

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
SOC Analyst
2026-03-19 09:50:41 +01:00
parent b2c3379aa0
commit 08054cb571
2 changed files with 12 additions and 4 deletions

View File

@ -199,7 +199,7 @@ def _run_clustering_job(k: int, hours: int) -> None:
for i, name in enumerate(FEATURE_NAMES) 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_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] sample_ips = [r["ip"] for r in sample_rows]

View File

@ -111,8 +111,8 @@ export default function ClusteringView() {
// Viewport deck.gl — centré à [WORLD/2, WORLD/2] // Viewport deck.gl — centré à [WORLD/2, WORLD/2]
const [viewState, setViewState] = useState({ const [viewState, setViewState] = useState({
target: [WORLD / 2, WORLD / 2, 0] as [number, number, number], target: [WORLD / 2, WORLD / 2, 0] as [number, number, number],
zoom: 0, zoom: -0.5, // montre légèrement plus que le monde [0,WORLD]
minZoom: -4, minZoom: -3,
maxZoom: 6, maxZoom: 6,
}); });
@ -138,10 +138,18 @@ export default function ClusteringView() {
const ys = res.data.nodes.map(n => toWorld(n.pca_y)); const ys = res.data.nodes.map(n => toWorld(n.pca_y));
const minX = Math.min(...xs), maxX = Math.max(...xs); const minX = Math.min(...xs), maxX = Math.max(...xs);
const minY = Math.min(...ys), maxY = Math.max(...ys); 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 => ({ setViewState(v => ({
...v, ...v,
target: [(minX + maxX) / 2, (minY + maxY) / 2, 0], 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),
),
})); }));
} }
} }