feat: taux de géoloc réel par frame + DU + périodeSélecteur + autoplay anim
- Affiche l'équivalent en DU pour le volume total et la moyenne par tx - Taux de géolocalisation réel par frame d'animation (via allTimestamps) - Sélecteur de période personnalisée inline à côté des boutons 24h/7j/30j - Clic sur Animer lance la lecture automatique à vitesse ×1 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+6
-3
@@ -18,8 +18,9 @@ export default function App() {
|
|||||||
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
|
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
|
||||||
const [source, setSource] = useState<'live' | 'mock'>('mock');
|
const [source, setSource] = useState<'live' | 'mock'>('mock');
|
||||||
const [currentUD, setCurrentUD] = useState<number>(11.78);
|
const [currentUD, setCurrentUD] = useState<number>(11.78);
|
||||||
|
const [allTimestamps, setAllTimestamps] = useState<number[]>([]);
|
||||||
|
|
||||||
const animation = useAnimation(transactions, periodDays);
|
const animation = useAnimation(transactions, periodDays, allTimestamps);
|
||||||
|
|
||||||
const handlePeriodChange = (days: number) => {
|
const handlePeriodChange = (days: number) => {
|
||||||
animation.deactivate();
|
animation.deactivate();
|
||||||
@@ -33,12 +34,13 @@ export default function App() {
|
|||||||
if (showLoading) setLoading(true);
|
if (showLoading) setLoading(true);
|
||||||
else setRefreshing(true);
|
else setRefreshing(true);
|
||||||
fetchData(periodDays)
|
fetchData(periodDays)
|
||||||
.then(({ transactions, stats, source, currentUD }) => {
|
.then(({ transactions, stats, source, currentUD, allTimestamps }) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setTransactions(transactions);
|
setTransactions(transactions);
|
||||||
setStats(stats);
|
setStats(stats);
|
||||||
setSource(source);
|
setSource(source);
|
||||||
setCurrentUD(currentUD);
|
setCurrentUD(currentUD);
|
||||||
|
setAllTimestamps(allTimestamps);
|
||||||
setLastUpdate(new Date());
|
setLastUpdate(new Date());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -59,6 +61,8 @@ export default function App() {
|
|||||||
? {
|
? {
|
||||||
...computeStats(animation.visibleTransactions),
|
...computeStats(animation.visibleTransactions),
|
||||||
geoCount: animation.visibleTransactions.length,
|
geoCount: animation.visibleTransactions.length,
|
||||||
|
// frameTotalCount = total réel (géo + non-géo) dans cette frame
|
||||||
|
transactionCount: animation.frameTotalCount ?? animation.visibleTransactions.length,
|
||||||
}
|
}
|
||||||
: stats;
|
: stats;
|
||||||
|
|
||||||
@@ -72,7 +76,6 @@ export default function App() {
|
|||||||
source={source}
|
source={source}
|
||||||
currentUD={currentUD}
|
currentUD={currentUD}
|
||||||
animationLabel={animation.active ? (animation.currentFrame?.label ?? undefined) : undefined}
|
animationLabel={animation.active ? (animation.currentFrame?.label ?? undefined) : undefined}
|
||||||
globalGeoStats={animation.active && stats ? { geoCount: stats.geoCount, transactionCount: stats.transactionCount } : undefined}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Map area */}
|
{/* Map area */}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ interface StatsPanelProps {
|
|||||||
source: 'live' | 'mock';
|
source: 'live' | 'mock';
|
||||||
currentUD: number;
|
currentUD: number;
|
||||||
animationLabel?: string;
|
animationLabel?: string;
|
||||||
// Stats de géoloc de la période complète (indépendantes de la frame courante)
|
|
||||||
globalGeoStats?: { geoCount: number; transactionCount: number };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MEDALS = ['🥇', '🥈', '🥉'];
|
const MEDALS = ['🥇', '🥈', '🥉'];
|
||||||
@@ -35,7 +33,7 @@ function formatDU(g1: number, ud: number): string {
|
|||||||
return `≈ ${Math.round(du).toLocaleString('fr-FR')} DU`;
|
return `≈ ${Math.round(du).toLocaleString('fr-FR')} DU`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatsPanel({ stats, loading, periodDays, source, currentUD, animationLabel, globalGeoStats }: StatsPanelProps) {
|
export function StatsPanel({ stats, loading, periodDays, source, currentUD, animationLabel }: StatsPanelProps) {
|
||||||
const periodLabel = periodDays === 1 ? '24 dernières heures' : `${periodDays} derniers jours`;
|
const periodLabel = periodDays === 1 ? '24 dernières heures' : `${periodDays} derniers jours`;
|
||||||
const prevStats = useRef<PeriodStats | null>(null);
|
const prevStats = useRef<PeriodStats | null>(null);
|
||||||
|
|
||||||
@@ -108,20 +106,14 @@ export function StatsPanel({ stats, loading, periodDays, source, currentUD, anim
|
|||||||
})()}
|
})()}
|
||||||
delta={prevTxCount !== null ? (stats.transactionCount > prevTxCount ? 'up' : stats.transactionCount < prevTxCount ? 'down' : null) : null}
|
delta={prevTxCount !== null ? (stats.transactionCount > prevTxCount ? 'up' : stats.transactionCount < prevTxCount ? 'down' : null) : null}
|
||||||
/>
|
/>
|
||||||
{/* Couverture géo — toujours basée sur la période complète (pas la frame) */}
|
{/* Couverture géo — transactionCount inclut le total réel de la frame */}
|
||||||
{source === 'live' && (() => {
|
{source === 'live' && stats.transactionCount > 0 && (() => {
|
||||||
const geo = globalGeoStats ?? { geoCount: stats.geoCount, transactionCount: stats.transactionCount };
|
const pct = Math.round((stats.geoCount / stats.transactionCount) * 100);
|
||||||
const pct = geo.transactionCount > 0
|
|
||||||
? Math.round((geo.geoCount / geo.transactionCount) * 100)
|
|
||||||
: null;
|
|
||||||
if (pct === null) return null;
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#0f1016] border border-[#2e2f3a] rounded-xl p-3">
|
<div className="bg-[#0f1016] border border-[#2e2f3a] rounded-xl p-3">
|
||||||
<div className="flex justify-between items-center mb-1.5">
|
<div className="flex justify-between items-center mb-1.5">
|
||||||
<p className="text-[#4b5563] text-xs uppercase tracking-widest">
|
<p className="text-[#4b5563] text-xs uppercase tracking-widest">Géolocalisées</p>
|
||||||
Géolocalisées{animationLabel ? <span className="normal-case ml-1 opacity-60">(période)</span> : ''}
|
<p className="text-[#6b7280] text-xs">{stats.geoCount} / {stats.transactionCount}</p>
|
||||||
</p>
|
|
||||||
<p className="text-[#6b7280] text-xs">{geo.geoCount} / {geo.transactionCount}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-[#1e1f2a] rounded-full h-1.5">
|
<div className="w-full bg-[#1e1f2a] rounded-full h-1.5">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ function buildFrames(periodDays: number): TimeFrame[] {
|
|||||||
return frames;
|
return frames;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAnimation(transactions: Transaction[], periodDays: number) {
|
export function useAnimation(transactions: Transaction[], periodDays: number, allTimestamps: number[] = []) {
|
||||||
const [active, setActive] = useState(false);
|
const [active, setActive] = useState(false);
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const [playing, setPlaying] = useState(false);
|
const [playing, setPlaying] = useState(false);
|
||||||
@@ -95,6 +95,14 @@ export function useAnimation(transactions: Transaction[], periodDays: number) {
|
|||||||
return transactions.filter((t) => t.timestamp >= frame.from && t.timestamp < frame.to);
|
return transactions.filter((t) => t.timestamp >= frame.from && t.timestamp < frame.to);
|
||||||
}, [active, transactions, frames, currentIndex]);
|
}, [active, transactions, frames, currentIndex]);
|
||||||
|
|
||||||
|
// Nombre total de transfers (géo + non-géo) dans la frame courante
|
||||||
|
const frameTotalCount = useMemo(() => {
|
||||||
|
if (!active || frames.length === 0 || allTimestamps.length === 0) return null;
|
||||||
|
const frame = frames[currentIndex];
|
||||||
|
if (!frame) return null;
|
||||||
|
return allTimestamps.filter((ts) => ts >= frame.from && ts < frame.to).length;
|
||||||
|
}, [active, allTimestamps, frames, currentIndex]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
active,
|
active,
|
||||||
activate: () => { setActive(true); setSpeed(1); setPlaying(true); },
|
activate: () => { setActive(true); setSpeed(1); setPlaying(true); },
|
||||||
@@ -109,5 +117,6 @@ export function useAnimation(transactions: Transaction[], periodDays: number) {
|
|||||||
frames,
|
frames,
|
||||||
currentFrame: frames[currentIndex] ?? null,
|
currentFrame: frames[currentIndex] ?? null,
|
||||||
visibleTransactions,
|
visibleTransactions,
|
||||||
|
frameTotalCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,11 +46,12 @@ async function fetchLiveTransactions(periodDays: number): Promise<{
|
|||||||
geolocated: Transaction[];
|
geolocated: Transaction[];
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
totalVolume: number;
|
totalVolume: number;
|
||||||
|
allTimestamps: number[];
|
||||||
}> {
|
}> {
|
||||||
// ~400 tx/jour sur le réseau Ğ1v2 → marge ×1.5 arrondie, minimum 2000
|
// ~400 tx/jour sur le réseau Ğ1v2 → marge ×1.5 arrondie, minimum 2000
|
||||||
const limit = Math.max(2000, Math.ceil(periodDays * 600));
|
const limit = Math.max(2000, Math.ceil(periodDays * 600));
|
||||||
const { transfers: rawTransfers, totalCount } = await fetchTransfers(periodDays, limit);
|
const { transfers: rawTransfers, totalCount } = await fetchTransfers(periodDays, limit);
|
||||||
if (rawTransfers.length === 0) return { geolocated: [], totalCount: 0, totalVolume: 0 };
|
if (rawTransfers.length === 0) return { geolocated: [], totalCount: 0, totalVolume: 0, allTimestamps: [] };
|
||||||
|
|
||||||
const totalVolume = rawTransfers.reduce((s, t) => s + t.amount, 0);
|
const totalVolume = rawTransfers.reduce((s, t) => s + t.amount, 0);
|
||||||
|
|
||||||
@@ -99,7 +100,7 @@ async function fetchLiveTransactions(periodDays: number): Promise<{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { geolocated, totalCount, totalVolume };
|
return { geolocated, totalCount, totalVolume, allTimestamps: rawTransfers.map((t) => t.timestamp) };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -113,10 +114,11 @@ export interface PeriodStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DataResult {
|
export interface DataResult {
|
||||||
transactions: Transaction[]; // uniquement géolocalisées → heatmap
|
transactions: Transaction[]; // uniquement géolocalisées → heatmap
|
||||||
stats: PeriodStats;
|
stats: PeriodStats;
|
||||||
source: 'live' | 'mock';
|
source: 'live' | 'mock';
|
||||||
currentUD: number; // valeur du DU courant en Ğ1
|
currentUD: number; // valeur du DU courant en Ğ1
|
||||||
|
allTimestamps: number[]; // timestamps de TOUS les transfers (géo + non-géo)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchData(periodDays: number): Promise<DataResult> {
|
export async function fetchData(periodDays: number): Promise<DataResult> {
|
||||||
@@ -129,10 +131,11 @@ export async function fetchData(periodDays: number): Promise<DataResult> {
|
|||||||
stats: { ...base, geoCount: transactions.length },
|
stats: { ...base, geoCount: transactions.length },
|
||||||
source: 'mock',
|
source: 'mock',
|
||||||
currentUD: 11.78,
|
currentUD: 11.78,
|
||||||
|
allTimestamps: transactions.map((t) => t.timestamp),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const [{ geolocated, totalCount, totalVolume }, currentUD] = await Promise.all([
|
const [{ geolocated, totalCount, totalVolume, allTimestamps }, currentUD] = await Promise.all([
|
||||||
fetchLiveTransactions(periodDays),
|
fetchLiveTransactions(periodDays),
|
||||||
getCurrentUD(),
|
getCurrentUD(),
|
||||||
]);
|
]);
|
||||||
@@ -148,5 +151,6 @@ export async function fetchData(periodDays: number): Promise<DataResult> {
|
|||||||
},
|
},
|
||||||
source: 'live',
|
source: 'live',
|
||||||
currentUD,
|
currentUD,
|
||||||
|
allTimestamps,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user