From 786bf30a7bc37041e39fb998d4db426b3f947ea3 Mon Sep 17 00:00:00 2001 From: syoul Date: Tue, 24 Mar 2026 11:36:20 +0100 Subject: [PATCH] feat: popup liste des villes + balance au clic sur un cluster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Affiche un panneau flottant au clic sur un nœud : liste des villes du cluster triées par |balance|, balance nette colorée (orange/teal). Se ferme sur déplacement/zoom de la carte ou via ✕. Co-Authored-By: Claude Sonnet 4.6 --- src/components/FlowMap.tsx | 51 +++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/components/FlowMap.tsx b/src/components/FlowMap.tsx index ea76337..f43d6ab 100644 --- a/src/components/FlowMap.tsx +++ b/src/components/FlowMap.tsx @@ -260,13 +260,19 @@ export function FlowMap({ arcs, focusCity, onCityClick }: FlowMapProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [corridors, cityNodes, focusCity, tick, mapReady]); - // Handler de clic : on transmet la première ville du cluster cliqué + const [popupIdx, setPopupIdx] = useState(null); + + // Ferme le popup sur déplacement/zoom + useEffect(() => { setPopupIdx(null); }, [tick]); + + // Handler de clic : ouvre/ferme le popup + focus const handleNodeClick = (nodeIdx: number) => { if (!svgElements) return; const node = svgElements.nodeElems[nodeIdx]; const firstCity = [...node.cl.cities][0]; const isCurrentFocus = node.cl.cities.has(focusCity ?? ''); onCityClick(isCurrentFocus ? null : firstCity); + setPopupIdx(popupIdx === nodeIdx ? null : nodeIdx); }; return ( @@ -338,6 +344,49 @@ export function FlowMap({ arcs, focusCity, onCityClick }: FlowMapProps) { )} + + {/* Popup cluster */} + {mapReady && svgElements && popupIdx !== null && (() => { + const node = svgElements.nodeElems[popupIdx]; + const cities = [...node.cl.cities].map(name => { + const d = cityNodes.get(name); + const net = d ? d.received - d.emitted : 0; + return { name, net }; + }).sort((a, b) => Math.abs(b.net) - Math.abs(a.net)); + + // Position : décale à droite du cercle, recadre si hors écran + const raw = node.cl.cx + node.r + 8; + const containerW = containerRef.current?.clientWidth ?? 0; + const popupW = 200; + const left = raw + popupW > containerW ? node.cl.cx - node.r - 8 - popupW : raw; + + return ( +
+
+ + {node.cityCount > 1 ? `${node.cityCount} villes` : 'Ville'} + + +
+
+ {cities.map(({ name, net }) => ( +
+ {name} + = 0 ? 'text-[#00acc1]' : 'text-[#ff8f00]'}`}> + {net >= 0 ? '+' : ''}{Math.round(net).toLocaleString('fr-FR')} Ğ1 + +
+ ))} +
+
+ ); + })()} ); }