irpf/src/lib/eurostat.ts
Marc Reverdin a35c43661b feat(irpf): couche data open source — Eurostat + INSEE build-time
- eurostat.ts : client JSON-stat 2.0, fetch multi-pays, parser flat index
- insee.ts : client BDM SDMX-JSON (prêt pour clé API)
- indicators-config.ts : 32 indicateurs sur 14 thèmes (finances, emploi,
  santé, éducation, cohésion, logement, énergie, numérique, agriculture)
  avec mapping Eurostat dataset + filtres validés
- fetch-indicators.ts : script pré-build → src/data/live/indicators.json
- fetch-indicators integration : Astro hook astro:build:start (cache 6h)
- KpiLive.astro : composant avec badge live, comparaison UE, code couleur
- donnees/index.astro : tableau de bord France avec 7 groupes + tableau
  comparatif France-Allemagne-Espagne-Italie-Suède-Pays-Bas-UE27
- package.json : scripts fetch-data + fetch-and-build

18 indicateurs Eurostat avec valeur FR correcte (Eurostat) au premier run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 00:06:05 +02:00

136 lines
4 KiB
TypeScript

/**
* 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<string, EurostatValue>; // { FR: {...}, DE: {...}, ... }
interface JSONStat {
id: string[];
size: number[];
dimension: Record<string, {
label: string;
category: {
index: Record<string, number>;
label: Record<string, string>;
};
}>;
value: Record<string, number> | 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, number>)[String(idx)];
const getValue = Array.isArray(data.value)
? (idx: number) => (data.value as number[])[idx]
: (idx: number) => (data.value as Record<string, number>)[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<string, string | string[]>,
geos: string[] = ['FR', 'DE', 'ES', 'IT', 'SE', 'NL', 'EU27_2020'],
): Promise<GeoValues> {
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<string, string>,
pays: string[],
): Promise<GeoValues> {
return fetchIndicateur(dataset, filters, pays);
}