feat: vue flux — arcs dirigés entre villes géolocalisées
ci/woodpecker/push/woodpecker Pipeline was successful

- Nouveau type TransactionArc + buildCorridors + computeFlowStats
- FlowMap : SVG overlay Leaflet, arcs bezier, flèches de direction, nœuds de villes cliquables
- Clic sur une ville : arcs sortants orange, entrants teal, reste grisé
- DataService : résolution géo des destinataires (toId) dans le même appel Cesium+
- useAnimation : expose visibleArcs filtré par frame
- PeriodSelector : bouton toggle Heatmap / Flux
- StatsPanel : stats flux (volume, top émetteurs, top récepteurs, balance nette)
- App : state viewMode + focusCity, FlowMap conditionnel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
syoul
2026-03-24 00:21:03 +01:00
parent ab72d8218b
commit 97ff22027c
7 changed files with 647 additions and 112 deletions
+39 -6
View File
@@ -1,17 +1,21 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo } from 'react';
import { StatsPanel } from './components/StatsPanel';
import { PeriodSelector } from './components/PeriodSelector';
import { HeatMap } from './components/HeatMap';
import { FlowMap } from './components/FlowMap';
import { AnimationPlayer } from './components/AnimationPlayer';
import { fetchData } from './services/DataService';
import type { PeriodStats } from './services/DataService';
import type { Transaction } from './data/mockData';
import type { TransactionArc } from './data/arcData';
import { computeStats } from './data/mockData';
import { computeFlowStats } from './data/arcData';
import { useAnimation } from './hooks/useAnimation';
export default function App() {
const [periodDays, setPeriodDays] = useState(7);
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [arcs, setArcs] = useState<TransactionArc[]>([]);
const [stats, setStats] = useState<PeriodStats | null>(null);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
@@ -19,14 +23,21 @@ export default function App() {
const [source, setSource] = useState<'live' | 'mock'>('mock');
const [currentUD, setCurrentUD] = useState<number>(11.78);
const [allTimestamps, setAllTimestamps] = useState<number[]>([]);
const [viewMode, setViewMode] = useState<'heatmap' | 'flow'>('heatmap');
const [focusCity, setFocusCity] = useState<string | null>(null);
const animation = useAnimation(transactions, periodDays, allTimestamps);
const animation = useAnimation(transactions, arcs, periodDays, allTimestamps);
const handlePeriodChange = (days: number) => {
animation.deactivate();
setPeriodDays(days);
};
const handleViewModeChange = (mode: 'heatmap' | 'flow') => {
setViewMode(mode);
setFocusCity(null);
};
useEffect(() => {
let cancelled = false;
@@ -34,9 +45,10 @@ export default function App() {
if (showLoading) setLoading(true);
else setRefreshing(true);
fetchData(periodDays)
.then(({ transactions, stats, source, currentUD, allTimestamps }) => {
.then(({ transactions, arcs, stats, source, currentUD, allTimestamps }) => {
if (!cancelled) {
setTransactions(transactions);
setArcs(arcs);
setStats(stats);
setSource(source);
setCurrentUD(currentUD);
@@ -56,16 +68,24 @@ export default function App() {
return () => { cancelled = true; clearInterval(interval); };
}, [periodDays]);
// Stats calculées sur la fenêtre courante en mode animation
// Stats heatmap sur la fenêtre courante en mode animation
const visibleStats: PeriodStats | null = animation.active
? {
...computeStats(animation.visibleTransactions),
geoCount: animation.visibleTransactions.length,
// frameTotalCount = total réel (géo + non-géo) dans cette frame
transactionCount: animation.frameTotalCount ?? animation.visibleTransactions.length,
}
: stats;
// Stats flux (recalculées sur les arcs visibles)
const flowStats = useMemo(
() => {
const activeArcs = animation.active ? animation.visibleArcs : arcs;
return activeArcs.length > 0 ? computeFlowStats(activeArcs) : null;
},
[arcs, animation.visibleArcs, animation.active],
);
return (
<div className="flex h-svh w-full overflow-hidden bg-[#0a0b0f] text-white">
{/* Side panel */}
@@ -76,11 +96,22 @@ export default function App() {
source={source}
currentUD={currentUD}
animationLabel={animation.active ? (animation.currentFrame?.label ?? undefined) : undefined}
viewMode={viewMode}
flowStats={flowStats}
focusCity={focusCity}
/>
{/* Map area */}
<div className="relative flex-1 min-w-0">
<HeatMap transactions={animation.visibleTransactions} />
{viewMode === 'heatmap' ? (
<HeatMap transactions={animation.visibleTransactions} />
) : (
<FlowMap
arcs={animation.active ? animation.visibleArcs : arcs}
focusCity={focusCity}
onCityClick={setFocusCity}
/>
)}
{/* Period selector — floating over map */}
<div className="absolute top-4 left-1/2 -translate-x-1/2 z-[1000]">
@@ -89,6 +120,8 @@ export default function App() {
onChange={handlePeriodChange}
animationActive={animation.active}
onAnimate={() => animation.active ? animation.deactivate() : animation.activate()}
viewMode={viewMode}
onViewModeChange={handleViewModeChange}
/>
</div>