stats: place × theme matrix (/stats/<place>/<theme>), sticky place/theme selector, generic metric engine with comparisons, 301s from the old section URLs
All checks were successful
Deploy production / deploy (push) Successful in 1m19s
All checks were successful
Deploy production / deploy (push) Successful in 1m19s
This commit is contained in:
170
src/lib/stats/views/CountriesView.svelte
Normal file
170
src/lib/stats/views/CountriesView.svelte
Normal file
@@ -0,0 +1,170 @@
|
||||
<script lang="ts">
|
||||
import type { CountriesViewData } 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 { barRankOption, lineOption, type StatPoint } from '#lib/stats/chart.js';
|
||||
import { compact, num } from '#lib/stats/format.js';
|
||||
import { COUNTRY } from '#lib/stats/regions.js';
|
||||
import { COMPARE_COLUMNS, COUNTRY_COLOR, FLAG } from '#lib/stats/countries.js';
|
||||
|
||||
let { data }: { data: CountriesViewData & StatsCommon } = $props();
|
||||
const s = $derived(data.series);
|
||||
const a = $derived(data.attributions);
|
||||
const pts = (id: string): StatPoint[] => s[id]?.points ?? [];
|
||||
const name = (c: string) => COUNTRY[c] ?? c;
|
||||
const HIGHLIGHT = ['NLD', 'CUW', 'USA', 'KOR', 'CHN', 'WLD'];
|
||||
|
||||
function show(v: { value: number } | null, fmt: string) {
|
||||
if (!v) return '–';
|
||||
if (fmt === 'compact') return compact(v.value);
|
||||
if (fmt === 'usd') return `$${compact(v.value)}`;
|
||||
if (fmt === 'years') return num(v.value, 1);
|
||||
return `${num(v.value, 1)}%`;
|
||||
}
|
||||
const rank = (key: string) =>
|
||||
data.rows
|
||||
.filter((r) => r.values[key])
|
||||
.map((r) => ({
|
||||
label: `${FLAG[r.code] ?? ''} ${name(r.code)}`,
|
||||
value: r.values[key]!.value,
|
||||
highlight: r.code === 'NLD' || r.code === 'CUW'
|
||||
}));
|
||||
const gdpLines = $derived(
|
||||
HIGHLIGHT.map((c) => ({
|
||||
name: name(c),
|
||||
points: pts(`wb:NY.GDP.PCAP.PP.KD:${c}`),
|
||||
color: COUNTRY_COLOR[c]
|
||||
})).filter((l) => l.points.length)
|
||||
);
|
||||
const lifeLines = $derived(
|
||||
HIGHLIGHT.map((c) => ({
|
||||
name: name(c),
|
||||
points: pts(`wb:SP.DYN.LE00.IN:${c}`),
|
||||
color: COUNTRY_COLOR[c]
|
||||
})).filter((l) => l.points.length)
|
||||
);
|
||||
const newestYear = (key: string) =>
|
||||
data.rows.reduce((m, r) => {
|
||||
const p = r.values[key]?.period;
|
||||
return p && p > m ? p : m;
|
||||
}, '');
|
||||
</script>
|
||||
|
||||
<section class="stats-intro">
|
||||
<span class="stats-eyebrow">stats / countries</span>
|
||||
<h1>Countries</h1>
|
||||
<p>
|
||||
The places I compare most, side by side. Latest actual values (no forecasts) from the World Bank
|
||||
and the IMF World Economic Outlook; the IMF is used where it covers a country. Click a country
|
||||
for its full profile.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Country</th>
|
||||
{#each COMPARE_COLUMNS as col (col.key)}<th class="num">{col.label}</th>{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.rows as r (r.code)}
|
||||
<tr class:hl={r.code === 'NLD' || r.code === 'CUW'}>
|
||||
<td>
|
||||
<a
|
||||
class="country-link"
|
||||
href={resolve(`/stats/${r.code.toLowerCase()}/overview` as '/stats')}
|
||||
><span class="flag">{FLAG[r.code]}</span>{name(r.code)}</a
|
||||
>
|
||||
</td>
|
||||
{#each COMPARE_COLUMNS as col (col.key)}
|
||||
{@const v = r.values[col.key]}
|
||||
<td class="num" title={v ? `${v.period.slice(0, 4)} · ${v.source}` : ''}
|
||||
>{show(v, col.fmt)}<small>{v ? ` '${v.period.slice(2, 4)}` : ''}</small></td
|
||||
>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="stats-note">
|
||||
Small numbers after each value are the year it refers to. GDP per capita in constant 2021
|
||||
international dollars (PPP). Growth, inflation, unemployment and debt from the IMF where
|
||||
available, otherwise the World Bank.
|
||||
</p>
|
||||
|
||||
<div class="stats-grid">
|
||||
<StatChart
|
||||
title="GDP per capita, PPP"
|
||||
subtitle={`constant 2021 international $ · ${newestYear('gdppc').slice(0, 4)}`}
|
||||
sources={['worldbank']}
|
||||
attributions={a}
|
||||
height="420px"
|
||||
option={(t) => barRankOption(t, rank('gdppc'), { decimals: 0, unit: '$', color: '#10b981' })}
|
||||
/>
|
||||
<StatChart
|
||||
title="Life expectancy at birth"
|
||||
subtitle={`years · ${newestYear('life').slice(0, 4)}`}
|
||||
sources={['worldbank']}
|
||||
attributions={a}
|
||||
height="420px"
|
||||
option={(t) => barRankOption(t, rank('life'), { decimals: 1, unit: 'yr', color: '#a855f7' })}
|
||||
/>
|
||||
<StatChart
|
||||
title="GDP per capita over time"
|
||||
subtitle="PPP, constant 2021 international $"
|
||||
sources={['worldbank']}
|
||||
attributions={a}
|
||||
option={(t) => lineOption(t, gdpLines, { unit: '$', decimals: 0 })}
|
||||
/>
|
||||
<StatChart
|
||||
title="Life expectancy since 1960"
|
||||
subtitle="years at birth"
|
||||
sources={['worldbank']}
|
||||
attributions={a}
|
||||
option={(t) => lineOption(t, lifeLines, { unit: 'years', decimals: 1, min: 'dataMin' })}
|
||||
/>
|
||||
<StatChart
|
||||
title="Government debt"
|
||||
subtitle="% of GDP · IMF where available, else World Bank (central government)"
|
||||
sources={['imf', 'worldbank']}
|
||||
attributions={a}
|
||||
height="420px"
|
||||
option={(t) => barRankOption(t, rank('debt'), { decimals: 0, unit: '%', color: '#e11d48' })}
|
||||
/>
|
||||
<StatChart
|
||||
title="Inflation"
|
||||
subtitle="% consumer prices, latest actual year"
|
||||
sources={['imf', 'worldbank']}
|
||||
attributions={a}
|
||||
height="420px"
|
||||
option={(t) => barRankOption(t, rank('infl'), { decimals: 1, unit: '%', color: '#f97316' })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.country-link {
|
||||
display: inline-flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
color: var(--site-text);
|
||||
text-decoration: none;
|
||||
font-weight: 550;
|
||||
}
|
||||
.country-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.flag {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
tr.hl td {
|
||||
background: color-mix(in srgb, var(--site-text) 4%, transparent);
|
||||
}
|
||||
td small {
|
||||
color: var(--site-muted);
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user