/** * Client Eurostat — API JSON-stat 2.0 * CORS ouvert, pas d'authentification requise. * Doc: https://wikis.ec.europa.eu/display/EUROSTATHELP/API+Statistics+-+data+query */ const BASE = 'https://ec.europa.eu/eurostat/api/dissemination/statistics/1.0/data'; export interface EurostatValue { value: number | null; year: string; unit?: string; } export type GeoValues = Record; // { FR: {...}, DE: {...}, ... } interface JSONStat { id: string[]; size: number[]; dimension: Record; label: Record; }; }>; value: Record | number[]; } /** * Parse une réponse JSON-stat et extrait la dernière valeur pour chaque pays. * Supporte les requêtes multi-géographies. */ function parseJSONStat(data: JSONStat, geos: string[]): GeoValues { const result: GeoValues = {}; const timeIdx = data.id.indexOf('time'); const geoIdx = data.id.indexOf('geo'); if (timeIdx === -1 || geoIdx === -1) return result; const timeCat = data.dimension['time']?.category; const geoCat = data.dimension['geo']?.category; if (!timeCat || !geoCat) return result; // Trouver l'année la plus récente avec une valeur const timeEntries = Object.entries(timeCat.index).sort((a, b) => b[1] - a[1]); // décroissant // Calculer les strides pour l'indexation flat const strides: number[] = new Array(data.id.length).fill(1); for (let i = data.id.length - 2; i >= 0; i--) { strides[i] = strides[i + 1] * data.size[i + 1]; } const values = Array.isArray(data.value) ? data.value : (idx: number) => (data.value as Record)[String(idx)]; const getValue = Array.isArray(data.value) ? (idx: number) => (data.value as number[])[idx] : (idx: number) => (data.value as Record)[String(idx)]; for (const geo of geos) { const geoPos = geoCat.index[geo]; if (geoPos === undefined) continue; // Chercher la dernière année non-nulle let found = false; for (const [year, timePos] of timeEntries) { // Calcul de l'index flat : somme de (position * stride) pour chaque dim // Les autres dims ont taille 1 donc position 0 const flatIdx = geoPos * strides[geoIdx] + timePos * strides[timeIdx]; const val = getValue(flatIdx); if (val !== null && val !== undefined && !isNaN(val)) { result[geo] = { value: val, year }; found = true; break; } } if (!found) result[geo] = { value: null, year: '' }; } return result; } /** * Fetch un indicateur Eurostat pour plusieurs pays. * Retourne les dernières valeurs disponibles par pays. */ export async function fetchIndicateur( dataset: string, filters: Record, geos: string[] = ['FR', 'DE', 'ES', 'IT', 'SE', 'NL', 'EU27_2020'], ): Promise { try { const url = new URL(`${BASE}/${dataset}`); url.searchParams.set('lang', 'en'); url.searchParams.set('format', 'JSON'); for (const geo of geos) url.searchParams.append('geo', geo); for (const [key, val] of Object.entries(filters)) { if (Array.isArray(val)) { for (const v of val) url.searchParams.append(key, v); } else { url.searchParams.set(key, val); } } const res = await fetch(url.toString(), { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(15_000), }); if (!res.ok) { console.warn(`[eurostat] ${dataset} → HTTP ${res.status}`); return {}; } const data: JSONStat = await res.json(); return parseJSONStat(data, geos); } catch (e) { console.warn(`[eurostat] ${dataset} → ${(e as Error).message}`); return {}; } } /** * Fetch la même série pour N pays — alias plus lisible. */ export async function fetchMultiPays( dataset: string, filters: Record, pays: string[], ): Promise { return fetchIndicateur(dataset, filters, pays); }