spb/countryatlas
Public
1/**2 * TypeScript mirror of the FastAPI response models in `src/countryatlas/api/schemas.py` (ARCHITECTURE §8).3 * Aligned on 2026-09-11 against that file — when it changes, change this one. The next agent extends it for4 * compare / rankings / indicators / regions / sources pages (the corresponding pydantic models already exist:5 * CompareResponse, RankingResponse, IndicatorResponse, RegionResponse, SourcesResponse … add them here).6 *7 * Conventions: every top-level response carries `meta`. Optional fields are `| null` (the API sends explicit8 * nulls, never omits) except where pydantic gives a default (`is_forecast: false`, lists `[]`). Dates are ISO9 * strings (`period` = first day of the period, `YYYY-MM-DD`). Models are `extra="allow"` server-side, so unknown10 * keys may appear — never rely on them without adding them here.11 */1213export type Frequency = 'A' | 'Q' | 'M';14export type ObservationStatus = 'verified' | 'imported' | 'warning' | 'stale' | 'quarantined';15export type IndicatorFormat =16 | 'number'17 | 'percent'18 | 'currency'19 | 'index'20 | 'years'21 | 'per_1000'22 | 'per_100k'23 | 'per_million'24 | 'ratio'25 | 'celsius'26 | 'tonnes'27 | 'kwh'28 | 'ha'29 | 'km';30export type IncomeGroup = 'HIC' | 'UMC' | 'LMC' | 'LIC';31export type CountryStatus = 'country' | 'territory' | 'historical';32export type SimilarityMode = 'overall' | 'economic' | 'demographic' | 'energy' | 'social';33export type ChangeKind =34 | 'yoy_drop'35 | 'yoy_jump'36 | 'record_high'37 | 'record_low'38 | 'n_year_high'39 | 'n_year_low'40 | 'sign_flip'41 | 'accelerating'42 | 'decelerating';43export type SearchHitType = 'country' | 'indicator' | 'topic' | 'region' | 'source' | 'country_topic' | 'country_indicator';44export type TopicId =45 | 'economy'46 | 'government'47 | 'population'48 | 'labor'49 | 'income'50 | 'housing'51 | 'health'52 | 'education'53 | 'trade'54 | 'energy'55 | 'climate'56 | 'environment'57 | 'infrastructure'58 | 'digital'59 | 'innovation'60 | 'agriculture'61 | 'tourism'62 | 'security'63 | 'quality-of-life';64export type DnaDimension = 'income' | 'demographics' | 'urbanization' | 'trade' | 'energy' | 'emissions' | 'innovation' | 'education' | 'public_spending';6566// ---------------------------------------------------------------------------------------------- envelope & errors6768export interface Meta {69 built_at: string | null;70 run_id: string | null;71 generated_at: string;72}7374/** RFC 7807 problem+json (errors.py `problem_body`). */75export interface Problem {76 type?: string;77 title: string;78 status: number;79 detail?: string;80 instance?: string;81 resource?: string;82 id?: string;83}8485// ---------------------------------------------------------------------------------------------- provenance & values8687export interface Provenance {88 source: string | null; // worldbank | imf | oecd | eurostat | who | fred | owid | bis | ilo89 source_name: string | null;90 dataset: string | null;91 series_code: string | null;92 retrieved_at: string | null;93 source_updated_at: string | null;94 url: string | null;95 transform: string | null;96 licence: string | null;97 /** Series endpoint `sources[]` only. */98 n_values?: number;99}100101export interface ChangeValue {102 abs: number | null;103 pct: number | null;104 formatted: string | null;105 /** `change_10y` only. */106 value_10y_ago?: number | null;107}108109export interface Prev {110 period: string | null;111 value: number | null;112}113114/** `[year, value]` — API sparklines (last ≤ 30 annual, non-forecast points). */115export type SparkPoint = [number, number | null];116117/** One indicator's latest value for a country (schemas.MetricValue) — headline & topic pages. */118export interface MetricValue {119 indicator: string; // slug120 indicator_name: string | null;121 has_data: boolean;122 value: number | null;123 /** Server-formatted display string (same rules as lib/format.ts) — prefer it when present. */124 formatted: string | null;125 period: string | null;126 year: number | null;127 frequency: Frequency | string | null;128 unit: string | null;129 unit_short: string | null;130 format: IndicatorFormat | string | null;131 is_estimate: boolean;132 is_forecast: boolean;133 status: ObservationStatus | string | null;134 prev: Prev | null;135 change: ChangeValue | null;136 change_10y: ChangeValue | null;137 rank_world: number | null;138 n_world: number | null;139 rank_region: number | null;140 n_region: number | null;141 rank_income: number | null;142 n_income: number | null;143 rank_year: number | null;144 rank_is_stale: boolean;145 higher_is_better: boolean | null;146 sparkline: SparkPoint[];147 provenance: Provenance | null;148}149150// ---------------------------------------------------------------------------------------------- cards151152export interface CountryCard {153 id: string; // ISO3154 iso2: string | null;155 slug: string | null;156 name: string | null;157 flag: string | null;158 region: string | null; // WB region id (ECS, NAC …)159 region_name: string | null;160 income: IncomeGroup | string | null;161 income_name: string | null;162 kind: 'country' | 'aggregate' | string | null;163}164165export interface IndicatorCard {166 id: string;167 slug: string;168 name: string | null;169 short_name: string | null;170 topic: TopicId | string | null;171 subtopic: string | null;172 unit: string | null;173 unit_short: string | null;174 format: IndicatorFormat | string | null;175 precision: number | null;176 frequency: Frequency | string | null;177 aggregation: string | null;178 higher_is_better: boolean | null;179 ranking_eligible: boolean | null;180 featured: boolean | null;181}182183export interface IndicatorSummary extends IndicatorCard {184 description: string | null;185 n_countries: number | null;186 n_observations: number | null;187 first_year: number | null;188 last_year: number | null;189 latest_source_updated_at: string | null;190 primary_source_id: string | null;191 coverage_pct: number | null;192 tags?: string[];193}194195export interface GroupCard {196 id: string;197 slug: string | null;198 name: string | null;199 kind: 'world' | 'region' | 'continent' | 'income' | 'org' | 'custom' | string | null;200 wb_code: string | null;201 n_members: number | null;202}203204// ---------------------------------------------------------------------------------------------- countries205206/** `/countries` item. */207export interface CountrySummary extends CountryCard {208 capital: string | null;209 continent: string | null;210 subregion: string | null;211 population_latest: number | null;212 population_year: number | null;213 gdp_latest: number | null;214 gdp_year: number | null;215 gdp_per_capita_latest: number | null;216 gdp_per_capita_year: number | null;217 coverage_pct: number | null;218 n_indicators: number | null;219}220221export interface CountriesResponse {222 meta: Meta;223 n: number;224 filters: { region: string | null; income: string | null; q: string | null; sort: string };225 items: CountrySummary[];226}227228export interface Country extends CountryCard {229 official_name: string | null;230 iso3: string | null;231 iso_numeric: string | null;232 capital: string | null;233 continent: string | null;234 subregion: string | null;235 currency_code: string | null;236 currency_name: string | null;237 area_km2: number | null;238 latitude: number | null;239 longitude: number | null;240 un_member: boolean | null;241 independent: boolean | null;242 landlocked: boolean | null;243 borders: string[] | null;244 languages: string[] | null;245 demonym: string | null;246 status: CountryStatus | string | null;247}248249export interface Coverage {250 n_indicators: number | null;251 n_observations: number | null;252 latest_year: number | null;253 coverage_pct: number | null;254 updated_at: string | null;255}256257export interface Freshness {258 source_updated_at: string | null;259 retrieved_at: string | null;260 built_at: string | null;261}262263export interface TopicSummary {264 id: TopicId | string;265 name: string;266 short: string | null;267 order: number | null;268 blurb: string | null;269 n_indicators: number;270 n_with_data: number;271}272273export interface CountryResponse {274 meta: Meta;275 country: Country;276 groups: GroupCard[];277 coverage: Coverage | null;278 freshness: Freshness;279 headline: MetricValue[];280 topics: TopicSummary[];281 neighbours: CountryCard[];282}283284export interface SubtopicBlock {285 subtopic: string;286 indicators: MetricValue[];287}288289export interface CountryTopicResponse {290 meta: Meta;291 country: CountryCard;292 topic: { id: TopicId | string; name: string; short: string | null; order: number | null; blurb: string | null };293 n_with_data: number;294 n_indicators: number;295 subtopics: SubtopicBlock[];296}297298// ---------------------------------------------------------------------------------------------- series299300export interface SeriesValue {301 period: string | null;302 year: number | null;303 frequency: Frequency | string | null;304 value: number | null;305 is_forecast: boolean;306 is_estimate: boolean;307 status: ObservationStatus | string | null;308 source_id: string | null;309 provenance: Provenance | null;310}311312export interface SeriesStats {313 min: { year: number; value: number } | null;314 max: { year: number; value: number } | null;315 first: { year: number; value: number } | null;316 last: { year: number; value: number } | null;317 cagr: number | null; // % per year318 n: number;319}320321export interface Series {322 indicator: IndicatorCard;323 country: CountryCard;324 unit: string | null;325 frequency: Frequency | string | null;326 values: SeriesValue[];327 alternatives: SeriesValue[] | null;328 provenance: Provenance | null;329 sources: Provenance[];330 stats: SeriesStats;331}332333export interface SeriesResponse extends Series {334 meta: Meta;335}336337export interface MultiSeriesResponse {338 meta: Meta;339 n: number;340 series: Series[];341}342343// ---------------------------------------------------------------------------------------------- changes / events344345export interface ChangeItem {346 id: string | null;347 country: CountryCard | null;348 indicator: IndicatorCard | { id: string; slug: string };349 kind: ChangeKind | string | null;350 period: string | null;351 year: number | null;352 value: number | null;353 ref_value: number | null;354 delta: number | null;355 delta_pct: number | null;356 window_years: number | null;357 severity: number | null; // 0–1358 headline: string | null;359 detail: unknown;360 detected_at: string | null;361 formatted: string | null;362 provenance: Provenance | null;363}364365export interface ChangesResponse {366 meta: Meta;367 n: number;368 items: ChangeItem[];369}370371// ---------------------------------------------------------------------------------------------- similar / insights / dna372373/** `contributions` JSON: {indicator: {z_a, z_b, weight, contribution}} (ARCHITECTURE §2.1). */374export type Contributions = Record<string, { z_a?: number | null; z_b?: number | null; weight?: number | null; contribution?: number | null; value_a?: number | null; value_b?: number | null }>;375376export interface SimilarPeer {377 country: CountryCard;378 score: number | null; // 0–100379 rank: number | null;380 contributions: Contributions | string | null;381}382383export interface SimilarResponse {384 meta: Meta;385 country: CountryCard;386 mode: SimilarityMode | string;387 modes: string[];388 peers: SimilarPeer[];389}390391export interface Insight {392 id: string | null;393 template_id: string | null;394 text: string;395 values: unknown;396 indicators: string[];397 computed_at: string | null;398 provenance: Provenance[];399}400401export interface InsightsResponse {402 meta: Meta;403 country: CountryCard;404 items: Insight[];405}406407export interface DnaDimensionRow {408 id: DnaDimension | string;409 label: string;410 indicator: string | null;411 value: number | null;412}413414export interface DNAResponse {415 meta: Meta;416 country: CountryCard;417 dims: Partial<Record<DnaDimension, number | null>>;418 year_ref: number | null;419 dimensions: DnaDimensionRow[];420}421422// ---------------------------------------------------------------------------------------------- maps & rankings423424export interface MapLegend {425 min: number | null;426 max: number | null;427 /** k − 1 interior quantile breaks (3–7 classes). */428 breaks: number[];429 n_classes: number;430}431432export interface MapResponse {433 meta: Meta;434 indicator: IndicatorCard;435 year: number | null;436 year_used: number | null;437 nearest: boolean;438 values: Record<string, number | null>; // ISO3 → value439 years: Record<string, number> | null;440 formatted: Record<string, string> | null;441 legend: MapLegend;442 n: number;443 provenance: Provenance | null;444 sources: Provenance[];445}446447export interface RankingRow {448 rank: number;449 rank_world: number | null;450 n_world: number | null;451 pct_rank: number | null;452 country: CountryCard;453 value: number | null;454 formatted: string | null;455 year: number | null;456 change_1y: ChangeValue | null;457 change_10y: ChangeValue | null;458 sparkline: SparkPoint[];459 provenance: Provenance | null;460}461462export interface RankingResponse {463 meta: Meta;464 indicator: IndicatorCard;465 group: GroupCard;466 year: number | null;467 year_used: number | null;468 years_available: number[];469 sort: 'asc' | 'desc' | string;470 n: number;471 limit: number;472 offset: number;473 rows: RankingRow[];474}475476// ---------------------------------------------------------------------------------------------- home / search / health477478export interface GlobalSnapshot {479 world_population: number | null;480 world_population_formatted: string | null;481 world_population_year: number | null;482 world_gdp: number | null;483 world_gdp_formatted: string | null;484 world_gdp_year: number | null;485 median_life_expectancy: number | null;486 median_life_expectancy_year: number | null;487 n_countries: number;488 n_territories: number;489 n_indicators: number;490 n_indicators_with_data: number;491 n_observations: number;492 n_sources: number;493 built_at: string | null;494 run_id: string | null;495 note: string | null;496}497498export interface HomeListRow {499 rank: number;500 country: CountryCard;501 value: number | null;502 formatted: string | null;503 year: number | null;504 change_pct: number | null;505 change_abs: number | null;506 rank_world: number | null;507 n_world: number | null;508 provenance: Provenance | null;509}510511export type HomeListKey = 'largest_economies' | 'fastest_population_growth' | 'highest_life_expectancy' | 'energy_transition_leaders' | 'highest_gdp_per_capita_ppp' | 'lowest_unemployment';512513export interface HomeList {514 title: string;515 description: string | null;516 indicator: IndicatorCard;517 sort: 'asc' | 'desc';518 rows: HomeListRow[];519}520521export interface HomeResponse {522 meta: Meta;523 snapshot: GlobalSnapshot;524 lists: Partial<Record<HomeListKey, HomeList>> & Record<string, HomeList>;525 recent_changes: ChangeItem[];526 recently_updated: IndicatorSummary[];527 featured_indicators: IndicatorSummary[];528 trending: IndicatorSummary[];529}530531export interface SearchHit {532 type: SearchHitType | string;533 id: string;534 slug: string | null;535 name: string;536 hint: string | null;537 score: number;538 /** Site-relative URL chosen by the API (`/countries/canada`, `/countries/canada/economy` …). */539 url: string | null;540 country: CountryCard | null;541 topic: string | null;542 indicator: string | null;543}544545export interface SearchResponse {546 meta: Meta;547 q: string;548 n: number;549 hits: SearchHit[];550}551552export interface HealthResponse {553 status: 'ok' | 'empty' | 'degraded' | string;554 run_id: string | null;555 built_at: string | null;556 observations: number | null;557 countries: number | null;558 indicators: number | null;559 db_path: string | null;560 version: string | null;561 cache: Record<string, number> | null;562}563564// ---------------------------------------------------------------------------------------------- helpers shared by components565566/** Minimal display spec derived from a MetricValue or an IndicatorCard (what lib/format.ts needs). */567export interface FormatSpec {568 format: IndicatorFormat | string | null | undefined;569 unit?: string | null;570 unit_short?: string | null;571 precision?: number | null;572 frequency?: Frequency | string | null;573 name?: string | null;574 higher_is_better?: boolean | null;575}576