stats step 3: elections, party positions and opinion feeds (schema, collectors, timers); world-opinions data moved into stats.wo_*
All checks were successful
Deploy production / deploy (push) Successful in 1m12s
All checks were successful
Deploy production / deploy (push) Successful in 1m12s
This commit is contained in:
@@ -467,3 +467,280 @@ export const statsCwCbcsBop = statsSchema.table(
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.period, t.item] })]
|
||||
);
|
||||
|
||||
// ── Step 3: elections (A14) ──────────────────────────────────────────────────────────────
|
||||
|
||||
/** One election: NL Tweede Kamer (Kiesraad, CBS 37278) or US presidential (MIT Election Lab). */
|
||||
export const statsElecElection = statsSchema.table('elec_election', {
|
||||
/** NL-TK-2025, US-PRES-2024, … */
|
||||
electionId: text('election_id').primaryKey(),
|
||||
/** ISO 3166-1 alpha-3. */
|
||||
country: char('country', { length: 3 }).notNull(),
|
||||
/** TK (Tweede Kamer), PRES (US president). */
|
||||
kind: text('kind').notNull(),
|
||||
date: date('date', { mode: 'string' }).notNull(),
|
||||
name: text('name').notNull(),
|
||||
sourceId: text('source_id').notNull(),
|
||||
totalSeats: integer('total_seats')
|
||||
});
|
||||
|
||||
export const statsElecResult = statsSchema.table(
|
||||
'elec_result',
|
||||
{
|
||||
electionId: text('election_id').notNull(),
|
||||
/** national, province (kieskring not used), muni, station, state. */
|
||||
level: text('level').notNull(),
|
||||
/** NL / GM0513 / GM0513-SB101 / TX … */
|
||||
areaCode: text('area_code').notNull(),
|
||||
areaName: text('area_name'),
|
||||
/** List name as the source spells it (normalised to a short label on the page). */
|
||||
party: text('party').notNull(),
|
||||
votes: bigint('votes', { mode: 'number' }),
|
||||
seats: integer('seats')
|
||||
},
|
||||
(t) => [
|
||||
primaryKey({ columns: [t.electionId, t.level, t.areaCode, t.party] }),
|
||||
index('elec_result_area_idx').on(t.level, t.areaCode)
|
||||
]
|
||||
);
|
||||
|
||||
export const statsElecTurnout = statsSchema.table(
|
||||
'elec_turnout',
|
||||
{
|
||||
electionId: text('election_id').notNull(),
|
||||
level: text('level').notNull(),
|
||||
areaCode: text('area_code').notNull(),
|
||||
areaName: text('area_name'),
|
||||
/** Persons entitled to vote (kiesgerechtigden). */
|
||||
electorate: bigint('electorate', { mode: 'number' }),
|
||||
/** Ballots cast / counted (opkomst). */
|
||||
ballots: bigint('ballots', { mode: 'number' }),
|
||||
valid: bigint('valid', { mode: 'number' }),
|
||||
blank: bigint('blank', { mode: 'number' }),
|
||||
invalid: bigint('invalid', { mode: 'number' })
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.electionId, t.level, t.areaCode] })]
|
||||
);
|
||||
|
||||
// ── Step 3: world opinions (§4, A15) ─────────────────────────────────────────────────────
|
||||
// The /projects/world-opinions guide reads these. Curated rows (seeded from the former
|
||||
// src/lib/world-opinions/data.ts) keep Stan's text exactly; collectors add CHES / V-Party data.
|
||||
|
||||
export const statsWoCountry = statsSchema.table('wo_country', {
|
||||
iso3: char('iso3', { length: 3 }).primaryKey(),
|
||||
/** ISO 3166-1 alpha-2, lower case (the guide's own id). */
|
||||
id: text('id').notNull().unique(),
|
||||
/** ISO 3166-1 numeric as a zero-padded string (world-atlas shape id). */
|
||||
isoNum: text('iso_num').notNull(),
|
||||
name: text('name').notNull(),
|
||||
/** One of the 11 RegionId keys in src/lib/world-opinions/data.ts. */
|
||||
region: text('region').notNull(),
|
||||
/** left | market | western */
|
||||
liberalMeaning: text('liberal_meaning'),
|
||||
/** economy | culture | history | none */
|
||||
mainDivide: text('main_divide'),
|
||||
note: text('note'),
|
||||
/** Country profile {center, fights[], timeline[{year,label}], blindSpots[]}, or null. */
|
||||
profile: jsonb('profile'),
|
||||
sort: integer('sort').notNull()
|
||||
});
|
||||
|
||||
/** Inglehart–Welzel map positions; the guide's hand-placed values are wave 'curated'. */
|
||||
export const statsWoCulturalMap = statsSchema.table(
|
||||
'wo_cultural_map',
|
||||
{
|
||||
iso3: char('iso3', { length: 3 }).notNull(),
|
||||
wave: text('wave').notNull(),
|
||||
yearFrom: integer('year_from'),
|
||||
yearTo: integer('year_to'),
|
||||
/** y axis: traditional (-) ↔ secular-rational (+). */
|
||||
tradSecular: doublePrecision('trad_secular'),
|
||||
/** x axis: survival (-) ↔ self-expression (+). */
|
||||
survSelfexp: doublePrecision('surv_selfexp'),
|
||||
n: integer('n'),
|
||||
source: text('source')
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.iso3, t.wave] })]
|
||||
);
|
||||
|
||||
/** WVS/EVS/Pew item aggregates (Tier C manual loads; empty until then). */
|
||||
export const statsWoValueScore = statsSchema.table(
|
||||
'wo_value_score',
|
||||
{
|
||||
iso3: char('iso3', { length: 3 }).notNull(),
|
||||
wave: text('wave').notNull(),
|
||||
itemCode: text('item_code').notNull(),
|
||||
itemLabel: text('item_label'),
|
||||
stat: text('stat').notNull(),
|
||||
value: doublePrecision('value'),
|
||||
n: integer('n')
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.iso3, t.wave, t.itemCode, t.stat] })]
|
||||
);
|
||||
|
||||
export const statsWoNarrative = statsSchema.table('wo_narrative', {
|
||||
sort: integer('sort').primaryKey(),
|
||||
event: text('event').notNull(),
|
||||
years: text('years').notNull(),
|
||||
/** [{country (alpha-2), story}] in display order. */
|
||||
views: jsonb('views').notNull()
|
||||
});
|
||||
|
||||
export const statsWoSource = statsSchema.table('wo_source', {
|
||||
sort: integer('sort').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
href: text('href').notNull()
|
||||
});
|
||||
|
||||
/**
|
||||
* Parties. Curated guide parties: party_id `wo:<cc>:<short>`; data-driven parties:
|
||||
* `pf:<partyfacts_id>` (or `ches:<id>` / `vp:<id>` when Party Facts has no link).
|
||||
*/
|
||||
export const statsWoParty = statsSchema.table(
|
||||
'wo_party',
|
||||
{
|
||||
partyId: text('party_id').primaryKey(),
|
||||
iso3: char('iso3', { length: 3 }),
|
||||
name: text('name'),
|
||||
abbrev: text('abbrev'),
|
||||
family: text('family'),
|
||||
chesId: integer('ches_id'),
|
||||
manifestoId: integer('manifesto_id'),
|
||||
vpartyId: integer('vparty_id'),
|
||||
partyfactsId: integer('partyfacts_id'),
|
||||
/** Display order of curated parties (null for data-driven rows). */
|
||||
sort: integer('sort')
|
||||
},
|
||||
(t) => [index('wo_party_iso3_idx').on(t.iso3)]
|
||||
);
|
||||
|
||||
export const statsWoPartyPosition = statsSchema.table(
|
||||
'wo_party_position',
|
||||
{
|
||||
partyId: text('party_id').notNull(),
|
||||
year: integer('year').notNull(),
|
||||
/** ches, vparty */
|
||||
source: text('source').notNull(),
|
||||
/** lrgen, lrecon, galtan, eu_position, v2xpa_popul, v2pariglef, … */
|
||||
dim: text('dim').notNull(),
|
||||
value: doublePrecision('value'),
|
||||
voteShare: doublePrecision('vote_share')
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.partyId, t.year, t.source, t.dim] })]
|
||||
);
|
||||
|
||||
/** Stan's curated shared-compass placements (x = economic 0–10, y = social 0–10). */
|
||||
export const statsWoCompassOverride = statsSchema.table('wo_compass_override', {
|
||||
partyId: text('party_id').primaryKey(),
|
||||
x: doublePrecision('x').notNull(),
|
||||
y: doublePrecision('y').notNull(),
|
||||
note: text('note')
|
||||
});
|
||||
|
||||
// ── Step 3: opinion feeds (A16) ──────────────────────────────────────────────────────────
|
||||
|
||||
/** Raw Wikipedia wikitext of the poll pages, one row per revision seen. */
|
||||
export const statsOpWikiSnapshot = statsSchema.table(
|
||||
'op_wiki_snapshot',
|
||||
{
|
||||
page: text('page').notNull(),
|
||||
revid: bigint('revid', { mode: 'number' }).notNull(),
|
||||
fetchedAt: timestamptz('fetched_at').notNull().defaultNow(),
|
||||
wikitext: text('wikitext').notNull()
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.page, t.revid] })]
|
||||
);
|
||||
|
||||
/** Parsed poll rows (long format): one value per poll × option. */
|
||||
export const statsOpPoll = statsSchema.table(
|
||||
'op_poll',
|
||||
{
|
||||
/** nl-tk (seats), us-approval, us-approval-avg, us-generic, kr-approval, kr-party. */
|
||||
series: text('series').notNull(),
|
||||
/** `${fieldwork_end}|${pollster}|${n}` (n = order within that pollster and date). */
|
||||
pollKey: text('poll_key').notNull(),
|
||||
fieldworkEnd: date('fieldwork_end', { mode: 'string' }).notNull(),
|
||||
pollster: text('pollster').notNull(),
|
||||
sample: integer('sample'),
|
||||
option: text('option').notNull(),
|
||||
value: doublePrecision('value'),
|
||||
seenAt: timestamptz('seen_at').notNull().defaultNow()
|
||||
},
|
||||
(t) => [
|
||||
primaryKey({ columns: [t.series, t.pollKey, t.option] }),
|
||||
index('op_poll_series_date_idx').on(t.series, t.fieldworkEnd)
|
||||
]
|
||||
);
|
||||
|
||||
/** Standard Eurobarometer volume A: most-important-issue and trust questions per country. */
|
||||
export const statsEbItem = statsSchema.table(
|
||||
'eb_item',
|
||||
{
|
||||
/** 104.1 etc. */
|
||||
wave: text('wave').notNull(),
|
||||
fieldworkEnd: date('fieldwork_end', { mode: 'string' }),
|
||||
/** mii_country, mii_personal, mii_eu, trust */
|
||||
topic: text('topic').notNull(),
|
||||
/** Sheet name in the wave's file (QA3, QA6_9, …). */
|
||||
qcode: text('qcode').notNull(),
|
||||
/** Answer (issue) for MII; institution for trust. */
|
||||
item: text('item').notNull(),
|
||||
/** Eurobarometer country code (EU27, BE, EL, …). */
|
||||
country: text('country').notNull(),
|
||||
/** Share of respondents, 0–1. */
|
||||
share: doublePrecision('share')
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.wave, t.topic, t.item, t.country] })]
|
||||
);
|
||||
|
||||
/** World Happiness Report: life evaluation (3-year average) and its explained parts. */
|
||||
export const statsWhrScore = statsSchema.table(
|
||||
'whr_score',
|
||||
{
|
||||
year: integer('year').notNull(),
|
||||
country: text('country').notNull(),
|
||||
iso3: char('iso3', { length: 3 }),
|
||||
rank: integer('rank'),
|
||||
ladder: doublePrecision('ladder'),
|
||||
lower: doublePrecision('lower'),
|
||||
upper: doublePrecision('upper'),
|
||||
gdp: doublePrecision('gdp'),
|
||||
social: doublePrecision('social'),
|
||||
health: doublePrecision('health'),
|
||||
freedom: doublePrecision('freedom'),
|
||||
generosity: doublePrecision('generosity'),
|
||||
corruption: doublePrecision('corruption'),
|
||||
dystopia: doublePrecision('dystopia')
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.year, t.country] })]
|
||||
);
|
||||
|
||||
/** Google Trends daily trending searches (RSS snapshot). */
|
||||
export const statsGtTrending = statsSchema.table(
|
||||
'gt_trending',
|
||||
{
|
||||
geo: text('geo').notNull(),
|
||||
date: date('date', { mode: 'string' }).notNull(),
|
||||
rank: integer('rank').notNull(),
|
||||
query: text('query').notNull(),
|
||||
approxTraffic: integer('approx_traffic'),
|
||||
pubDate: timestamptz('pub_date'),
|
||||
/** First related news headline + source, for context. */
|
||||
news: text('news')
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.geo, t.date, t.query] })]
|
||||
);
|
||||
|
||||
/** GDELT DOC timelinevol: share of monitored news coverage per theme and source country. */
|
||||
export const statsGdeltThemeVol = statsSchema.table(
|
||||
'gdelt_theme_vol',
|
||||
{
|
||||
date: date('date', { mode: 'string' }).notNull(),
|
||||
/** ISO 3166-1 alpha-3 of the source country. */
|
||||
country: char('country', { length: 3 }).notNull(),
|
||||
theme: text('theme').notNull(),
|
||||
/** Percentage of all monitored articles (daily mean of GDELT's volume intensity). */
|
||||
volPct: doublePrecision('vol_pct')
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.date, t.country, t.theme] })]
|
||||
);
|
||||
|
||||
76
src/lib/server/world-opinions.ts
Normal file
76
src/lib/server/world-opinions.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { client } from '#lib/server/db/index.js';
|
||||
import { cached } from '#lib/server/stats.js';
|
||||
import type {
|
||||
Country,
|
||||
Divide,
|
||||
LiberalMeaning,
|
||||
Narrative,
|
||||
Party,
|
||||
Profile,
|
||||
RegionId,
|
||||
Source,
|
||||
WorldOpinionsData
|
||||
} from '#lib/world-opinions/data.js';
|
||||
|
||||
// /projects/world-opinions content from stats.wo_* (seeded from the former data.ts). Rebuilds
|
||||
// the exact shapes the components used, leaving optional keys out when the column is null.
|
||||
export function loadWorldOpinions(): Promise<WorldOpinionsData> {
|
||||
return cached('world-opinions', async () => {
|
||||
const [countryRows, partyRows, narrativeRows, sourceRows] = await Promise.all([
|
||||
client`
|
||||
select c.id, c.iso_num, c.name, c.region, c.liberal_meaning, c.main_divide, c.note,
|
||||
c.profile, m.surv_selfexp, m.trad_secular
|
||||
from stats.wo_country c
|
||||
left join stats.wo_cultural_map m on m.iso3 = c.iso3 and m.wave = 'curated'
|
||||
order by c.sort`,
|
||||
client`
|
||||
select c.id as country, p.abbrev, p.name, o.x, o.y, o.note
|
||||
from stats.wo_party p
|
||||
join stats.wo_compass_override o using (party_id)
|
||||
join stats.wo_country c on c.iso3 = p.iso3
|
||||
where p.sort is not null
|
||||
order by p.sort`,
|
||||
client`select event, years, views from stats.wo_narrative order by sort`,
|
||||
client`select name, href from stats.wo_source order by sort`
|
||||
]);
|
||||
const countries: Country[] = [];
|
||||
const profiles: Profile[] = [];
|
||||
for (const r of countryRows) {
|
||||
const country: Country = {
|
||||
id: r.id,
|
||||
iso: r.iso_num,
|
||||
name: r.name,
|
||||
region: r.region as RegionId
|
||||
};
|
||||
if (r.surv_selfexp != null && r.trad_secular != null)
|
||||
country.values = { x: r.surv_selfexp, y: r.trad_secular };
|
||||
if (r.liberal_meaning) country.liberal = r.liberal_meaning as LiberalMeaning;
|
||||
if (r.main_divide) country.divide = r.main_divide as Divide;
|
||||
if (r.note) country.note = r.note;
|
||||
countries.push(country);
|
||||
if (r.profile) profiles.push({ id: r.id, ...(r.profile as Omit<Profile, 'id'>) });
|
||||
}
|
||||
const parties: Party[] = partyRows.map((r) => {
|
||||
const party: Party = {
|
||||
country: r.country,
|
||||
short: r.abbrev,
|
||||
name: r.name,
|
||||
econ: r.x,
|
||||
social: r.y
|
||||
};
|
||||
if (r.note) party.note = r.note;
|
||||
return party;
|
||||
});
|
||||
return {
|
||||
countries,
|
||||
parties,
|
||||
profiles,
|
||||
narratives: narrativeRows.map((r) => ({
|
||||
event: r.event,
|
||||
years: r.years,
|
||||
views: r.views as Narrative['views']
|
||||
})),
|
||||
sources: sourceRows.map((r) => ({ name: r.name, href: r.href }) as Source)
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import EyeOff from 'lucide-svelte/icons/eye-off';
|
||||
import Swords from 'lucide-svelte/icons/swords';
|
||||
import { countryById, divides, liberalMeanings, profileById, regions } from './data';
|
||||
import { divides, liberalMeanings, regions, worldOpinions } from './data';
|
||||
|
||||
const { countryById, profileById } = worldOpinions();
|
||||
|
||||
let { id }: { id: string } = $props();
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { countries, regions, type RegionId } from './data';
|
||||
import { regions, worldOpinions, type RegionId } from './data';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
|
||||
const { countries } = worldOpinions();
|
||||
|
||||
const culturalMap = countries.flatMap((country) =>
|
||||
country.values ? [{ ...country, x: country.values.x, y: country.values.y }] : []
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { countryById, parties, partyCountries } from './data';
|
||||
import { worldOpinions } from './data';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
|
||||
const { countryById, parties, partyCountries } = worldOpinions();
|
||||
|
||||
let { focus = null }: { focus?: string | null } = $props();
|
||||
|
||||
const MAX = 3;
|
||||
|
||||
@@ -31,7 +31,9 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { countries, divides, liberalMeanings, type Country } from './data';
|
||||
import { divides, liberalMeanings, worldOpinions, type Country } from './data';
|
||||
|
||||
const { countries } = worldOpinions();
|
||||
|
||||
let {
|
||||
mode,
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import world from 'world-atlas/countries-110m.json';
|
||||
import { countries, countryById, narratives, parties, partyCountries, profiles } from './data';
|
||||
import seed from '../../../scripts/stats/world-opinions-seed.json';
|
||||
import { buildWorldOpinions, type WorldOpinionsData } from './data';
|
||||
|
||||
describe('world opinions data', () => {
|
||||
// The guide's content lives in stats.wo_*; this checks the snapshot the stats_wo_seed migration
|
||||
// was generated from (scripts/stats/world-opinions-seed.json).
|
||||
const { countries, countryById, narratives, parties, partyCountries, profiles } =
|
||||
buildWorldOpinions(seed as WorldOpinionsData);
|
||||
|
||||
describe('world opinions seed', () => {
|
||||
it('has unique country ids and ISO codes', () => {
|
||||
expect(new Set(countries.map((country) => country.id)).size).toBe(countries.length);
|
||||
expect(new Set(countries.map((country) => country.iso)).size).toBe(countries.length);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// 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. Sources are listed in `sources` at the bottom of this file.
|
||||
// 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'
|
||||
@@ -63,483 +67,6 @@ export type Country = {
|
||||
note?: string;
|
||||
};
|
||||
|
||||
export const countries: Country[] = [
|
||||
{
|
||||
id: 'se',
|
||||
iso: '752',
|
||||
name: 'Sweden',
|
||||
region: 'nordics',
|
||||
values: { x: 2.4, y: 1.8 },
|
||||
liberal: 'market',
|
||||
divide: 'economy'
|
||||
},
|
||||
{
|
||||
id: 'dk',
|
||||
iso: '208',
|
||||
name: 'Denmark',
|
||||
region: 'nordics',
|
||||
values: { x: 2.3, y: 1.2 },
|
||||
liberal: 'market',
|
||||
divide: 'economy',
|
||||
note: '"Venstre" (literally "Left") is the liberal, center-right party'
|
||||
},
|
||||
{
|
||||
id: 'no',
|
||||
iso: '578',
|
||||
name: 'Norway',
|
||||
region: 'nordics',
|
||||
values: { x: 2.1, y: 1.4 },
|
||||
liberal: 'market',
|
||||
divide: 'economy'
|
||||
},
|
||||
{
|
||||
id: 'fi',
|
||||
iso: '246',
|
||||
name: 'Finland',
|
||||
region: 'nordics',
|
||||
values: { x: 1.6, y: 1.0 },
|
||||
liberal: 'market',
|
||||
divide: 'economy'
|
||||
},
|
||||
{
|
||||
id: 'nl',
|
||||
iso: '528',
|
||||
name: 'Netherlands',
|
||||
region: 'western-europe',
|
||||
values: { x: 2.0, y: 1.1 },
|
||||
liberal: 'market',
|
||||
divide: 'economy',
|
||||
note: 'Liberal VVD is the main center-right party'
|
||||
},
|
||||
{
|
||||
id: 'be',
|
||||
iso: '056',
|
||||
name: 'Belgium',
|
||||
region: 'western-europe',
|
||||
values: { x: 1.4, y: 0.8 },
|
||||
liberal: 'market',
|
||||
divide: 'economy',
|
||||
note: 'Language (Flemish vs French-speaking) splits every party family'
|
||||
},
|
||||
{
|
||||
id: 'de',
|
||||
iso: '276',
|
||||
name: 'Germany',
|
||||
region: 'western-europe',
|
||||
values: { x: 1.3, y: 1.4 },
|
||||
liberal: 'market',
|
||||
divide: 'economy',
|
||||
note: 'Liberal FDP = pro-market, low taxes'
|
||||
},
|
||||
{
|
||||
id: 'at',
|
||||
iso: '040',
|
||||
name: 'Austria',
|
||||
region: 'western-europe',
|
||||
values: { x: 1.3, y: 0.6 },
|
||||
liberal: 'market',
|
||||
divide: 'economy'
|
||||
},
|
||||
{
|
||||
id: 'ch',
|
||||
iso: '756',
|
||||
name: 'Switzerland',
|
||||
region: 'western-europe',
|
||||
values: { x: 1.9, y: 0.7 },
|
||||
liberal: 'market',
|
||||
divide: 'economy'
|
||||
},
|
||||
{
|
||||
id: 'gb',
|
||||
iso: '826',
|
||||
name: 'United Kingdom',
|
||||
region: 'western-europe',
|
||||
values: { x: 1.8, y: 0.3 },
|
||||
liberal: 'market',
|
||||
divide: 'economy',
|
||||
note: 'Liberal Democrats sit in the center; Brexit cut across old lines'
|
||||
},
|
||||
{
|
||||
id: 'ie',
|
||||
iso: '372',
|
||||
name: 'Ireland',
|
||||
region: 'western-europe',
|
||||
values: { x: 1.4, y: -0.4 },
|
||||
liberal: 'market',
|
||||
divide: 'history',
|
||||
note: 'The two big parties descend from opposite sides of the 1922 civil war'
|
||||
},
|
||||
{
|
||||
id: 'fr',
|
||||
iso: '250',
|
||||
name: 'France',
|
||||
region: 'western-europe',
|
||||
values: { x: 1.1, y: 0.8 },
|
||||
liberal: 'market',
|
||||
divide: 'economy',
|
||||
note: '"Libéral" means pro-market, often used as a criticism'
|
||||
},
|
||||
{
|
||||
id: 'es',
|
||||
iso: '724',
|
||||
name: 'Spain',
|
||||
region: 'western-europe',
|
||||
values: { x: 1.3, y: 0.3 },
|
||||
liberal: 'market',
|
||||
divide: 'economy',
|
||||
note: 'Regional nationalism (Catalonia, Basque Country) is a second axis'
|
||||
},
|
||||
{
|
||||
id: 'pt',
|
||||
iso: '620',
|
||||
name: 'Portugal',
|
||||
region: 'western-europe',
|
||||
values: { x: 0.8, y: -0.3 },
|
||||
liberal: 'market',
|
||||
divide: 'economy'
|
||||
},
|
||||
{
|
||||
id: 'it',
|
||||
iso: '380',
|
||||
name: 'Italy',
|
||||
region: 'western-europe',
|
||||
values: { x: 0.5, y: 0.0 },
|
||||
liberal: 'market',
|
||||
divide: 'economy'
|
||||
},
|
||||
{
|
||||
id: 'gr',
|
||||
iso: '300',
|
||||
name: 'Greece',
|
||||
region: 'eastern-europe',
|
||||
values: { x: 0.2, y: 0.0 },
|
||||
liberal: 'market',
|
||||
divide: 'economy'
|
||||
},
|
||||
{
|
||||
id: 'us',
|
||||
iso: '840',
|
||||
name: 'United States',
|
||||
region: 'north-america',
|
||||
values: { x: 1.3, y: -0.5 },
|
||||
liberal: 'left',
|
||||
divide: 'culture',
|
||||
note: '"Liberal" = Democrat; the Democrats sit near Europe\'s center economically'
|
||||
},
|
||||
{
|
||||
id: 'ca',
|
||||
iso: '124',
|
||||
name: 'Canada',
|
||||
region: 'north-america',
|
||||
values: { x: 1.8, y: 0.0 },
|
||||
liberal: 'left',
|
||||
divide: 'economy'
|
||||
},
|
||||
{
|
||||
id: 'mx',
|
||||
iso: '484',
|
||||
name: 'Mexico',
|
||||
region: 'latin-america',
|
||||
values: { x: 0.6, y: -1.3 },
|
||||
liberal: 'market',
|
||||
divide: 'economy'
|
||||
},
|
||||
{
|
||||
id: 'br',
|
||||
iso: '076',
|
||||
name: 'Brazil',
|
||||
region: 'latin-america',
|
||||
values: { x: 0.1, y: -1.0 },
|
||||
liberal: 'market',
|
||||
divide: 'economy',
|
||||
note: 'Bolsonaro\'s party is literally called "Partido Liberal"'
|
||||
},
|
||||
{
|
||||
id: 'ar',
|
||||
iso: '032',
|
||||
name: 'Argentina',
|
||||
region: 'latin-america',
|
||||
values: { x: 0.7, y: -0.6 },
|
||||
liberal: 'market',
|
||||
divide: 'economy',
|
||||
note: 'President Milei calls himself a "liberal-libertarian"'
|
||||
},
|
||||
{
|
||||
id: 'cl',
|
||||
iso: '152',
|
||||
name: 'Chile',
|
||||
region: 'latin-america',
|
||||
values: { x: 0.8, y: -0.3 },
|
||||
liberal: 'market',
|
||||
divide: 'economy'
|
||||
},
|
||||
{
|
||||
id: 'co',
|
||||
iso: '170',
|
||||
name: 'Colombia',
|
||||
region: 'latin-america',
|
||||
values: { x: 0.5, y: -1.6 },
|
||||
divide: 'economy'
|
||||
},
|
||||
{
|
||||
id: 'pe',
|
||||
iso: '604',
|
||||
name: 'Peru',
|
||||
region: 'latin-america',
|
||||
values: { x: 0.3, y: -1.1 },
|
||||
liberal: 'market',
|
||||
divide: 'economy'
|
||||
},
|
||||
{
|
||||
id: 've',
|
||||
iso: '862',
|
||||
name: 'Venezuela',
|
||||
region: 'latin-america',
|
||||
values: { x: 0.4, y: -1.5 },
|
||||
divide: 'none'
|
||||
},
|
||||
{
|
||||
id: 'au',
|
||||
iso: '036',
|
||||
name: 'Australia',
|
||||
region: 'oceania',
|
||||
values: { x: 2.0, y: 0.2 },
|
||||
liberal: 'market',
|
||||
divide: 'economy',
|
||||
note: 'The Liberal Party is the main conservative party'
|
||||
},
|
||||
{
|
||||
id: 'nz',
|
||||
iso: '554',
|
||||
name: 'New Zealand',
|
||||
region: 'oceania',
|
||||
values: { x: 2.2, y: 0.1 },
|
||||
liberal: 'market',
|
||||
divide: 'economy'
|
||||
},
|
||||
{
|
||||
id: 'pl',
|
||||
iso: '616',
|
||||
name: 'Poland',
|
||||
region: 'eastern-europe',
|
||||
values: { x: 0.2, y: -0.6 },
|
||||
liberal: 'western',
|
||||
divide: 'culture'
|
||||
},
|
||||
{
|
||||
id: 'cz',
|
||||
iso: '203',
|
||||
name: 'Czechia',
|
||||
region: 'eastern-europe',
|
||||
values: { x: 0.5, y: 1.2 },
|
||||
liberal: 'western',
|
||||
divide: 'culture'
|
||||
},
|
||||
{
|
||||
id: 'hu',
|
||||
iso: '348',
|
||||
name: 'Hungary',
|
||||
region: 'eastern-europe',
|
||||
values: { x: -0.4, y: 0.4 },
|
||||
liberal: 'western',
|
||||
divide: 'culture',
|
||||
note: 'Orbán calls his model "illiberal democracy"'
|
||||
},
|
||||
{
|
||||
id: 'ro',
|
||||
iso: '642',
|
||||
name: 'Romania',
|
||||
region: 'eastern-europe',
|
||||
values: { x: -0.4, y: -0.6 },
|
||||
liberal: 'western',
|
||||
divide: 'culture'
|
||||
},
|
||||
{
|
||||
id: 'rs',
|
||||
iso: '688',
|
||||
name: 'Serbia',
|
||||
region: 'eastern-europe',
|
||||
values: { x: -0.6, y: 0.1 },
|
||||
liberal: 'western',
|
||||
divide: 'history'
|
||||
},
|
||||
{
|
||||
id: 'ua',
|
||||
iso: '804',
|
||||
name: 'Ukraine',
|
||||
region: 'eastern-europe',
|
||||
values: { x: -0.9, y: 0.2 },
|
||||
divide: 'history',
|
||||
note: 'Politics long split between pro-Western and pro-Russian orientation'
|
||||
},
|
||||
{
|
||||
id: 'ru',
|
||||
iso: '643',
|
||||
name: 'Russia',
|
||||
region: 'eastern-europe',
|
||||
values: { x: -1.0, y: 0.6 },
|
||||
liberal: 'western',
|
||||
divide: 'none',
|
||||
note: '"Liberal" = the pro-Western opposition, mostly in exile or prison'
|
||||
},
|
||||
{
|
||||
id: 'kz',
|
||||
iso: '398',
|
||||
name: 'Kazakhstan',
|
||||
region: 'eastern-europe',
|
||||
values: { x: -0.6, y: 0.4 },
|
||||
divide: 'none'
|
||||
},
|
||||
{
|
||||
id: 'tr',
|
||||
iso: '792',
|
||||
name: 'Turkey',
|
||||
region: 'middle-east',
|
||||
values: { x: -0.6, y: -0.9 },
|
||||
liberal: 'western',
|
||||
divide: 'culture',
|
||||
note: 'Main divide: religious-conservative vs secular'
|
||||
},
|
||||
{
|
||||
id: 'ir',
|
||||
iso: '364',
|
||||
name: 'Iran',
|
||||
region: 'middle-east',
|
||||
values: { x: -0.9, y: -0.6 },
|
||||
liberal: 'western',
|
||||
divide: 'none'
|
||||
},
|
||||
{
|
||||
id: 'iq',
|
||||
iso: '368',
|
||||
name: 'Iraq',
|
||||
region: 'middle-east',
|
||||
values: { x: -1.2, y: -1.3 },
|
||||
divide: 'culture'
|
||||
},
|
||||
{
|
||||
id: 'eg',
|
||||
iso: '818',
|
||||
name: 'Egypt',
|
||||
region: 'middle-east',
|
||||
values: { x: -1.5, y: -1.5 },
|
||||
divide: 'none'
|
||||
},
|
||||
{ id: 'ma', iso: '504', name: 'Morocco', region: 'middle-east', values: { x: -1.2, y: -1.5 } },
|
||||
{ id: 'sa', iso: '682', name: 'Saudi Arabia', region: 'middle-east', divide: 'none' },
|
||||
{ id: 'ng', iso: '566', name: 'Nigeria', region: 'africa', values: { x: -0.3, y: -1.8 } },
|
||||
{ id: 'et', iso: '231', name: 'Ethiopia', region: 'africa', values: { x: -0.9, y: -1.5 } },
|
||||
{ id: 'ke', iso: '404', name: 'Kenya', region: 'africa', values: { x: -0.4, y: -1.5 } },
|
||||
{
|
||||
id: 'za',
|
||||
iso: '710',
|
||||
name: 'South Africa',
|
||||
region: 'africa',
|
||||
values: { x: 0.0, y: -1.2 },
|
||||
divide: 'history',
|
||||
note: 'Politics still shaped by apartheid and the liberation struggle'
|
||||
},
|
||||
{
|
||||
id: 'in',
|
||||
iso: '356',
|
||||
name: 'India',
|
||||
region: 'south-asia',
|
||||
values: { x: -0.5, y: -0.5 },
|
||||
divide: 'culture',
|
||||
note: 'Main divide: Hindu nationalism vs secular pluralism'
|
||||
},
|
||||
{ id: 'pk', iso: '586', name: 'Pakistan', region: 'south-asia', values: { x: -1.0, y: -1.4 } },
|
||||
{
|
||||
id: 'bd',
|
||||
iso: '050',
|
||||
name: 'Bangladesh',
|
||||
region: 'south-asia',
|
||||
values: { x: -1.0, y: -1.1 },
|
||||
divide: 'history'
|
||||
},
|
||||
{
|
||||
id: 'cn',
|
||||
iso: '156',
|
||||
name: 'China',
|
||||
region: 'east-asia',
|
||||
values: { x: -0.9, y: 1.5 },
|
||||
liberal: 'western',
|
||||
divide: 'none',
|
||||
note: '"Liberal" = pro-Western intellectuals, a label the state treats with suspicion'
|
||||
},
|
||||
{
|
||||
id: 'jp',
|
||||
iso: '392',
|
||||
name: 'Japan',
|
||||
region: 'east-asia',
|
||||
values: { x: 0.4, y: 1.8 },
|
||||
liberal: 'market',
|
||||
divide: 'history',
|
||||
note: 'The "Liberal Democratic Party" is the conservative party in power since 1955'
|
||||
},
|
||||
{
|
||||
id: 'kr',
|
||||
iso: '410',
|
||||
name: 'South Korea',
|
||||
region: 'east-asia',
|
||||
values: { x: -0.3, y: 1.2 },
|
||||
liberal: 'left',
|
||||
divide: 'history',
|
||||
note: 'Divide is mostly about North Korea, Japan and the US'
|
||||
},
|
||||
{
|
||||
id: 'tw',
|
||||
iso: '158',
|
||||
name: 'Taiwan',
|
||||
region: 'east-asia',
|
||||
values: { x: 0.0, y: 1.3 },
|
||||
divide: 'history',
|
||||
note: 'Main divide: how close to be with mainland China'
|
||||
},
|
||||
{
|
||||
id: 'vn',
|
||||
iso: '704',
|
||||
name: 'Vietnam',
|
||||
region: 'southeast-asia',
|
||||
values: { x: -0.6, y: 0.5 },
|
||||
divide: 'none'
|
||||
},
|
||||
{
|
||||
id: 'th',
|
||||
iso: '764',
|
||||
name: 'Thailand',
|
||||
region: 'southeast-asia',
|
||||
values: { x: 0.0, y: -0.4 },
|
||||
divide: 'history',
|
||||
note: 'Main divide: monarchy and military vs reformists'
|
||||
},
|
||||
{
|
||||
id: 'my',
|
||||
iso: '458',
|
||||
name: 'Malaysia',
|
||||
region: 'southeast-asia',
|
||||
values: { x: -0.6, y: -0.9 },
|
||||
divide: 'culture'
|
||||
},
|
||||
{
|
||||
id: 'ph',
|
||||
iso: '608',
|
||||
name: 'Philippines',
|
||||
region: 'southeast-asia',
|
||||
values: { x: -0.2, y: -1.4 }
|
||||
},
|
||||
{
|
||||
id: 'id',
|
||||
iso: '360',
|
||||
name: 'Indonesia',
|
||||
region: 'southeast-asia',
|
||||
values: { x: -0.8, y: -1.2 },
|
||||
divide: 'culture'
|
||||
}
|
||||
];
|
||||
|
||||
export const countryById = new Map(countries.map((country) => [country.id, country]));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Party compass: parties on one shared scale
|
||||
// econ: 0 = far left (state-led, redistributive) … 10 = far right (free market)
|
||||
@@ -557,154 +84,7 @@ export type Party = {
|
||||
note?: string;
|
||||
};
|
||||
|
||||
export const partyCountries = [
|
||||
'us',
|
||||
'nl',
|
||||
'de',
|
||||
'gb',
|
||||
'fr',
|
||||
'se',
|
||||
'pl',
|
||||
'jp',
|
||||
'kr',
|
||||
'in',
|
||||
'br',
|
||||
'ca'
|
||||
];
|
||||
|
||||
export const parties: Party[] = [
|
||||
{
|
||||
country: 'us',
|
||||
short: 'Dem',
|
||||
name: 'Democratic Party',
|
||||
econ: 4.8,
|
||||
social: 3.5,
|
||||
note: 'The US "liberals": near Europe\'s center economically'
|
||||
},
|
||||
{ country: 'us', short: 'GOP', name: 'Republican Party', econ: 7.8, social: 8.0 },
|
||||
|
||||
{ country: 'nl', short: 'GL-PvdA', name: 'GroenLinks-PvdA', econ: 2.5, social: 1.5 },
|
||||
{ country: 'nl', short: 'SP', name: 'Socialistische Partij', econ: 1.5, social: 5.0 },
|
||||
{ country: 'nl', short: 'D66', name: 'Democraten 66', econ: 5.5, social: 1.5 },
|
||||
{ country: 'nl', short: 'CDA', name: 'Christen-Democratisch Appèl', econ: 6.0, social: 6.0 },
|
||||
{
|
||||
country: 'nl',
|
||||
short: 'VVD',
|
||||
name: 'Volkspartij voor Vrijheid en Democratie',
|
||||
econ: 7.5,
|
||||
social: 5.0,
|
||||
note: 'Dutch "liberals": market-liberal, center-right'
|
||||
},
|
||||
{ country: 'nl', short: 'BBB', name: 'BoerBurgerBeweging', econ: 6.0, social: 7.5 },
|
||||
{
|
||||
country: 'nl',
|
||||
short: 'PVV',
|
||||
name: 'Partij voor de Vrijheid',
|
||||
econ: 4.5,
|
||||
social: 9.0,
|
||||
note: 'Radical right on immigration, centrist on welfare'
|
||||
},
|
||||
|
||||
{ country: 'de', short: 'Linke', name: 'Die Linke', econ: 1.0, social: 2.5 },
|
||||
{ country: 'de', short: 'Grüne', name: 'Bündnis 90/Die Grünen', econ: 3.0, social: 1.5 },
|
||||
{ country: 'de', short: 'SPD', name: 'Sozialdemokratische Partei', econ: 3.5, social: 3.5 },
|
||||
{ country: 'de', short: 'BSW', name: 'Bündnis Sahra Wagenknecht', econ: 2.0, social: 7.0 },
|
||||
{
|
||||
country: 'de',
|
||||
short: 'FDP',
|
||||
name: 'Freie Demokratische Partei',
|
||||
econ: 8.5,
|
||||
social: 3.0,
|
||||
note: 'German "liberals": socially open, strongly pro-market'
|
||||
},
|
||||
{ country: 'de', short: 'CDU', name: 'CDU/CSU', econ: 6.5, social: 6.0 },
|
||||
{ country: 'de', short: 'AfD', name: 'Alternative für Deutschland', econ: 7.0, social: 9.5 },
|
||||
|
||||
{ country: 'gb', short: 'Green', name: 'Green Party', econ: 1.5, social: 1.5 },
|
||||
{ country: 'gb', short: 'Lab', name: 'Labour', econ: 3.8, social: 3.5 },
|
||||
{ country: 'gb', short: 'LD', name: 'Liberal Democrats', econ: 5.0, social: 2.5 },
|
||||
{ country: 'gb', short: 'Con', name: 'Conservatives', econ: 7.0, social: 6.5 },
|
||||
{ country: 'gb', short: 'Reform', name: 'Reform UK', econ: 7.5, social: 8.5 },
|
||||
|
||||
{ country: 'fr', short: 'LFI', name: 'La France Insoumise', econ: 1.0, social: 2.5 },
|
||||
{ country: 'fr', short: 'PS', name: 'Parti Socialiste', econ: 3.0, social: 3.0 },
|
||||
{ country: 'fr', short: 'RE', name: 'Renaissance', econ: 6.5, social: 4.0 },
|
||||
{ country: 'fr', short: 'LR', name: 'Les Républicains', econ: 7.5, social: 7.0 },
|
||||
{
|
||||
country: 'fr',
|
||||
short: 'RN',
|
||||
name: 'Rassemblement National',
|
||||
econ: 4.5,
|
||||
social: 9.0,
|
||||
note: 'Economically left of Macron: lower retirement age, protectionism'
|
||||
},
|
||||
|
||||
{ country: 'se', short: 'V', name: 'Vänsterpartiet', econ: 1.0, social: 2.0 },
|
||||
{ country: 'se', short: 'S', name: 'Socialdemokraterna', econ: 3.0, social: 3.5 },
|
||||
{ country: 'se', short: 'C', name: 'Centerpartiet', econ: 7.0, social: 2.5 },
|
||||
{ country: 'se', short: 'M', name: 'Moderaterna', econ: 7.5, social: 5.5 },
|
||||
{ country: 'se', short: 'SD', name: 'Sverigedemokraterna', econ: 5.5, social: 9.0 },
|
||||
|
||||
{ country: 'pl', short: 'Lewica', name: 'Lewica', econ: 2.0, social: 2.0 },
|
||||
{ country: 'pl', short: 'KO', name: 'Koalicja Obywatelska', econ: 6.5, social: 4.0 },
|
||||
{
|
||||
country: 'pl',
|
||||
short: 'PiS',
|
||||
name: 'Prawo i Sprawiedliwość',
|
||||
econ: 3.5,
|
||||
social: 9.0,
|
||||
note: 'Culturally right, economically left (big family benefits)'
|
||||
},
|
||||
{ country: 'pl', short: 'Konf', name: 'Konfederacja', econ: 9.0, social: 9.0 },
|
||||
|
||||
{ country: 'jp', short: 'JCP', name: 'Japanese Communist Party', econ: 1.0, social: 3.0 },
|
||||
{ country: 'jp', short: 'CDP', name: 'Constitutional Democratic Party', econ: 4.0, social: 3.5 },
|
||||
{ country: 'jp', short: 'Komeito', name: 'Komeito', econ: 5.0, social: 5.5 },
|
||||
{
|
||||
country: 'jp',
|
||||
short: 'LDP',
|
||||
name: 'Liberal Democratic Party',
|
||||
econ: 6.0,
|
||||
social: 7.0,
|
||||
note: 'Called "Liberal", governs as conservatives since 1955'
|
||||
},
|
||||
{ country: 'jp', short: 'Ishin', name: 'Nippon Ishin no Kai', econ: 7.5, social: 6.0 },
|
||||
{ country: 'jp', short: 'Sansei', name: 'Sanseitō', econ: 6.0, social: 9.0 },
|
||||
|
||||
{ country: 'kr', short: 'Justice', name: 'Justice Party', econ: 1.5, social: 1.5 },
|
||||
{
|
||||
country: 'kr',
|
||||
short: 'DPK',
|
||||
name: 'Democratic Party of Korea',
|
||||
econ: 4.0,
|
||||
social: 4.5,
|
||||
note: 'The "progressive" camp, cautious on social issues'
|
||||
},
|
||||
{ country: 'kr', short: 'PPP', name: 'People Power Party', econ: 7.0, social: 7.0 },
|
||||
|
||||
{ country: 'in', short: 'INC', name: 'Indian National Congress', econ: 4.5, social: 4.0 },
|
||||
{
|
||||
country: 'in',
|
||||
short: 'BJP',
|
||||
name: 'Bharatiya Janata Party',
|
||||
econ: 6.0,
|
||||
social: 8.5,
|
||||
note: 'Hindu-nationalist, pro-business, big welfare schemes'
|
||||
},
|
||||
|
||||
{ country: 'br', short: 'PT', name: 'Partido dos Trabalhadores', econ: 2.5, social: 4.5 },
|
||||
{ country: 'br', short: 'MDB', name: 'Movimento Democrático Brasileiro', econ: 6.0, social: 6.0 },
|
||||
{ country: 'br', short: 'PL', name: 'Partido Liberal', econ: 7.5, social: 9.0 },
|
||||
|
||||
{ country: 'ca', short: 'NDP', name: 'New Democratic Party', econ: 2.5, social: 2.5 },
|
||||
{ country: 'ca', short: 'LPC', name: 'Liberal Party of Canada', econ: 4.8, social: 3.0 },
|
||||
{ country: 'ca', short: 'CPC', name: 'Conservative Party of Canada', econ: 7.5, social: 7.0 }
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Country profiles, kept short enough to scan
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Country profiles, kept short enough to scan.
|
||||
export type Profile = {
|
||||
id: string;
|
||||
/** What "normal" politics looks like, in one line. */
|
||||
@@ -714,263 +94,43 @@ export type Profile = {
|
||||
blindSpots: string[];
|
||||
};
|
||||
|
||||
export const profiles: Profile[] = [
|
||||
{
|
||||
id: 'us',
|
||||
center: 'Individual liberty and suspicion of government, on both sides',
|
||||
fights: ['abortion', 'guns', 'race', 'immigration', 'religion'],
|
||||
timeline: [
|
||||
{ year: '1776', label: 'revolt against a distant state' },
|
||||
{ year: '1861', label: 'civil war over slavery' },
|
||||
{ year: '1945', label: 'wins WWII, superpower' },
|
||||
{ year: '1964', label: 'Civil Rights Act' },
|
||||
{ year: '1991', label: 'wins the Cold War' }
|
||||
],
|
||||
blindSpots: [
|
||||
'assumes its left/right is universal',
|
||||
'partisan media bubbles',
|
||||
'little view of other systems'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'nl',
|
||||
center: 'Coalition consensus ("polder model") around a strong welfare state',
|
||||
fights: ['immigration', 'housing', 'nitrogen & farmers', 'EU'],
|
||||
timeline: [
|
||||
{ year: '1600s', label: 'Golden Age, trade empire' },
|
||||
{ year: '1940', label: 'German occupation' },
|
||||
{ year: '1949', label: 'loses Indonesia' },
|
||||
{ year: '1960s', label: 'pillars break, secularization' },
|
||||
{ year: '2002', label: 'Fortuyn, immigration debate' }
|
||||
],
|
||||
blindSpots: ['tolerance self-image vs discrimination', 'colonial past taught thinly']
|
||||
},
|
||||
{
|
||||
id: 'de',
|
||||
center: 'Social market economy, fiscal caution, "never again"',
|
||||
fights: ['migration', 'energy & climate', 'East vs West', 'debt brake'],
|
||||
timeline: [
|
||||
{ year: '1923', label: 'hyperinflation' },
|
||||
{ year: '1933', label: 'Nazi dictatorship' },
|
||||
{ year: '1949', label: 'split + economic miracle' },
|
||||
{ year: '1990', label: 'reunification' },
|
||||
{ year: '2015', label: 'refugee crisis' }
|
||||
],
|
||||
blindSpots: ['debt as moral failing', 'Russian gas as "just trade"']
|
||||
},
|
||||
{
|
||||
id: 'fr',
|
||||
center: 'Strong central state, laïcité, protest as normal politics',
|
||||
fights: ['pensions', 'Paris vs periphery', 'Islam & laïcité', 'EU'],
|
||||
timeline: [
|
||||
{ year: '1789', label: 'Revolution invents left/right' },
|
||||
{ year: '1940', label: 'defeat and Vichy' },
|
||||
{ year: '1962', label: 'loses Algerian war' },
|
||||
{ year: '1968', label: "May '68" },
|
||||
{ year: '2018', label: 'Yellow Vests' }
|
||||
],
|
||||
blindSpots: ['colorblind universalism hides racism', 'influence in former African colonies']
|
||||
},
|
||||
{
|
||||
id: 'gb',
|
||||
center: 'Pragmatic and market-friendly, but the NHS is sacred',
|
||||
fights: ['Brexit', 'class', 'North vs South', 'Scotland'],
|
||||
timeline: [
|
||||
{ year: '1900', label: 'largest empire in history' },
|
||||
{ year: '1940', label: '"stood alone"' },
|
||||
{ year: '1948', label: 'NHS founded' },
|
||||
{ year: '1979', label: 'Thatcher' },
|
||||
{ year: '2016', label: 'Brexit vote' }
|
||||
],
|
||||
blindSpots: ['empire taught selectively', 'overrates the "special relationship"']
|
||||
},
|
||||
{
|
||||
id: 'se',
|
||||
center: 'High-trust, high-tax welfare state; very secular and individualist',
|
||||
fights: ['immigration', 'gang violence', 'private welfare', 'NATO'],
|
||||
timeline: [
|
||||
{ year: '1814', label: 'last war fought' },
|
||||
{ year: '1932', label: "Social Democrats' era" },
|
||||
{ year: '1939', label: 'neutral in WWII' },
|
||||
{ year: '2015', label: 'record asylum intake' },
|
||||
{ year: '2024', label: 'joins NATO' }
|
||||
],
|
||||
blindSpots: ['consensus makes disagreement taboo', 'neutrality myth (iron to Germany)']
|
||||
},
|
||||
{
|
||||
id: 'pl',
|
||||
center: 'Pro-NATO and wary of Russia across all parties',
|
||||
fights: ['Catholic vs liberal', 'abortion', 'rule of law', 'EU'],
|
||||
timeline: [
|
||||
{ year: '1795', label: 'erased by partition' },
|
||||
{ year: '1939', label: 'invaded by Germany and USSR' },
|
||||
{ year: '1980', label: 'Solidarity movement' },
|
||||
{ year: '1989', label: 'communism falls' },
|
||||
{ year: '2004', label: 'joins the EU' }
|
||||
],
|
||||
blindSpots: ['victimhood crowds out dark chapters', 'distrust of Germany']
|
||||
},
|
||||
{
|
||||
id: 'ru',
|
||||
center: 'Strong state and great-power status; opposition repressed',
|
||||
fights: ['largely suppressed in public'],
|
||||
timeline: [
|
||||
{ year: '1812', label: 'Napoleon invades' },
|
||||
{ year: '1941', label: 'Nazi invasion, ~27M dead' },
|
||||
{ year: '1991', label: 'USSR collapses' },
|
||||
{ year: '1990s', label: 'chaos and poverty' },
|
||||
{ year: '2022', label: 'full invasion of Ukraine' }
|
||||
],
|
||||
blindSpots: [
|
||||
'state-controlled media',
|
||||
'neighbors seen as sphere of influence',
|
||||
'Soviet crimes downplayed'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'cn',
|
||||
center: 'Order, growth and national rejuvenation, led by the Party',
|
||||
fights: ['youth jobs', 'property crisis', 'local corruption'],
|
||||
timeline: [
|
||||
{ year: '1839', label: 'Opium Wars, "humiliation"' },
|
||||
{ year: '1937', label: 'Japanese invasion' },
|
||||
{ year: '1966', label: 'Cultural Revolution' },
|
||||
{ year: '1978', label: 'reform and opening' },
|
||||
{ year: '1989', label: 'Tiananmen (unspoken)' }
|
||||
],
|
||||
blindSpots: ['censorship shapes history', 'criticism read as new humiliation']
|
||||
},
|
||||
{
|
||||
id: 'jp',
|
||||
center: 'Stability and harmony; one party governing since 1955',
|
||||
fights: ['pacifist constitution', 'aging & immigration', 'war memory'],
|
||||
timeline: [
|
||||
{ year: '1868', label: 'Meiji modernization' },
|
||||
{ year: '1937', label: 'war in Asia' },
|
||||
{ year: '1945', label: 'atomic bombs, defeat' },
|
||||
{ year: '1947', label: 'pacifist constitution' },
|
||||
{ year: '1990', label: 'bubble bursts' }
|
||||
],
|
||||
blindSpots: ['wartime atrocities taught thinly', 'sees itself as war victim']
|
||||
},
|
||||
{
|
||||
id: 'kr',
|
||||
center: 'Fierce democracy won by protest; hyper-competitive society',
|
||||
fights: ['North Korea', 'Japan', 'gender', 'chaebol power'],
|
||||
timeline: [
|
||||
{ year: '1910', label: 'Japanese colonization' },
|
||||
{ year: '1950', label: 'Korean War' },
|
||||
{ year: '1980', label: 'Gwangju massacre' },
|
||||
{ year: '1987', label: 'democracy' },
|
||||
{ year: '2024', label: 'martial law crisis' }
|
||||
],
|
||||
blindSpots: ['US and Japan seen through party lens']
|
||||
},
|
||||
{
|
||||
id: 'in',
|
||||
center: 'Largest democracy; religion, caste and language drive politics',
|
||||
fights: ['Hindu nationalism', 'caste quotas', 'language', 'center vs states'],
|
||||
timeline: [
|
||||
{ year: '1858', label: 'British Raj' },
|
||||
{ year: '1947', label: 'independence and Partition' },
|
||||
{ year: '1961', label: 'Non-Aligned Movement' },
|
||||
{ year: '1991', label: 'economic liberalization' },
|
||||
{ year: '2014', label: 'BJP majority' }
|
||||
],
|
||||
blindSpots: ['polarized media', 'reduced to poverty & religion by outsiders']
|
||||
},
|
||||
{
|
||||
id: 'br',
|
||||
center: 'Inequality makes redistribution central; religious society',
|
||||
fights: ['Lula vs Bolsonaro', 'crime', 'Amazon vs agribusiness'],
|
||||
timeline: [
|
||||
{ year: '1888', label: 'last to abolish slavery' },
|
||||
{ year: '1964', label: 'military coup' },
|
||||
{ year: '1985', label: 'democracy returns' },
|
||||
{ year: '1994', label: 'ends hyperinflation' },
|
||||
{ year: '2014', label: 'Car Wash scandal' }
|
||||
],
|
||||
blindSpots: ['"racial democracy" myth', 'WhatsApp information bubbles']
|
||||
},
|
||||
{
|
||||
id: 'tr',
|
||||
center: 'National pride, strong state, balancing West and region',
|
||||
fights: ['religious vs secular', 'Kurdish question', 'inflation'],
|
||||
timeline: [
|
||||
{ year: '1915', label: 'Armenian genocide (denied)' },
|
||||
{ year: '1920', label: 'Sèvres, fear of partition' },
|
||||
{ year: '1923', label: "Atatürk's secular republic" },
|
||||
{ year: '1980', label: 'military coup' },
|
||||
{ year: '2016', label: 'failed coup' }
|
||||
],
|
||||
blindSpots: ['genocide denial', 'restricted press', 'Western criticism read as hostility']
|
||||
}
|
||||
];
|
||||
|
||||
export const profileById = new Map(profiles.map((profile) => [profile.id, profile]));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// One event, many stories
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// One event, many stories.
|
||||
export type Narrative = {
|
||||
event: string;
|
||||
years: string;
|
||||
views: { country: string; story: string }[];
|
||||
};
|
||||
|
||||
export const narratives: Narrative[] = [
|
||||
{
|
||||
event: 'World War II',
|
||||
years: '1939–45',
|
||||
views: [
|
||||
{ country: 'us', story: 'D-Day: democracy beats fascism' },
|
||||
{ country: 'ru', story: 'We broke Hitler, 27M dead' },
|
||||
{ country: 'cn', story: 'Our war began in 1937' },
|
||||
{ country: 'pl', story: 'Liberation became Soviet occupation' },
|
||||
{ country: 'jp', story: 'Hiroshima: we suffered' }
|
||||
]
|
||||
},
|
||||
{
|
||||
event: 'End of the Cold War',
|
||||
years: '1989–91',
|
||||
views: [
|
||||
{ country: 'us', story: 'We won: the end of history' },
|
||||
{ country: 'pl', story: 'We freed ourselves' },
|
||||
{ country: 'ru', story: 'Collapse and humiliation' },
|
||||
{ country: 'cn', story: 'A warning: never lose control' }
|
||||
]
|
||||
},
|
||||
{
|
||||
event: 'Colonialism',
|
||||
years: '1500s–1970s',
|
||||
views: [
|
||||
{ country: 'nl', story: 'Golden Age trade, now debated' },
|
||||
{ country: 'gb', story: 'Empire, remembered fondly by many' },
|
||||
{ country: 'in', story: 'Extraction and Partition' },
|
||||
{ country: 'cn', story: 'Century of humiliation' },
|
||||
{ country: 'br', story: 'Conquest, then US dominance' }
|
||||
]
|
||||
},
|
||||
{
|
||||
event: 'Iraq War',
|
||||
years: '2003',
|
||||
views: [
|
||||
{ country: 'us', story: 'Disarmament, later a mistake' },
|
||||
{ country: 'fr', story: 'We warned you' },
|
||||
{ country: 'iq', story: 'Invasion, then civil war' },
|
||||
{ country: 'ru', story: 'Proof the rules are selective' }
|
||||
]
|
||||
}
|
||||
];
|
||||
export type Source = { name: string; href: string };
|
||||
|
||||
export const sources = [
|
||||
{
|
||||
name: 'World Values Survey / EVS cultural map',
|
||||
href: 'https://www.worldvaluessurvey.org/WVSContents.jsp?CMSID=Findings'
|
||||
},
|
||||
{ name: 'Chapel Hill Expert Survey', href: 'https://www.chesdata.eu/' },
|
||||
{ name: 'Manifesto Project', href: 'https://manifesto-project.wzb.eu/' },
|
||||
{ name: 'Pew Research Center', href: 'https://www.pewresearch.org/global/' },
|
||||
{ name: 'Natural Earth (map shapes)', href: 'https://www.naturalearthdata.com/' }
|
||||
];
|
||||
/** 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);
|
||||
|
||||
Reference in New Issue
Block a user