Files
website/src/routes/stats/opinions/world/+page.svelte

147 lines
5.3 KiB
Svelte

<script lang="ts">
import { NativeSelect } from '#lib/components/ui/native-select/index.js';
import './world-opinions.css';
import CountryPanel from '#lib/world-opinions/CountryPanel.svelte';
import CultureMap from '#lib/world-opinions/CultureMap.svelte';
import PartyCompass from '#lib/world-opinions/PartyCompass.svelte';
import WorldMap, { type MapMode } from '#lib/world-opinions/WorldMap.svelte';
import { buildWorldOpinions, provideWorldOpinions } from '#lib/world-opinions/data.js';
import { Button } from '#lib/components/ui/button/index.js';
import { resolve } from '$app/paths';
let { data } = $props();
// Static per load (stats.wo_*), so it is read once and shared with the components.
const wo = provideWorldOpinions(buildWorldOpinions(data.wo));
const { countries, countryById, narratives, sources } = wo;
const modes: { id: MapMode; label: string }[] = [
{ id: 'liberal', label: 'what "liberal" means' },
{ id: 'divide', label: 'what left vs right is about' },
{ id: 'tradition', label: 'traditional ↔ secular' },
{ id: 'expression', label: 'survival ↔ self-expression' }
];
const sorted = [...countries].sort((a, b) => a.name.localeCompare(b.name));
let mode = $state<MapMode>('liberal');
let countryId = $state('us');
let eventIndex = $state(0);
let narrative = $derived(narratives[eventIndex]);
function select(id: string) {
countryId = id;
document.getElementById('country')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
</script>
<svelte:head>
<title>world opinions · stats · Stan Runge</title>
<meta
name="description"
content="How political words, parties, values and national histories differ across 57 countries: what liberal means, what left and right are about, a shared party compass and a values map."
/>
</svelte:head>
<section class="stats-intro">
<span class="stats-eyebrow">stats / opinions / world</span>
<h1>World opinions</h1>
<p>
Same words, different politics. Hover the map, click a country. For measured party positions
(Chapel Hill Expert Survey, V-Party) see <a href={resolve('/stats/opinions') + '#parties'}
>where Dutch parties stand</a
>; for polls, happiness and Eurobarometer see <a href={resolve('/stats/opinions')}>opinions</a>.
</p>
</section>
<div class="wo">
<section aria-label="World map">
<div class="mb-4 flex flex-wrap gap-2" role="tablist" aria-label="Map layer">
{#each modes as item (item.id)}
<Button
variant={mode === item.id ? 'secondary' : 'outline'}
size="sm"
role="tab"
aria-selected={mode === item.id}
onclick={() => (mode = item.id)}>{item.label}</Button
>
{/each}
</div>
<WorldMap {mode} selected={countryId} onselect={select} />
</section>
<section id="country" class="scroll-mt-20 pt-10" aria-label="Country">
<label class="mb-3 block max-w-xs text-sm">
<span class="sr-only">Country</span>
<NativeSelect
bind:value={countryId}
class="w-full rounded-md border border-[var(--site-border)] bg-[var(--site-surface)] px-3 py-2"
>
{#each sorted as country (country.id)}
<option value={country.id}>{country.name}</option>
{/each}
</NativeSelect>
</label>
<CountryPanel id={countryId} />
</section>
<section class="mt-12 border-t border-[var(--site-border)] py-10">
<h2 class="stats-section-title mb-1">Party compass</h2>
<p class="mb-5 text-[var(--site-muted)]">
Every party on one shared scale, hand-placed for comparison. Measured CHES and V-Party
positions for Dutch parties are on <a
class="underline underline-offset-2"
href={resolve('/stats/opinions') + '#parties'}>opinions</a
>.
</p>
<PartyCompass focus={countryId} />
</section>
<section class="border-t border-[var(--site-border)] py-10">
<h2 class="stats-section-title mb-1">Values map</h2>
<p class="mb-5 text-[var(--site-muted)]">Both value axes at once.</p>
<CultureMap selected={countryId} onselect={select} />
</section>
<section class="border-t border-[var(--site-border)] py-10">
<h2 class="stats-section-title mb-4">One event, many stories</h2>
<div class="mb-5 flex flex-wrap gap-2" role="tablist" aria-label="Event">
{#each narratives as item, index (item.event)}
<Button
variant={eventIndex === index ? 'secondary' : 'outline'}
size="sm"
role="tab"
aria-selected={eventIndex === index}
onclick={() => (eventIndex = index)}
>{item.event.toLowerCase()} <span class="opacity-60">{item.years}</span></Button
>
{/each}
</div>
<div role="tabpanel">
<ul class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{#each narrative.views as view (view.country)}
<li>
<button class="wo-card wo-quote w-full text-left" onclick={() => select(view.country)}>
<span class="text-sm text-[var(--site-muted)]"
>{countryById.get(view.country)?.name}</span
>
<span class="block text-lg">“{view.story}”</span>
</button>
</li>
{/each}
</ul>
</div>
</section>
<footer class="border-t border-[var(--site-border)] pt-6 text-sm text-[var(--site-muted)]">
<p class="mb-2">Approximate, hand-placed positions for comparison. Based on:</p>
<p class="flex flex-wrap gap-x-4 gap-y-1">
{#each sources as source (source.href)}
<a
class="underline underline-offset-2 hover:text-[var(--site-text)]"
href={source.href}
rel="external">{source.name}</a
>
{/each}
</p>
</footer>
</div>