feat: popup liste des villes + balance au clic sur un cluster
ci/woodpecker/push/woodpecker Pipeline was successful

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 <noreply@anthropic.com>
This commit is contained in:
syoul
2026-03-24 11:36:20 +01:00
parent 839acf8aa8
commit 786bf30a7b
+50 -1
View File
@@ -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<number | null>(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) {
</g>
</svg>
)}
{/* 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 (
<div
className="absolute z-[600] bg-[#0a0b0f]/95 border border-[#2e2f3a] rounded-xl p-3 shadow-xl"
style={{ left, top: Math.max(4, node.cl.cy - 80), width: popupW, pointerEvents: 'auto' }}
>
<div className="flex items-center justify-between mb-2">
<span className="text-[#4b5563] text-[10px] uppercase tracking-widest">
{node.cityCount > 1 ? `${node.cityCount} villes` : 'Ville'}
</span>
<button
onClick={() => setPopupIdx(null)}
className="text-[#4b5563] hover:text-white text-xs leading-none ml-2"
></button>
</div>
<div className="space-y-1 max-h-48 overflow-y-auto">
{cities.map(({ name, net }) => (
<div key={name} className="flex items-center justify-between gap-1">
<span className="text-white text-xs truncate">{name}</span>
<span className={`text-xs font-mono shrink-0 ${net >= 0 ? 'text-[#00acc1]' : 'text-[#ff8f00]'}`}>
{net >= 0 ? '+' : ''}{Math.round(net).toLocaleString('fr-FR')} Ğ1
</span>
</div>
))}
</div>
</div>
);
})()}
</div>
);
}