Files
g1flux/src/data/arcData.ts
T
syoul 8e396cd331
ci/woodpecker/push/woodpecker Pipeline was successful
feat: nature des échanges — catégorisation et détail des commentaires de transactions
- Nouveau commentParser.ts : ~80 règles regex multilingues, 11 catégories
- SubsquidAdapter : fetch du champ comment.remark depuis SubSquid
- Transaction et TransactionArc : champs comment et category
- StatsPanel : section Nature des échanges avec barres cliquables (détail inline)
- FlowMap : tooltip au survol des arcs avec répartition catégories + commentaires
- InfoPanel mis à jour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 21:29:59 +02:00

138 lines
5.2 KiB
TypeScript

import type { Transaction } from './mockData';
import type { TxCategory } from './commentParser';
import { aggregateCategories } from './commentParser';
export interface TransactionArc {
id: string;
timestamp: number; // Unix ms
amount: number; // Ğ1
fromLat: number;
fromLng: number;
fromCity: string;
fromCountry: string;
fromKey: string;
toLat: number;
toLng: number;
toCity: string;
toCountry: string;
toKey: string;
comment: string | null;
category: TxCategory;
}
/** Corridor agrégé par paire de villes (fromCity → toCity). */
export interface Corridor {
fromCity: string;
fromLat: number;
fromLng: number;
fromCountry: string;
toCity: string;
toLat: number;
toLng: number;
toCountry: string;
totalVolume: number;
count: number;
categories: { category: TxCategory; count: number; volume: number }[];
comments: string[]; // échantillon de commentaires bruts (max 5, non nuls)
}
export interface FlowStats {
totalVolume: number;
arcCount: number;
topEmitters: { city: string; volume: number; count: number; countryCode: string }[];
topReceivers: { city: string; volume: number; count: number; countryCode: string }[];
netBalance: { city: string; net: number; countryCode: string }[];
}
/** Agrège les arcs individuels en corridors ville→ville, triés par volume. */
export function buildCorridors(arcs: TransactionArc[]): Corridor[] {
const map = new Map<string, { corridor: Omit<Corridor, 'categories' | 'comments'>; items: TransactionArc[] }>();
for (const arc of arcs) {
const key = `${arc.fromCity}||${arc.toCity}`;
if (!map.has(key)) {
map.set(key, {
corridor: {
fromCity: arc.fromCity, fromLat: arc.fromLat, fromLng: arc.fromLng, fromCountry: arc.fromCountry,
toCity: arc.toCity, toLat: arc.toLat, toLng: arc.toLng, toCountry: arc.toCountry,
totalVolume: 0, count: 0,
},
items: [],
});
}
const entry = map.get(key)!;
entry.corridor.totalVolume += arc.amount;
entry.corridor.count++;
entry.items.push(arc);
}
return [...map.values()].map(({ corridor, items }) => ({
...corridor,
categories: aggregateCategories(items.map((a) => ({ category: a.category, amount: a.amount }))),
comments: items.map((a) => a.comment).filter((c): c is string => !!c).slice(0, 5),
})).sort((a, b) => b.totalVolume - a.totalVolume);
}
export function computeFlowStats(arcs: TransactionArc[]): FlowStats {
const emitters = new Map<string, { volume: number; count: number; country: string }>();
const receivers = new Map<string, { volume: number; count: number; country: string }>();
for (const arc of arcs) {
if (!emitters.has(arc.fromCity)) emitters.set(arc.fromCity, { volume: 0, count: 0, country: arc.fromCountry });
if (!receivers.has(arc.toCity)) receivers.set(arc.toCity, { volume: 0, count: 0, country: arc.toCountry });
emitters.get(arc.fromCity)!.volume += arc.amount;
emitters.get(arc.fromCity)!.count++;
receivers.get(arc.toCity)!.volume += arc.amount;
receivers.get(arc.toCity)!.count++;
}
const allCities = new Set([...emitters.keys(), ...receivers.keys()]);
const netBalance = [...allCities].map(city => ({
city,
net: (receivers.get(city)?.volume ?? 0) - (emitters.get(city)?.volume ?? 0),
countryCode: emitters.get(city)?.country ?? receivers.get(city)?.country ?? '',
})).sort((a, b) => Math.abs(b.net) - Math.abs(a.net)).slice(0, 5);
return {
totalVolume: arcs.reduce((s, a) => s + a.amount, 0),
arcCount: arcs.length,
topEmitters: [...emitters.entries()].sort((a, b) => b[1].volume - a[1].volume).slice(0, 3)
.map(([city, d]) => ({ city, volume: d.volume, count: d.count, countryCode: d.country })),
topReceivers: [...receivers.entries()].sort((a, b) => b[1].volume - a[1].volume).slice(0, 3)
.map(([city, d]) => ({ city, volume: d.volume, count: d.count, countryCode: d.country })),
netBalance,
};
}
/**
* Génère des arcs mock : chaque transaction devient un arc dont le destinataire
* est une transaction aléatoire d'une ville différente.
*/
export function buildMockArcs(transactions: Transaction[]): TransactionArc[] {
if (transactions.length < 2) return [];
const arcs: TransactionArc[] = [];
for (let i = 0; i < transactions.length; i++) {
if (Math.random() > 0.55) continue; // ~55 % de couverture
const from = transactions[i];
let toIdx = Math.floor(Math.random() * transactions.length);
for (let tries = 0; tries < 8 && transactions[toIdx].city === from.city; tries++) {
toIdx = Math.floor(Math.random() * transactions.length);
}
const to = transactions[toIdx];
if (to.city === from.city) continue;
arcs.push({
id: `${from.id}-arc`,
timestamp: from.timestamp,
amount: from.amount,
fromLat: from.lat, fromLng: from.lng,
fromCity: from.city, fromCountry: from.countryCode,
fromKey: from.fromKey,
toLat: to.lat, toLng: to.lng,
toCity: to.city, toCountry: to.countryCode,
toKey: to.toKey,
comment: from.comment,
category: from.category,
});
}
return arcs;
}