Files
website/src/lib/stats/views/CountryView.svelte

732 lines
19 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import type { CountryViewData } from '#lib/server/stats-views.js';
import type { StatsCommon } from '#lib/stats/matrix.js';
import { resolve } from '$app/paths';
import StatChart from '#lib/stats/StatChart.svelte';
import Kpi from '#lib/stats/Kpi.svelte';
import {
barRankOption,
change,
latest,
lineOption,
type Line,
type StatPoint
} from '#lib/stats/chart.js';
import { CW_INDUSTRIES, CW_NIGHT_MARKETS, EU_TRADE } from '#lib/stats/curacao.js';
import { compact, num, period } from '#lib/stats/format.js';
import { COUNTRY } from '#lib/stats/regions.js';
import { COUNTRY_COLOR, FLAG } from '#lib/stats/countries.js';
import ArrowLeft from 'lucide-svelte/icons/arrow-left';
import PlaceHeritage from '#lib/stats/PlaceHeritage.svelte';
let { data }: { data: CountryViewData & StatsCommon } = $props();
const c = $derived(data.code);
const s = $derived(data.series);
const a = $derived(data.attributions);
const name = $derived(COUNTRY[c] ?? c);
const color = $derived(COUNTRY_COLOR[c] ?? '#3b82f6');
const has = (id: string) => (s[`${id}:${c}`]?.points.length ?? 0) > 0;
const pts = (id: string, code = c): StatPoint[] => s[`${id}:${code}`]?.points ?? [];
const forecastFrom = (id: string) => s[`${id}:${c}`]?.forecastFrom;
/** IMF series split into outcome (solid) and WEO projection (dashed). */
function imfLines(id: string, label: string, col: string): Line[] {
const all = pts(id);
const from = forecastFrom(id);
if (!from) return [{ name: label, points: all, color: col }];
const cut = `${from}-01-01`;
const past = all.filter(([d]) => d < cut);
const future = all.filter(([d]) => d >= cut);
return [
{ name: label, points: past, color: col },
{
name: `${label} (IMF forecast)`,
points: past.length ? [past[past.length - 1], ...future] : future,
color: col,
dashed: true
}
];
}
/** IMF if it covers the country, else the World Bank fallback. */
const pick = (imf: string, wb: string) => (has(imf) ? imf : wb);
const lastOf = (id: string) => {
const from = forecastFrom(id);
const p = pts(id).filter(([d]) => !from || d < `${from}-01-01`);
return latest(p);
};
const kPop = $derived(lastOf('wb:SP.POP.TOTL'));
const kGdp = $derived(lastOf('wb:NY.GDP.PCAP.PP.KD'));
const kGrowth = $derived(lastOf(pick('imf:NGDP_RPCH', 'wb:NY.GDP.MKTP.KD.ZG')));
const kInfl = $derived(lastOf(pick('imf:PCPIPCH', 'wb:FP.CPI.TOTL.ZG')));
const kUnemp = $derived(lastOf(pick('imf:LUR', 'wb:SL.UEM.TOTL.ZS')));
const kLife = $derived(lastOf('wb:SP.DYN.LE00.IN'));
const kTour = $derived(lastOf('wb:ST.INT.ARVL'));
const kDebt = $derived(lastOf(pick('imf:GGXWDG_NGDP', 'wb:GC.DOD.TOTL.GD.ZS')));
const popDelta = $derived(change(pts('wb:SP.POP.TOTL'), 10)?.pct ?? null);
const imfCovered = $derived(has('imf:NGDP_RPCH'));
// §8 Curaçao detail series: stored without the `:CUW` suffix.
const cw = (id: string): StatPoint[] => s[id]?.points ?? [];
const isCw = $derived(c === 'CUW' && cw('cw:cpi:index').length > 0);
const cwGdpYear = $derived(latest(cw('cw:gdp:gdp'))?.[0]);
const cwIndustries = $derived(
Object.entries(CW_INDUSTRIES)
.map(([k, label]) => ({
label,
value: cw(`cw:gdp:${k}`).find(([d]) => d === cwGdpYear)?.[1] ?? NaN
}))
.filter((r) => Number.isFinite(r.value))
);
const cwCpi = $derived(latest(cw('cw:cpi:yoy')));
const cwPop = $derived(latest(cw('cw:pop:jan1')));
const cwUnemp = $derived(latest(cw('cw:lfs:unemployment_rate_pct:total')));
const cwGrowth = $derived(latest(cw('cw:gdp:real_growth_pct')));
</script>
<section class="stats-intro">
<a class="back" href={resolve('/stats/core/overview' as '/stats')}
><ArrowLeft size={14} /> countries</a
>
<span class="stats-eyebrow">stats / countries / {c}</span>
<h1><span class="flag">{FLAG[c]}</span> {name}</h1>
<p>
{#if c === 'CUW'}
Curaçao has been a country within the Kingdom of the Netherlands since 2010, with about
158,000 inhabitants. The IMF's World Economic Outlook doesn't cover it, so the headline
numbers come from the World Bank; the detail further down comes from CBS Curaçao (the island's
statistics office), Eurostat, ILOSTAT, Our World in Data and the UN. Amounts in guilders (ANG,
pegged at 1.79 to the US dollar). The central bank's balance-of-payments tables can't be
fetched automatically, so they aren't here yet.
{:else}
{name} at a glance, from the World Bank's development indicators and the IMF World Economic Outlook{imfCovered
? ' (dashed lines are IMF forecasts)'
: ''}.
{/if}
</p>
</section>
<div class="kpis">
<Kpi
label="Population"
value={compact(kPop?.[1])}
sub={` 10 yr · ${period(kPop?.[0], 'A')}`}
delta={popDelta}
deltaUnit="%"
{color}
/>
<Kpi
label="GDP per capita, PPP"
value={`$${compact(kGdp?.[1])}`}
sub={`constant 2021 int. $ · ${period(kGdp?.[0], 'A')}`}
color="#10b981"
/>
<Kpi
label="Real growth"
value={num(kGrowth?.[1], 1)}
unit="%"
sub={period(kGrowth?.[0], 'A')}
color="#3b82f6"
/>
<Kpi
label="Inflation"
value={num(kInfl?.[1], 1)}
unit="%"
sub={period(kInfl?.[0], 'A')}
color="#e11d48"
/>
{#if kUnemp}
<Kpi
label="Unemployment"
value={num(kUnemp[1], 1)}
unit="%"
sub={period(kUnemp[0], 'A')}
color="#a855f7"
/>
{/if}
{#if kDebt}
<Kpi
label="Government debt"
value={num(kDebt[1], 0)}
unit="% GDP"
sub={period(kDebt[0], 'A')}
color="#f97316"
/>
{/if}
<Kpi
label="Life expectancy"
value={num(kLife?.[1], 1)}
unit="yr"
sub={period(kLife?.[0], 'A')}
color="#06b6d4"
/>
{#if kTour}
<Kpi
label="Tourist arrivals"
value={compact(kTour[1])}
sub={period(kTour[0], 'A')}
color="#eab308"
/>
{/if}
</div>
<h2 class="stats-section-title">People</h2>
<div class="stats-grid">
<StatChart
title="Population"
subtitle="inhabitants · net migration on the right"
sources={['worldbank']}
attributions={a}
option={(t) =>
lineOption(
t,
[
{ name: 'Population', points: pts('wb:SP.POP.TOTL'), color, kind: 'area' },
{
name: 'Net migration',
points: pts('wb:SM.POP.NETM'),
color: '#64748b',
kind: 'bar',
right: true
}
],
{ decimals: 0, min: 'dataMin', rightUnit: 'people' }
)}
/>
<StatChart
title="Life expectancy"
subtitle="years at birth, next to the Netherlands and the world"
sources={['worldbank']}
attributions={a}
option={(t) =>
lineOption(
t,
[
{ name, points: pts('wb:SP.DYN.LE00.IN'), color },
...(c !== 'NLD'
? [
{
name: 'Netherlands',
points: pts('wb:SP.DYN.LE00.IN', 'NLD'),
color: '#f97316',
dashed: true
}
]
: []),
...(c !== 'WLD'
? [
{
name: 'World',
points: pts('wb:SP.DYN.LE00.IN', 'WLD'),
color: '#94a3b8',
dashed: true
}
]
: [])
],
{ unit: 'years', decimals: 1, min: 'dataMin' }
)}
/>
<StatChart
title="Fertility and ageing"
subtitle="births per woman · share aged 65+ on the right"
sources={['worldbank']}
attributions={a}
option={(t) =>
lineOption(
t,
[
{ name: 'Fertility rate', points: pts('wb:SP.DYN.TFRT.IN'), color: '#ec4899' },
{
name: 'Aged 65+ (%)',
points: pts('wb:SP.POP.65UP.TO.ZS'),
color: '#64748b',
right: true
}
],
{ unit: 'births', rightUnit: '%', decimals: 2 }
)}
/>
</div>
<h2 class="stats-section-title">Economy</h2>
<div class="stats-grid">
<StatChart
title="GDP per capita"
subtitle="PPP, constant 2021 international $ · vs the Netherlands and the world"
sources={['worldbank']}
attributions={a}
option={(t) =>
lineOption(
t,
[
{ name, points: pts('wb:NY.GDP.PCAP.PP.KD'), color },
...(c !== 'NLD'
? [
{
name: 'Netherlands',
points: pts('wb:NY.GDP.PCAP.PP.KD', 'NLD'),
color: '#f97316',
dashed: true
}
]
: []),
...(c !== 'WLD'
? [
{
name: 'World',
points: pts('wb:NY.GDP.PCAP.PP.KD', 'WLD'),
color: '#94a3b8',
dashed: true
}
]
: [])
],
{ unit: '$', decimals: 0 }
)}
/>
<StatChart
title="Real GDP growth"
subtitle={has('imf:NGDP_RPCH') ? '% a year · IMF WEO incl. forecasts' : '% a year · World Bank'}
sources={has('imf:NGDP_RPCH') ? ['imf'] : ['worldbank']}
attributions={a}
option={(t) =>
lineOption(
t,
has('imf:NGDP_RPCH')
? imfLines('imf:NGDP_RPCH', 'Growth', '#3b82f6')
: [
{ name: 'Growth', points: pts('wb:NY.GDP.MKTP.KD.ZG'), color: '#3b82f6', kind: 'bar' }
],
{ unit: '%', decimals: 1, legend: has('imf:NGDP_RPCH') }
)}
/>
<StatChart
title="Inflation"
subtitle="% consumer prices, a year"
sources={has('imf:PCPIPCH') ? ['imf', 'worldbank'] : ['worldbank']}
attributions={a}
option={(t) =>
lineOption(
t,
[
...(has('imf:PCPIPCH')
? imfLines('imf:PCPIPCH', name, '#e11d48')
: [{ name, points: pts('wb:FP.CPI.TOTL.ZG'), color: '#e11d48' }]),
...(c !== 'NLD'
? [
{
name: 'Netherlands',
points: pts('wb:FP.CPI.TOTL.ZG', 'NLD'),
color: '#f97316',
dashed: true
}
]
: [])
],
{ unit: '%', decimals: 1 }
)}
/>
{#if has('imf:LUR') || has('wb:SL.UEM.TOTL.ZS')}
<StatChart
title="Unemployment"
subtitle="% of the labour force"
sources={has('imf:LUR') ? ['imf'] : ['worldbank']}
attributions={a}
option={(t) =>
lineOption(
t,
has('imf:LUR')
? imfLines('imf:LUR', 'Unemployment', '#a855f7')
: [{ name: 'Unemployment', points: pts('wb:SL.UEM.TOTL.ZS'), color: '#a855f7' }],
{ unit: '%', decimals: 1 }
)}
/>
{/if}
{#if has('wb:NE.EXP.GNFS.ZS') || has('wb:NE.TRD.GNFS.ZS')}
<StatChart
title="Trade"
subtitle="exports and imports of goods and services, % of GDP"
sources={['worldbank']}
attributions={a}
option={(t) =>
lineOption(
t,
[
{ name: 'Exports', points: pts('wb:NE.EXP.GNFS.ZS'), color: '#10b981' },
{ name: 'Imports', points: pts('wb:NE.IMP.GNFS.ZS'), color: '#e11d48' },
{
name: 'Services value added',
points: pts('wb:NV.SRV.TOTL.ZS'),
color: '#64748b',
dashed: true
}
],
{ unit: '% GDP', decimals: 1 }
)}
/>
{/if}
{#if has('wb:ST.INT.ARVL') || has('wb:ST.INT.RCPT.CD')}
<StatChart
title="Tourism"
subtitle="international arrivals · receipts (US$) on the right"
sources={['worldbank']}
attributions={a}
option={(t) =>
lineOption(
t,
[
{ name: 'Arrivals', points: pts('wb:ST.INT.ARVL'), color: '#eab308', kind: 'bar' },
{ name: 'Receipts', points: pts('wb:ST.INT.RCPT.CD'), color: '#06b6d4', right: true }
],
{ decimals: 0, rightUnit: 'US$' }
)}
/>
{/if}
</div>
<h2 class="stats-section-title">Government and energy</h2>
<div class="stats-grid">
{#if !isCw || has('imf:GGXWDG_NGDP') || has('wb:GC.DOD.TOTL.GD.ZS')}
<StatChart
title="Government finances"
subtitle={has('imf:GGXWDG_NGDP')
? '% of GDP · IMF (general government)'
: '% of GDP · World Bank (central government)'}
sources={has('imf:GGXWDG_NGDP') ? ['imf'] : ['worldbank']}
attributions={a}
option={(t) =>
lineOption(
t,
has('imf:GGXWDG_NGDP')
? [
...imfLines('imf:GGXWDG_NGDP', 'Debt', '#e11d48'),
...imfLines('imf:GGR_G01_GDP_PT', 'Revenue', '#10b981'),
...imfLines('imf:G_X_G01_GDP_PT', 'Spending', '#3b82f6')
]
: [
{ name: 'Debt', points: pts('wb:GC.DOD.TOTL.GD.ZS'), color: '#e11d48' },
{ name: 'Revenue', points: pts('wb:GC.REV.XGRT.GD.ZS'), color: '#10b981' },
{ name: 'Expense', points: pts('wb:GC.XPN.TOTL.GD.ZS'), color: '#3b82f6' }
],
{ unit: '% GDP', decimals: 1 }
)}
note={!has('imf:GGXWDG_NGDP') && !has('wb:GC.DOD.TOTL.GD.ZS')
? 'No government finance data for this country yet.'
: ''}
/>
{/if}
<StatChart
title="Energy"
subtitle="net energy imports (% of use) · renewable share of electricity"
sources={['worldbank']}
attributions={a}
option={(t) =>
lineOption(
t,
[
{ name: 'Net energy imports', points: pts('wb:EG.IMP.CONS.ZS'), color: '#64748b' },
{ name: 'Renewable electricity', points: pts('wb:EG.ELC.RNEW.ZS'), color: '#10b981' }
],
{ unit: '%', decimals: 1 }
)}
/>
</div>
{#if isCw}
<h2 class="stats-section-title">Curaçao in detail</h2>
<div class="kpis">
<Kpi
label="Population, 1 Jan"
value={compact(cwPop?.[1])}
sub={`CBS Curaçao · ${period(cwPop?.[0], 'A')}`}
{color}
/>
<Kpi
label="Inflation"
value={num(cwCpi?.[1], 1)}
unit="%"
sub={`year on year · ${period(cwCpi?.[0], 'M')}`}
color="#e11d48"
/>
<Kpi
label="Real growth"
value={num(cwGrowth?.[1], 1)}
unit="%"
sub={period(cwGrowth?.[0], 'A')}
color="#3b82f6"
/>
<Kpi
label="Unemployment"
value={num(cwUnemp?.[1], 1)}
unit="%"
sub={`labour force survey · ${period(cwUnemp?.[0], 'A')}`}
color="#a855f7"
/>
</div>
<div class="stats-grid">
<StatChart
title="Consumer prices since 1970"
subtitle="CPI (2023 = 100) · inflation year on year (%) on the right"
sources={['cbscw']}
attributions={a}
option={(t) =>
lineOption(
t,
[
{ name: 'CPI', points: cw('cw:cpi:index'), color: '#64748b', kind: 'area' },
{ name: 'Inflation', points: cw('cw:cpi:yoy'), color: '#e11d48', right: true }
],
{ unit: 'index', rightUnit: '%', decimals: 1, zoom: true }
)}
/>
<StatChart
title="GDP"
subtitle="nominal, million guilders · real growth (%) on the right"
sources={['cbscw']}
attributions={a}
option={(t) =>
lineOption(
t,
[
{ name: 'GDP', points: cw('cw:gdp:gdp'), color, kind: 'bar' },
{
name: 'Real growth',
points: cw('cw:gdp:real_growth_pct'),
color: '#3b82f6',
right: true
}
],
{ unit: 'mln ANG', rightUnit: '%', decimals: 1 }
)}
/>
<StatChart
title="GDP by industry"
subtitle={`gross value added, million guilders · ${period(cwGdpYear, 'A')}`}
sources={['cbscw']}
attributions={a}
height="420px"
option={(t) => barRankOption(t, cwIndustries, { decimals: 0, unit: 'mln ANG', color })}
/>
<StatChart
title="Population since 1900"
subtitle="1 January (31 December before 1992) and census counts · UN estimate dashed"
sources={['cbscw', 'owid']}
attributions={a}
option={(t) =>
lineOption(
t,
[
{ name: 'Population', points: cw('cw:pop:jan1'), color, kind: 'area' },
{ name: 'Census', points: cw('cw:pop:census'), color: '#0f172a', kind: 'bar' },
{
name: 'UN WPP (via OWID)',
points: cw('owid:cw:population'),
color: '#94a3b8',
dashed: true
}
],
{ decimals: 0, unit: 'people' }
)}
/>
<StatChart
title="Unemployment"
subtitle="% of the labour force · CBS labour force survey (no survey in 2021 and 2023), ILO dashed"
sources={['cbscw', 'ilostat']}
attributions={a}
option={(t) =>
lineOption(
t,
[
{ name: 'Total', points: cw('cw:lfs:unemployment_rate_pct:total'), color: '#a855f7' },
{ name: 'Men', points: cw('cw:lfs:unemployment_rate_pct:m'), color: '#3b82f6' },
{ name: 'Women', points: cw('cw:lfs:unemployment_rate_pct:f'), color: '#ec4899' },
{
name: 'Aged 15–24',
points: cw('cw:lfs:unemployment_rate_pct:15-24'),
color: '#f97316'
},
{
name: 'ILO estimate (15+)',
points: cw('ilostat:une_deap_sex_age_rt:total:15plus'),
color: '#94a3b8',
dashed: true
}
],
{ unit: '%', decimals: 1 }
)}
/>
<StatChart
title="Tourism"
subtitle="cruise passengers and daytrippers · stay-over nights on the right"
sources={['cbscw']}
attributions={a}
option={(t) =>
lineOption(
t,
[
{
name: 'Cruise passengers',
points: cw('cw:tour:cruise_pax'),
color: '#06b6d4',
kind: 'bar'
},
{
name: 'Daytrippers',
points: cw('cw:tour:daytrippers'),
color: '#eab308',
kind: 'bar'
},
{
name: 'Stay-over nights',
points: cw('cw:tour:nights'),
color: '#e11d48',
right: true
}
],
{ decimals: 0, rightUnit: 'nights' }
)}
/>
<StatChart
title="Stay-over nights by market"
subtitle="tourist nights by region of residence"
sources={['cbscw']}
attributions={a}
option={(t) =>
lineOption(
t,
Object.entries(CW_NIGHT_MARKETS).map(([k, label], i) => ({
name: label,
points: cw(`cw:tour:nights:${k}`),
color: ['#3b82f6', '#e11d48', '#10b981', '#eab308', '#94a3b8'][i],
kind: 'bar' as const,
stack: 'nights'
})),
{ decimals: 0, unit: 'nights' }
)}
/>
<StatChart
title="Government revenue and expenditure"
subtitle="million guilders · balance as bars (recent years provisional)"
sources={['cbscw']}
attributions={a}
option={(t) =>
lineOption(
t,
[
{ name: 'Revenue', points: cw('cw:gov:r.total'), color: '#10b981' },
{ name: 'Expenditure', points: cw('cw:gov:e.total'), color: '#3b82f6' },
{ name: 'Balance', points: cw('cw:gov:balance'), color: '#e11d48', kind: 'bar' }
],
{ unit: 'mln ANG', decimals: 0 }
)}
/>
<StatChart
title="Trade with the EU"
subtitle="EU27 goods exports to and imports from Curaçao, million €"
sources={['eurostat']}
attributions={a}
option={(t) =>
lineOption(
t,
[
{
name: 'EU exports to Curaçao',
points: cw(EU_TRADE('MIO_EXP_VAL')),
color: '#3b82f6'
},
{
name: 'EU imports from Curaçao',
points: cw(EU_TRADE('MIO_IMP_VAL')),
color: '#f97316'
},
{
name: 'EU balance',
points: cw(EU_TRADE('MIO_BAL_VAL')),
color: '#94a3b8',
kind: 'bar'
}
],
{ unit: 'mln €', decimals: 0 }
)}
/>
<StatChart
title="Goods trade (as reported by Curaçao)"
subtitle="US$ · only 2017–2020 are reported to the UN"
sources={['comtrade']}
attributions={a}
option={(t) =>
lineOption(
t,
[
{
name: 'Imports',
points: cw('comtrade:cw:imports:world'),
color: '#e11d48',
kind: 'bar'
},
{
name: 'Exports',
points: cw('comtrade:cw:exports:world'),
color: '#10b981',
kind: 'bar'
},
{
name: 'Imports from the US',
points: cw('comtrade:cw:imports:usa'),
color: '#3b82f6'
},
{ name: 'Imports from NL', points: cw('comtrade:cw:imports:nld'), color: '#f97316' }
],
{ decimals: 0, unit: 'US$' }
)}
/>
<StatChart
title="Climate and energy"
subtitle="CO₂ per person (t) · renewable share of final energy (%) on the right"
sources={['owid', 'unsdg']}
attributions={a}
option={(t) =>
lineOption(
t,
[
{
name: 'CO₂ per person',
points: cw('owid:cw:co-emissions-per-capita'),
color: '#64748b'
},
{
name: 'Renewable share',
points: cw('unsdg:eg_fec_rnew'),
color: '#10b981',
right: true
}
],
{ unit: 't', rightUnit: '%', decimals: 1 }
)}
/>
</div>
{/if}
{#if data.heritage}
<PlaceHeritage h={data.heritage} attributions={a} />
{/if}
<style>
.back {
display: inline-flex;
align-items: center;
gap: 0.3rem;
color: var(--site-muted);
font-size: 0.8rem;
text-decoration: none;
width: fit-content;
}
.back:hover {
color: var(--site-text);
}
.flag {
font-size: 0.9em;
}
</style>