- 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>
136 lines
4 KiB
TypeScript
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);
|
|
}
|