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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [corridors, cityNodes, focusCity, tick, mapReady]);
|
}, [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) => {
|
const handleNodeClick = (nodeIdx: number) => {
|
||||||
if (!svgElements) return;
|
if (!svgElements) return;
|
||||||
const node = svgElements.nodeElems[nodeIdx];
|
const node = svgElements.nodeElems[nodeIdx];
|
||||||
const firstCity = [...node.cl.cities][0];
|
const firstCity = [...node.cl.cities][0];
|
||||||
const isCurrentFocus = node.cl.cities.has(focusCity ?? '');
|
const isCurrentFocus = node.cl.cities.has(focusCity ?? '');
|
||||||
onCityClick(isCurrentFocus ? null : firstCity);
|
onCityClick(isCurrentFocus ? null : firstCity);
|
||||||
|
setPopupIdx(popupIdx === nodeIdx ? null : nodeIdx);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -338,6 +344,49 @@ export function FlowMap({ arcs, focusCity, onCityClick }: FlowMapProps) {
|
|||||||
</g>
|
</g>
|
||||||
</svg>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user