From ffe09ea44a2de0ef100b7d660549899673379254 Mon Sep 17 00:00:00 2001 From: syoul Date: Tue, 24 Mar 2026 11:24:03 +0100 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20afficher=20le=20%=20g=C3=A9olocal?= =?UTF-8?q?is=C3=A9=20dans=20la=20barre=20de=20contr=C3=B4les?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute un badge "XX% géo" à droite du bouton Flux/Heatmap dans PeriodSelector, mis à jour à chaque frame d'animation. Co-Authored-By: Claude Sonnet 4.6 --- src/App.tsx | 3 +++ src/components/PeriodSelector.tsx | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index a797b9e..2c8298a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -138,6 +138,9 @@ export default function App() { onAnimate={() => animation.active ? animation.deactivate() : animation.activate()} viewMode={viewMode} onViewModeChange={handleViewModeChange} + geoPercent={visibleStats && visibleStats.transactionCount > 0 + ? Math.round((visibleStats.geoCount / visibleStats.transactionCount) * 100) + : null} /> diff --git a/src/components/PeriodSelector.tsx b/src/components/PeriodSelector.tsx index a5e2e1d..5745223 100644 --- a/src/components/PeriodSelector.tsx +++ b/src/components/PeriodSelector.tsx @@ -7,6 +7,7 @@ interface PeriodSelectorProps { onAnimate: () => void; viewMode: 'heatmap' | 'flow'; onViewModeChange: (mode: 'heatmap' | 'flow') => void; + geoPercent?: number | null; } const PERIODS = [ @@ -17,7 +18,7 @@ const PERIODS = [ const PRESET_DAYS = new Set([1, 7, 30]); -export function PeriodSelector({ value, onChange, animationActive, onAnimate, viewMode, onViewModeChange }: PeriodSelectorProps) { +export function PeriodSelector({ value, onChange, animationActive, onAnimate, viewMode, onViewModeChange, geoPercent }: PeriodSelectorProps) { const [customOpen, setCustomOpen] = useState(false); const [inputVal, setInputVal] = useState(''); const inputRef = useRef(null); @@ -124,6 +125,11 @@ export function PeriodSelector({ value, onChange, animationActive, onAnimate, vi > {viewMode === 'flow' ? '⊙ Heatmap' : '◉ Flux'} + {geoPercent != null && ( + + {geoPercent}% géo + + )} ); } From 839acf8aa80d16eacfc55ae8f10e0edf56aa42a7 Mon Sep 17 00:00:00 2001 From: syoul Date: Tue, 24 Mar 2026 11:26:50 +0100 Subject: [PATCH 02/17] =?UTF-8?q?fix:=20libell=C3=A9=20badge=20g=C3=A9o=20?= =?UTF-8?q?=E2=86=92=20"XX%=20Tx=20g=C3=A9oloc."?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/components/PeriodSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PeriodSelector.tsx b/src/components/PeriodSelector.tsx index 5745223..9f4f53a 100644 --- a/src/components/PeriodSelector.tsx +++ b/src/components/PeriodSelector.tsx @@ -127,7 +127,7 @@ export function PeriodSelector({ value, onChange, animationActive, onAnimate, vi {geoPercent != null && ( - {geoPercent}% géo + {geoPercent}% Tx géoloc. )} From 786bf30a7bc37041e39fb998d4db426b3f947ea3 Mon Sep 17 00:00:00 2001 From: syoul Date: Tue, 24 Mar 2026 11:36:20 +0100 Subject: [PATCH 03/17] 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 + +
+ ))} +
+
+ ); + })()} ); } From 851dc463948faee53b5385b563b8019e10f9a3bf Mon Sep 17 00:00:00 2001 From: syoul Date: Tue, 24 Mar 2026 11:44:23 +0100 Subject: [PATCH 04/17] =?UTF-8?q?fix:=20corriger=20layout=20desktop=20?= =?UTF-8?q?=E2=80=94=20StatsPanel=20w-72=20fixe,=20w-full=20dans=20drawer?= =?UTF-8?q?=20mobile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remplace w-full lg:w-72 h-full (qui cassait les écrans 640-1023px) par un prop className : w-72 shrink-0 par défaut (desktop), w-full flex-1 min-h-0 dans le drawer mobile. Co-Authored-By: Claude Sonnet 4.6 --- src/App.tsx | 2 +- src/components/StatsPanel.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 2c8298a..b274c01 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -213,7 +213,7 @@ export default function App() {
- setPanelOpen(false)} /> + setPanelOpen(false)} className="w-full flex-1 min-h-0" />
)} diff --git a/src/components/StatsPanel.tsx b/src/components/StatsPanel.tsx index ce808fc..37d50fc 100644 --- a/src/components/StatsPanel.tsx +++ b/src/components/StatsPanel.tsx @@ -7,6 +7,7 @@ interface StatsPanelProps { loading: boolean; periodDays: number; source: 'live' | 'mock'; + className?: string; currentUD: number; animationLabel?: string; viewMode?: 'heatmap' | 'flow'; @@ -59,7 +60,7 @@ function CityRow({ city, volume, count, countryCode, accent }: { ); } -export function StatsPanel({ stats, loading, periodDays, source, currentUD, animationLabel, viewMode = 'heatmap', flowStats, focusCity, onClose }: StatsPanelProps) { +export function StatsPanel({ stats, loading, periodDays, source, currentUD, animationLabel, viewMode = 'heatmap', flowStats, focusCity, onClose, className }: StatsPanelProps) { const periodLabel = periodDays === 1 ? '24 dernières heures' : `${periodDays} derniers jours`; const prevStats = useRef(null); @@ -83,7 +84,7 @@ export function StatsPanel({ stats, loading, periodDays, source, currentUD, anim if (stats && !loading) prevStats.current = stats; return ( -