feat: vue flux — arcs dirigés entre villes géolocalisées
ci/woodpecker/push/woodpecker Pipeline was successful
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:
+39
-6
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user