feat: popup liste des villes + balance au clic sur un cluster
ci/woodpecker/push/woodpecker Pipeline was successful
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user