feat: initialisation de ĞéoFlux — visualisation géographique Ğ1
- Carte Leaflet plein écran avec heatmap (OpenStreetMap, dark mode) - Sélecteur de période 24h / 7j / 30j - Panneau latéral : volume total, compteur de transactions, top 3 villes - mockData.ts : 2 400 transactions simulées sur 24 villes FR/EU - DataService.ts : abstraction prête pour branchement Subsquid/Ğ1v2 - Schémas Zod (g1.schema.ts) : validation runtime Duniter GVA + Cesium+ - Adaptateurs DuniterAdapter et CesiumAdapter (Ğ1v1, à migrer v2) - Suite de tests Vitest : 43 tests, conformité schéma Ğ1 vérifiée Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
114
src/data/mockData.ts
Normal file
114
src/data/mockData.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
export interface Transaction {
|
||||
id: string;
|
||||
timestamp: number; // Unix ms
|
||||
lat: number;
|
||||
lng: number;
|
||||
amount: number; // Ğ1
|
||||
city: string;
|
||||
fromKey: string;
|
||||
toKey: string;
|
||||
}
|
||||
|
||||
// French + European cities where Ğ1 is used
|
||||
const CITIES: { name: string; lat: number; lng: number; weight: number }[] = [
|
||||
{ name: 'Paris', lat: 48.8566, lng: 2.3522, weight: 12 },
|
||||
{ name: 'Lyon', lat: 45.7640, lng: 4.8357, weight: 9 },
|
||||
{ name: 'Bordeaux', lat: 44.8378, lng: -0.5792, weight: 8 },
|
||||
{ name: 'Toulouse', lat: 43.6047, lng: 1.4442, weight: 8 },
|
||||
{ name: 'Montpellier', lat: 43.6108, lng: 3.8767, weight: 7 },
|
||||
{ name: 'Nantes', lat: 47.2184, lng: -1.5536, weight: 6 },
|
||||
{ name: 'Rennes', lat: 48.1173, lng: -1.6778, weight: 6 },
|
||||
{ name: 'Grenoble', lat: 45.1885, lng: 5.7245, weight: 5 },
|
||||
{ name: 'Marseille', lat: 43.2965, lng: 5.3698, weight: 7 },
|
||||
{ name: 'Strasbourg', lat: 48.5734, lng: 7.7521, weight: 4 },
|
||||
{ name: 'Lille', lat: 50.6292, lng: 3.0573, weight: 4 },
|
||||
{ name: 'Rouen', lat: 49.4432, lng: 1.0993, weight: 3 },
|
||||
{ name: 'Clermont-Ferrand', lat: 45.7772, lng: 3.0870, weight: 4 },
|
||||
{ name: 'Tours', lat: 47.3941, lng: 0.6848, weight: 3 },
|
||||
{ name: 'Poitiers', lat: 46.5802, lng: 0.3404, weight: 3 },
|
||||
{ name: 'Besançon', lat: 47.2378, lng: 6.0241, weight: 3 },
|
||||
{ name: 'Caen', lat: 49.1829, lng: -0.3707, weight: 2 },
|
||||
{ name: 'Nice', lat: 43.7102, lng: 7.2620, weight: 4 },
|
||||
{ name: 'Barcelone', lat: 41.3851, lng: 2.1734, weight: 3 },
|
||||
{ name: 'Bruxelles', lat: 50.8503, lng: 4.3517, weight: 3 },
|
||||
{ name: 'Genève', lat: 46.2044, lng: 6.1432, weight: 2 },
|
||||
{ name: 'Saint-Étienne', lat: 45.4397, lng: 4.3872, weight: 3 },
|
||||
{ name: 'Dijon', lat: 47.3220, lng: 5.0415, weight: 3 },
|
||||
{ name: 'Angers', lat: 47.4784, lng: -0.5632, weight: 2 },
|
||||
];
|
||||
|
||||
function randomBetween(min: number, max: number): number {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
function weightedRandom<T extends { weight: number }>(items: T[]): T {
|
||||
const totalWeight = items.reduce((sum, item) => sum + item.weight, 0);
|
||||
let rand = Math.random() * totalWeight;
|
||||
for (const item of items) {
|
||||
rand -= item.weight;
|
||||
if (rand <= 0) return item;
|
||||
}
|
||||
return items[items.length - 1];
|
||||
}
|
||||
|
||||
function generateKey(): string {
|
||||
const chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||
return Array.from({ length: 44 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
|
||||
}
|
||||
|
||||
function generateTransactions(count: number, maxAgeMs: number): Transaction[] {
|
||||
const now = Date.now();
|
||||
const transactions: Transaction[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const city = weightedRandom(CITIES);
|
||||
// Add slight spatial noise around city center
|
||||
const lat = city.lat + randomBetween(-0.08, 0.08);
|
||||
const lng = city.lng + randomBetween(-0.12, 0.12);
|
||||
const amount = Math.round(randomBetween(0.5, 150) * 100) / 100;
|
||||
|
||||
transactions.push({
|
||||
id: `tx-${i}-${Math.random().toString(36).slice(2)}`,
|
||||
timestamp: Math.floor(now - Math.random() * maxAgeMs),
|
||||
lat,
|
||||
lng,
|
||||
amount,
|
||||
city: city.name,
|
||||
fromKey: generateKey(),
|
||||
toKey: generateKey(),
|
||||
});
|
||||
}
|
||||
|
||||
return transactions.sort((a, b) => b.timestamp - a.timestamp);
|
||||
}
|
||||
|
||||
// Pre-generate a pool of 30 days worth of transactions
|
||||
const TRANSACTION_POOL = generateTransactions(2400, 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
export function getTransactionsForPeriod(periodDays: number): Transaction[] {
|
||||
const cutoff = Date.now() - periodDays * 24 * 60 * 60 * 1000;
|
||||
return TRANSACTION_POOL.filter((tx) => tx.timestamp >= cutoff);
|
||||
}
|
||||
|
||||
export function computeStats(transactions: Transaction[]) {
|
||||
const totalVolume = transactions.reduce((sum, tx) => sum + tx.amount, 0);
|
||||
const transactionCount = transactions.length;
|
||||
|
||||
const cityVolumes: Record<string, { volume: number; count: number }> = {};
|
||||
for (const tx of transactions) {
|
||||
if (!cityVolumes[tx.city]) {
|
||||
cityVolumes[tx.city] = { volume: 0, count: 0 };
|
||||
}
|
||||
cityVolumes[tx.city].volume += tx.amount;
|
||||
cityVolumes[tx.city].count += 1;
|
||||
}
|
||||
|
||||
const topCities = Object.entries(cityVolumes)
|
||||
.sort((a, b) => b[1].volume - a[1].volume)
|
||||
.slice(0, 3)
|
||||
.map(([name, data]) => ({ name, ...data }));
|
||||
|
||||
return { totalVolume, transactionCount, topCities };
|
||||
}
|
||||
|
||||
export type { };
|
||||
Reference in New Issue
Block a user