All checks were successful
Deploy production / deploy (push) Successful in 1m13s
- /stats/economy defaults multi-country charts to CAN KOR CHN JPN USA NLD CUW, with add-a-country chips and an EU 27 comparison line - CPI index levels rebased to a chosen base year, index/YoY toggle, categories (food, energy, housing, core) where the source has them - new no-key collectors: oecd (CPI categories, 10y yields), statcan (CPI); bis WS_LONG_CPI, FRED US CPI categories, Eurostat prc_hicp_minr, WB FP.CPI.TOTL - Canada added as a region; CBS Curacao fallbacks for growth/unemployment - timers stats-oecd and stats-statcan
599 lines
18 KiB
Svelte
599 lines
18 KiB
Svelte
<script lang="ts">
|
||
import StatChart from '#lib/stats/StatChart.svelte';
|
||
import Kpi from '#lib/stats/Kpi.svelte';
|
||
import CountryPicker from '#lib/stats/CountryPicker.svelte';
|
||
import * as Tabs from '#lib/components/ui/tabs/index.js';
|
||
import { barRankOption, latest, lineOption, spread, type Line } from '#lib/stats/chart.js';
|
||
import { num, period } from '#lib/stats/format.js';
|
||
import { COUNTRY } from '#lib/stats/regions.js';
|
||
import { COUNTRY_COLOR } from '#lib/stats/countries.js';
|
||
import {
|
||
ANNUAL,
|
||
CORE7,
|
||
CPI_BY_CATEGORY,
|
||
CPI_CATS,
|
||
CPI_CAT_LABEL,
|
||
CPI_TOTAL,
|
||
POLICY_RATE,
|
||
rebase,
|
||
yoyByDate,
|
||
type CpiCat
|
||
} from '#lib/stats/econ.js';
|
||
|
||
let { data } = $props();
|
||
const s = $derived(data.series);
|
||
const a = $derived(data.attributions);
|
||
const pts = (id: string) => s[id]?.points ?? [];
|
||
|
||
// Country selection shared by every multi-country chart.
|
||
let selected = $state<string[]>([...CORE7]);
|
||
let eu = $state(true);
|
||
const regions = $derived([...selected, ...(eu ? ['EU27'] : [])]);
|
||
const nameOf = (c: string) =>
|
||
c === 'EU27' ? 'EU 27' : c === 'EA' ? 'Euro area' : (COUNTRY[c] ?? c);
|
||
const colorOf = (c: string) => COUNTRY_COLOR[c] ?? '#64748b';
|
||
const missing = (list: string[]) =>
|
||
list.length ? `No data for ${list.map(nameOf).join(', ')}.` : '';
|
||
|
||
// CPI: index level rebased to a common year, or year-on-year %.
|
||
let cpiMode = $state<'level' | 'yoy'>('level');
|
||
let baseYear = $state(2015);
|
||
const BASE_YEARS = [2000, 2010, 2015, 2020, 2025];
|
||
const cpiLine = (id: string): [string, number][] | null => {
|
||
const p = pts(id);
|
||
if (!p.length) return null;
|
||
return cpiMode === 'yoy' ? yoyByDate(p) : rebase(p, baseYear);
|
||
};
|
||
const cpiLines = $derived.by(() => {
|
||
const lines: Line[] = [];
|
||
const gaps: string[] = [];
|
||
for (const c of regions) {
|
||
const src = CPI_TOTAL[c];
|
||
const points = src ? cpiLine(src.id) : null;
|
||
if (!points?.length) gaps.push(c);
|
||
else lines.push({ name: nameOf(c), points, color: colorOf(c), dashed: c === 'EU27' });
|
||
}
|
||
return { lines, gaps };
|
||
});
|
||
const cpiSources = $derived([
|
||
...new Set(regions.map((c) => CPI_TOTAL[c]?.source).filter(Boolean))
|
||
] as string[]);
|
||
|
||
let catCountry = $state('NLD');
|
||
const CAT_COLORS: Record<CpiCat, string> = {
|
||
total: '#64748b',
|
||
food: '#10b981',
|
||
housing: '#a855f7',
|
||
energy: '#f97316',
|
||
core: '#3b82f6'
|
||
};
|
||
const catLines = $derived.by(() => {
|
||
const def = CPI_BY_CATEGORY[catCountry];
|
||
return CPI_CATS.flatMap((k) => {
|
||
const id = def?.ids[k];
|
||
const points = id ? cpiLine(id) : null;
|
||
return points?.length
|
||
? [{ name: CPI_CAT_LABEL[k], points, color: CAT_COLORS[k], dashed: k === 'total' }]
|
||
: [];
|
||
});
|
||
});
|
||
const catMissing = $derived(
|
||
CPI_CATS.filter((k) => !CPI_BY_CATEGORY[catCountry]?.ids[k]).map((k) =>
|
||
CPI_CAT_LABEL[k].toLowerCase()
|
||
)
|
||
);
|
||
|
||
// Policy rates (BIS), euro members follow the ECB.
|
||
const rateLines = $derived.by(() => {
|
||
const seen: string[] = [];
|
||
const lines: Line[] = [];
|
||
const gaps: string[] = [];
|
||
for (const c of [...selected, ...(eu ? ['EA'] : [])]) {
|
||
const area = POLICY_RATE[c] ?? (['DEU', 'FRA', 'BEL'].includes(c) ? 'XM' : undefined);
|
||
const p = area ? pts(`bis:M:${area}`) : [];
|
||
if (!p.length) gaps.push(c);
|
||
else if (!seen.includes(area!)) {
|
||
seen.push(area!);
|
||
lines.push({
|
||
name: area === 'XM' ? 'ECB (euro area)' : nameOf(c),
|
||
points: p,
|
||
color: area === 'XM' ? '#1d4ed8' : colorOf(c),
|
||
kind: 'step'
|
||
});
|
||
}
|
||
}
|
||
return { lines, gaps };
|
||
});
|
||
|
||
const yieldLines = $derived.by(() => {
|
||
const lines: Line[] = [];
|
||
const gaps: string[] = [];
|
||
for (const c of [...selected, ...(eu ? ['EA'] : [])]) {
|
||
const p = pts(`oecd:irlt:${c}`);
|
||
if (!p.length) gaps.push(c);
|
||
else lines.push({ name: nameOf(c), points: p, color: colorOf(c), dashed: c === 'EA' });
|
||
}
|
||
return { lines, gaps };
|
||
});
|
||
|
||
// Annual indicators: IMF WEO (outcomes only, forecasts cut), World Bank where the IMF has none.
|
||
const annual = (key: keyof typeof ANNUAL) => {
|
||
const ind = ANNUAL[key];
|
||
const lines: Line[] = [];
|
||
const gaps: string[] = [];
|
||
const used: string[] = [];
|
||
for (const c of regions) {
|
||
const imf = ind.imf ? s[`imf:${ind.imf}:${c}`] : undefined;
|
||
const wb = s[`wb:${ind.wb}:${c}`];
|
||
const cw = c === 'CUW' && ind.cw ? s[ind.cw] : undefined;
|
||
const pick = imf?.points.length
|
||
? imf
|
||
: cw?.points.length
|
||
? cw
|
||
: wb?.points.length
|
||
? wb
|
||
: undefined;
|
||
if (!pick) {
|
||
gaps.push(c);
|
||
continue;
|
||
}
|
||
if (!used.includes(pick.sourceId)) used.push(pick.sourceId);
|
||
const cut = pick.forecastFrom ? `${pick.forecastFrom}-01-01` : '9999';
|
||
lines.push({
|
||
name: nameOf(c),
|
||
points: pick.points.filter(([d]) => d < cut),
|
||
color: colorOf(c),
|
||
dashed: c === 'EU27'
|
||
});
|
||
}
|
||
return { lines, gaps, sources: used };
|
||
};
|
||
const growth = $derived(annual('growth'));
|
||
const unemployment = $derived(annual('unemployment'));
|
||
const govDebt = $derived(annual('debt'));
|
||
const trade = $derived(annual('trade'));
|
||
|
||
// Europe-only charts (Eurostat).
|
||
const y = (g: string) => pts(`eurostat:irt_lt_mcby_m:MCBY.${g}`);
|
||
const frde = $derived(spread(y('FR'), y('DE')));
|
||
const itde = $derived(spread(y('IT'), y('DE')));
|
||
const nlde = $derived(spread(y('NL'), y('DE')));
|
||
const us10 = $derived(pts('fred:DGS10'));
|
||
const curve = $derived(spread(us10, pts('fred:DGS2')));
|
||
const EU_LIST = ['NL', 'DE', 'FR', 'BE', 'IT', 'ES', 'PL', 'SE', 'DK', 'AT', 'EU27_2020', 'EA20'];
|
||
const lastOf = (id: string) => latest(s[id]?.points);
|
||
const lastYear = (prefix: string) =>
|
||
EU_LIST.map((g) => ({ g, p: lastOf(`${prefix}.${g}`) }))
|
||
.filter((r) => r.p)
|
||
.map((r) => ({
|
||
label: COUNTRY[r.g] ?? r.g,
|
||
value: r.p![1],
|
||
highlight: r.g === 'NL',
|
||
year: r.p![0].slice(0, 4)
|
||
}));
|
||
const debt = $derived(lastYear('eurostat:gov_10dd_edpt1:PC_GDP.S13.GD'));
|
||
const deficit = $derived(lastYear('eurostat:gov_10dd_edpt1:PC_GDP.S13.B9'));
|
||
const migrationPerK = $derived(
|
||
EU_LIST.filter((g) => !g.startsWith('E'))
|
||
.map((g) => {
|
||
const m = lastOf(`eurostat:migr_imm1ctz:TOTAL.COMPLET.TOTAL.NR.T.${g}`);
|
||
const pop = pts(`eurostat:demo_pjan:NR.TOTAL.T.${g}`).find(([d]) => m && d === m[0]);
|
||
return m && pop
|
||
? { label: COUNTRY[g] ?? g, value: (m[1] / pop[1]) * 1000, highlight: g === 'NL' }
|
||
: null;
|
||
})
|
||
.filter((r) => r != null)
|
||
);
|
||
const k = (id: string) => lastOf(id);
|
||
const fed = $derived(k('bis:M:US'));
|
||
const ecb = $derived(k('bis:M:XM'));
|
||
const boc = $derived(k('bis:M:CA'));
|
||
const boj = $derived(k('bis:M:JP'));
|
||
const nlInfl = $derived(k('eurostat:prc_hicp_minr:RCH_A.TOTAL.NL'));
|
||
const eaInfl = $derived(k('eurostat:prc_hicp_minr:RCH_A.TOTAL.EA20'));
|
||
const usInfl = $derived(latest(yoyByDate(pts('fred:CPIAUCSL'))));
|
||
const caInfl = $derived(latest(yoyByDate(pts('statcan:cpi:total'))));
|
||
</script>
|
||
|
||
<svelte:head>
|
||
<title>economy · stats · Stan Runge</title>
|
||
<meta
|
||
name="description"
|
||
content="Consumer prices (index levels and inflation by category), interest rates, bond yields, growth, unemployment, debt and trade for Canada, South Korea, China, Japan, the US, the Netherlands and Curaçao, with the EU as a comparison."
|
||
/>
|
||
</svelte:head>
|
||
|
||
<section class="stats-intro">
|
||
<span class="stats-eyebrow">stats / economy</span>
|
||
<h1>Economy</h1>
|
||
<p>
|
||
Consumer prices, central bank rates, bond yields, growth, jobs, debt and trade for Canada, South
|
||
Korea, China, Japan, the US, the Netherlands and Curaçao, with the EU as a comparison line. Add
|
||
other countries with the picker. Refreshed from open sources (CBS, BLS, Statistics Canada, CBS
|
||
Curaçao, BIS, OECD, IMF, World Bank, Eurostat).
|
||
</p>
|
||
</section>
|
||
|
||
<div class="kpis">
|
||
<Kpi
|
||
label="Fed policy rate"
|
||
value={num(fed?.[1], 2)}
|
||
unit="%"
|
||
sub={period(fed?.[0])}
|
||
color="#3b82f6"
|
||
/>
|
||
<Kpi
|
||
label="ECB policy rate"
|
||
value={num(ecb?.[1], 2)}
|
||
unit="%"
|
||
sub={period(ecb?.[0])}
|
||
color="#eab308"
|
||
/>
|
||
<Kpi
|
||
label="Bank of Canada"
|
||
value={num(boc?.[1], 2)}
|
||
unit="%"
|
||
sub={period(boc?.[0])}
|
||
color="#dc2626"
|
||
/>
|
||
<Kpi
|
||
label="Bank of Japan"
|
||
value={num(boj?.[1], 2)}
|
||
unit="%"
|
||
sub={period(boj?.[0])}
|
||
color="#ec4899"
|
||
/>
|
||
<Kpi
|
||
label="NL inflation (HICP)"
|
||
value={num(nlInfl?.[1])}
|
||
unit="%"
|
||
sub={period(nlInfl?.[0])}
|
||
color="#f97316"
|
||
/>
|
||
<Kpi
|
||
label="Euro area inflation"
|
||
value={num(eaInfl?.[1])}
|
||
unit="%"
|
||
sub={period(eaInfl?.[0])}
|
||
color="#10b981"
|
||
/>
|
||
<Kpi
|
||
label="US inflation (CPI)"
|
||
value={num(usInfl?.[1])}
|
||
unit="%"
|
||
sub={period(usInfl?.[0])}
|
||
color="#06b6d4"
|
||
/>
|
||
<Kpi
|
||
label="Canada inflation (CPI)"
|
||
value={num(caInfl?.[1])}
|
||
unit="%"
|
||
sub={period(caInfl?.[0])}
|
||
color="#dc2626"
|
||
/>
|
||
</div>
|
||
|
||
<CountryPicker bind:selected bind:eu />
|
||
|
||
<h2 class="stats-section-title">Prices and rates</h2>
|
||
<div class="stats-grid">
|
||
<StatChart
|
||
title="Consumer prices"
|
||
subtitle={cpiMode === 'level'
|
||
? `index, ${baseYear} average = 100 · monthly`
|
||
: '% change on the same month a year earlier'}
|
||
sources={cpiSources}
|
||
attributions={a}
|
||
height="400px"
|
||
wide
|
||
note={`National CPI for the US (BLS CPI-U), Canada (Statistics Canada) and Curaçao (CBS Curaçao); BIS long series (national CPIs) for the Netherlands, Japan, South Korea, China and the added countries; EU 27 is the HICP. Each source keeps its own base year; the chart divides by the ${baseYear} average.${cpiMode === 'level' ? ' Series that do not cover the base year are left out.' : ''} ${missing(cpiLines.gaps)}`}
|
||
option={(t) =>
|
||
lineOption(t, cpiLines.lines, { unit: cpiMode === 'level' ? 'index' : '%', zoom: true })}
|
||
>
|
||
<div class="controls">
|
||
<Tabs.Root bind:value={cpiMode}>
|
||
<Tabs.List>
|
||
<Tabs.Trigger value="level">index level</Tabs.Trigger>
|
||
<Tabs.Trigger value="yoy">year on year %</Tabs.Trigger>
|
||
</Tabs.List>
|
||
</Tabs.Root>
|
||
{#if cpiMode === 'level'}
|
||
<label class="select">
|
||
base year
|
||
<select bind:value={baseYear}>
|
||
{#each BASE_YEARS as yr (yr)}<option value={yr}>{yr}</option>{/each}
|
||
</select>
|
||
</label>
|
||
{/if}
|
||
</div>
|
||
</StatChart>
|
||
<StatChart
|
||
title={`Consumer prices by category: ${nameOf(catCountry)}`}
|
||
subtitle={cpiMode === 'level'
|
||
? `index, ${baseYear} average = 100 · monthly`
|
||
: '% change on the same month a year earlier'}
|
||
sources={[CPI_BY_CATEGORY[catCountry]?.source ?? 'eurostat']}
|
||
attributions={a}
|
||
height="380px"
|
||
wide
|
||
note={`${CPI_BY_CATEGORY[catCountry]?.note ?? ''}.${catMissing.length ? ` Not published here: ${catMissing.join(', ')}.` : ''} Category definitions differ a little per source (Canada's housing is shelter; the HICP and OECD use COICOP 04, housing including utilities).`}
|
||
option={(t) =>
|
||
lineOption(t, catLines, { unit: cpiMode === 'level' ? 'index' : '%', zoom: true })}
|
||
>
|
||
<div class="controls">
|
||
<label class="select">
|
||
country
|
||
<select bind:value={catCountry}>
|
||
{#each Object.keys(CPI_BY_CATEGORY) as c (c)}<option value={c}>{nameOf(c)}</option>{/each}
|
||
</select>
|
||
</label>
|
||
<span class="hint">uses the index / year-on-year switch and base year above</span>
|
||
</div>
|
||
</StatChart>
|
||
<StatChart
|
||
title="Central bank policy rates"
|
||
subtitle="% · monthly · BIS"
|
||
sources={['bis']}
|
||
attributions={a}
|
||
note={`Euro members follow the ECB. ${missing(rateLines.gaps)}${rateLines.gaps.includes('CUW') ? ' Curaçao: the CBCS pegs the guilder to the US dollar and is not in the BIS policy rate set.' : ''}`}
|
||
option={(t) => lineOption(t, rateLines.lines, { unit: '%', zoom: true })}
|
||
/>
|
||
<StatChart
|
||
title="10-year government bond yields"
|
||
subtitle="% · monthly · OECD long-term interest rates"
|
||
sources={['oecd']}
|
||
attributions={a}
|
||
note={missing(yieldLines.gaps)}
|
||
option={(t) => lineOption(t, yieldLines.lines, { unit: '%', zoom: true })}
|
||
/>
|
||
</div>
|
||
|
||
<h2 class="stats-section-title">Growth, jobs and debt</h2>
|
||
<div class="stats-grid">
|
||
<StatChart
|
||
title="Real GDP growth"
|
||
subtitle="% change on the previous year · outcomes only, no forecasts"
|
||
sources={growth.sources}
|
||
attributions={a}
|
||
note={`IMF World Economic Outlook; CBS Curaçao (real growth, 2011→) for Curaçao. ${missing(growth.gaps)}`}
|
||
option={(t) => lineOption(t, growth.lines, { unit: '%', zoom: true })}
|
||
/>
|
||
<StatChart
|
||
title="Unemployment rate"
|
||
subtitle="% of the labour force · annual"
|
||
sources={unemployment.sources}
|
||
attributions={a}
|
||
note={`IMF WEO; CBS Curaçao labour force survey (2014→) for Curaçao. ${missing(unemployment.gaps)}`}
|
||
option={(t) => lineOption(t, unemployment.lines, { unit: '%', zoom: true })}
|
||
/>
|
||
<StatChart
|
||
title="Government debt"
|
||
subtitle="general government gross debt, % of GDP · annual"
|
||
sources={govDebt.sources}
|
||
attributions={a}
|
||
note={`IMF WEO; World Bank central government debt where the IMF has none. ${missing(govDebt.gaps)}`}
|
||
option={(t) => lineOption(t, govDebt.lines, { unit: '% of GDP', zoom: true })}
|
||
/>
|
||
<StatChart
|
||
title="Trade openness"
|
||
subtitle="exports + imports of goods and services, % of GDP · annual"
|
||
sources={trade.sources}
|
||
attributions={a}
|
||
note={missing(trade.gaps)}
|
||
option={(t) => lineOption(t, trade.lines, { unit: '% of GDP', zoom: true })}
|
||
/>
|
||
</div>
|
||
|
||
<h2 class="stats-section-title">Bond and stock markets</h2>
|
||
<div class="stats-grid">
|
||
<StatChart
|
||
title="Spreads over Germany"
|
||
subtitle="percentage points · 10-year yield minus the German Bund"
|
||
sources={['eurostat']}
|
||
attributions={a}
|
||
option={(t) =>
|
||
lineOption(
|
||
t,
|
||
[
|
||
{ name: 'France − Germany', points: frde, color: '#3b82f6', kind: 'area' },
|
||
{ name: 'Italy − Germany', points: itde, color: '#10b981' },
|
||
{ name: 'Netherlands − Germany', points: nlde, color: '#f97316' }
|
||
],
|
||
{ unit: 'pp', zoom: true }
|
||
)}
|
||
/>
|
||
<StatChart
|
||
title="US yield curve: 10-year minus 2-year"
|
||
subtitle="percentage points · monthly average · below zero = inverted"
|
||
sources={['fred']}
|
||
attributions={a}
|
||
option={(t) =>
|
||
lineOption(t, [{ name: '10y − 2y', points: curve, color: '#a855f7', kind: 'area' }], {
|
||
unit: 'pp',
|
||
zoom: true,
|
||
legend: false
|
||
})}
|
||
/>
|
||
<StatChart
|
||
title="Share prices"
|
||
subtitle="index 2015 = 100 · OECD main economic indicators"
|
||
sources={['fred']}
|
||
attributions={a}
|
||
option={(t) =>
|
||
lineOption(
|
||
t,
|
||
[
|
||
{ name: 'Netherlands', points: pts('fred:SPASTT01NLM661N'), color: '#f97316' },
|
||
{ name: 'United States', points: pts('fred:SPASTT01USM661N'), color: '#3b82f6' },
|
||
{ name: 'South Korea', points: pts('fred:SPASTT01KRM661N'), color: '#e11d48' },
|
||
{ name: 'China', points: pts('fred:SPASTT01CNM661N'), color: '#eab308' }
|
||
],
|
||
{ zoom: true }
|
||
)}
|
||
/>
|
||
<StatChart
|
||
title="S&P 500 and NASDAQ Composite"
|
||
subtitle="index points · weekly average"
|
||
sources={['fred']}
|
||
attributions={a}
|
||
wide
|
||
option={(t) =>
|
||
lineOption(
|
||
t,
|
||
[
|
||
{ name: 'S&P 500', points: pts('fred:SP500'), color: '#3b82f6' },
|
||
{ name: 'NASDAQ Composite', points: pts('fred:NASDAQCOM'), color: '#10b981', right: true }
|
||
],
|
||
{ zoom: true, unit: 'S&P', rightUnit: 'NASDAQ' }
|
||
)}
|
||
/>
|
||
</div>
|
||
|
||
<h2 class="stats-section-title">United States</h2>
|
||
<div class="stats-grid">
|
||
<StatChart
|
||
title="US real GDP and unemployment"
|
||
subtitle="billions of chained 2017 USD (SAAR) · unemployment %"
|
||
sources={['fred']}
|
||
attributions={a}
|
||
option={(t) =>
|
||
lineOption(
|
||
t,
|
||
[
|
||
{ name: 'Real GDP', points: pts('fred:GDPC1'), color: '#3b82f6', kind: 'area' },
|
||
{ name: 'Unemployment rate', points: pts('fred:UNRATE'), color: '#e11d48', right: true }
|
||
],
|
||
{ unit: 'USD bn', rightUnit: '%', zoom: true }
|
||
)}
|
||
/>
|
||
</div>
|
||
|
||
<h2 class="stats-section-title">Europe: public finances and migration</h2>
|
||
<div class="stats-grid">
|
||
<StatChart
|
||
title="Government debt"
|
||
subtitle={`% of GDP · ${debt[0]?.year ?? ''}`}
|
||
sources={['eurostat']}
|
||
attributions={a}
|
||
height="360px"
|
||
option={(t) => barRankOption(t, debt, { unit: '% of GDP', color: '#a855f7' })}
|
||
/>
|
||
<StatChart
|
||
title="Government balance"
|
||
subtitle={`surplus (+) or deficit (−), % of GDP · ${deficit[0]?.year ?? ''}`}
|
||
sources={['eurostat']}
|
||
attributions={a}
|
||
height="360px"
|
||
option={(t) => barRankOption(t, deficit, { unit: '% of GDP', color: '#10b981' })}
|
||
/>
|
||
<StatChart
|
||
title="Dutch government: spending vs revenue"
|
||
subtitle="% of GDP · general government"
|
||
sources={['eurostat']}
|
||
attributions={a}
|
||
option={(t) =>
|
||
lineOption(
|
||
t,
|
||
[
|
||
{
|
||
name: 'Spending',
|
||
points: pts('eurostat:gov_10a_main:PC_GDP.S13.TE.NL'),
|
||
color: '#e11d48'
|
||
},
|
||
{
|
||
name: 'Revenue',
|
||
points: pts('eurostat:gov_10a_main:PC_GDP.S13.TR.NL'),
|
||
color: '#10b981'
|
||
},
|
||
{
|
||
name: 'EU 27 spending',
|
||
points: pts('eurostat:gov_10a_main:PC_GDP.S13.TE.EU27_2020'),
|
||
color: '#e11d48',
|
||
dashed: true
|
||
},
|
||
{
|
||
name: 'EU 27 revenue',
|
||
points: pts('eurostat:gov_10a_main:PC_GDP.S13.TR.EU27_2020'),
|
||
color: '#10b981',
|
||
dashed: true
|
||
}
|
||
],
|
||
{ unit: '% of GDP', min: 'dataMin' }
|
||
)}
|
||
/>
|
||
<StatChart
|
||
title="Immigration"
|
||
subtitle="people arriving per year"
|
||
sources={['eurostat']}
|
||
attributions={a}
|
||
option={(t) =>
|
||
lineOption(
|
||
t,
|
||
[
|
||
{
|
||
name: 'Netherlands',
|
||
points: pts('eurostat:migr_imm1ctz:TOTAL.COMPLET.TOTAL.NR.T.NL'),
|
||
color: '#f97316',
|
||
kind: 'area'
|
||
},
|
||
{
|
||
name: 'Belgium',
|
||
points: pts('eurostat:migr_imm1ctz:TOTAL.COMPLET.TOTAL.NR.T.BE'),
|
||
color: '#eab308'
|
||
},
|
||
{
|
||
name: 'Sweden',
|
||
points: pts('eurostat:migr_imm1ctz:TOTAL.COMPLET.TOTAL.NR.T.SE'),
|
||
color: '#3b82f6'
|
||
},
|
||
{
|
||
name: 'Austria',
|
||
points: pts('eurostat:migr_imm1ctz:TOTAL.COMPLET.TOTAL.NR.T.AT'),
|
||
color: '#e11d48'
|
||
}
|
||
],
|
||
{ decimals: 0 }
|
||
)}
|
||
/>
|
||
<StatChart
|
||
title="Immigration per 1,000 inhabitants"
|
||
subtitle="latest year"
|
||
sources={['eurostat']}
|
||
attributions={a}
|
||
height="340px"
|
||
option={(t) => barRankOption(t, migrationPerK, { unit: 'per 1,000', color: '#06b6d4' })}
|
||
/>
|
||
</div>
|
||
<p class="stats-note">
|
||
Policy rates: BIS central bank policy rates (month end). Spreads use Eurostat's EMU
|
||
convergence-criterion yields. The country picker drives the charts in the first two sections; the
|
||
European and US sections keep their own fixed sets.
|
||
</p>
|
||
|
||
<style>
|
||
.controls {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
}
|
||
.select {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
font-size: 0.8rem;
|
||
color: var(--site-muted);
|
||
}
|
||
.select select {
|
||
border: 1px solid var(--site-border);
|
||
border-radius: 0.5rem;
|
||
background: var(--site-bg);
|
||
color: var(--site-text);
|
||
padding: 0.2rem 0.4rem;
|
||
}
|
||
.hint {
|
||
font-size: 0.75rem;
|
||
color: var(--site-muted);
|
||
}
|
||
</style>
|