All checks were successful
Deploy production / deploy (push) Successful in 1m12s
137 lines
4.3 KiB
TypeScript
137 lines
4.3 KiB
TypeScript
// World Opinions: a hand-curated orientation guide, not a dataset of record.
|
||
// Positions are approximate and meant for comparing countries/parties relative
|
||
// to each other. The curated content (countries, party placements, profiles,
|
||
// narratives, sources) lives in the `stats.wo_*` tables (seeded by the
|
||
// stats_wo_seed migration) and is loaded by +page.server.ts; this file keeps
|
||
// the types, the label dictionaries and the lookups the components share.
|
||
import { getContext, setContext } from 'svelte';
|
||
|
||
export type RegionId =
|
||
| 'north-america'
|
||
| 'western-europe'
|
||
| 'nordics'
|
||
| 'eastern-europe'
|
||
| 'east-asia'
|
||
| 'south-asia'
|
||
| 'southeast-asia'
|
||
| 'latin-america'
|
||
| 'middle-east'
|
||
| 'africa'
|
||
| 'oceania';
|
||
|
||
export const regions: Record<RegionId, string> = {
|
||
'north-america': 'North America',
|
||
'western-europe': 'Western Europe',
|
||
nordics: 'Nordics',
|
||
'eastern-europe': 'Central & Eastern Europe',
|
||
'east-asia': 'East Asia',
|
||
'south-asia': 'South Asia',
|
||
'southeast-asia': 'Southeast Asia',
|
||
'latin-america': 'Latin America',
|
||
'middle-east': 'Middle East & North Africa',
|
||
africa: 'Sub-Saharan Africa',
|
||
oceania: 'Oceania'
|
||
};
|
||
|
||
// What a local means by "liberal".
|
||
export type LiberalMeaning = 'left' | 'market' | 'western';
|
||
export const liberalMeanings: Record<LiberalMeaning, string> = {
|
||
left: 'left-leaning, progressive',
|
||
market: 'free-market, often center-right',
|
||
western: 'pro-Western opposition to nationalists'
|
||
};
|
||
|
||
// What the main political divide is mostly about.
|
||
export type Divide = 'economy' | 'culture' | 'history' | 'none';
|
||
export const divides: Record<Divide, string> = {
|
||
economy: 'economy (taxes, welfare, unions)',
|
||
culture: 'culture (religion, identity, values)',
|
||
history: 'history & geopolitics',
|
||
none: 'no open party competition'
|
||
};
|
||
|
||
export type Country = {
|
||
id: string;
|
||
/** ISO 3166-1 numeric, as used by world-atlas */
|
||
iso: string;
|
||
name: string;
|
||
region: RegionId;
|
||
/**
|
||
* Inglehart–Welzel position. x: survival (-) ↔ self-expression (+);
|
||
* y: traditional (-) ↔ secular-rational (+). Undefined = not surveyed.
|
||
*/
|
||
values?: { x: number; y: number };
|
||
liberal?: LiberalMeaning;
|
||
divide?: Divide;
|
||
/** One line shown on hover, only where the label alone would mislead. */
|
||
note?: string;
|
||
};
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Party compass: parties on one shared scale
|
||
// econ: 0 = far left (state-led, redistributive) … 10 = far right (free market)
|
||
// social: 0 = progressive/libertarian … 10 = traditional/authoritarian
|
||
// Hand-placed approximations inspired by the Chapel Hill Expert Survey and
|
||
// Manifesto Project; good for relative position, not for decimals.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export type Party = {
|
||
country: string;
|
||
short: string;
|
||
name: string;
|
||
econ: number;
|
||
social: number;
|
||
note?: string;
|
||
};
|
||
|
||
// Country profiles, kept short enough to scan.
|
||
export type Profile = {
|
||
id: string;
|
||
/** What "normal" politics looks like, in one line. */
|
||
center: string;
|
||
fights: string[];
|
||
timeline: { year: string; label: string }[];
|
||
blindSpots: string[];
|
||
};
|
||
|
||
// One event, many stories.
|
||
export type Narrative = {
|
||
event: string;
|
||
years: string;
|
||
views: { country: string; story: string }[];
|
||
};
|
||
|
||
export type Source = { name: string; href: string };
|
||
|
||
/** What the page loads: plain arrays in display order. */
|
||
export type WorldOpinionsData = {
|
||
countries: Country[];
|
||
parties: Party[];
|
||
profiles: Profile[];
|
||
narratives: Narrative[];
|
||
sources: Source[];
|
||
};
|
||
|
||
/** The loaded data plus the lookups the components use. */
|
||
export type WorldOpinions = WorldOpinionsData & {
|
||
countryById: Map<string, Country>;
|
||
profileById: Map<string, Profile>;
|
||
/** Countries with compass parties, in the order their parties are listed. */
|
||
partyCountries: string[];
|
||
};
|
||
|
||
export function buildWorldOpinions(data: WorldOpinionsData): WorldOpinions {
|
||
return {
|
||
...data,
|
||
countryById: new Map(data.countries.map((country) => [country.id, country])),
|
||
profileById: new Map(data.profiles.map((profile) => [profile.id, profile])),
|
||
partyCountries: [...new Set(data.parties.map((party) => party.country))]
|
||
};
|
||
}
|
||
|
||
const KEY = Symbol('world-opinions');
|
||
|
||
/** Called once by the page; the map, panels and compass read it with `worldOpinions()`. */
|
||
export const provideWorldOpinions = (data: WorldOpinions) => setContext(KEY, data);
|
||
export const worldOpinions = () => getContext<WorldOpinions>(KEY);
|