From d71e39f40a121c3f4ef930fcdbd99b709dda6384 Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Mon, 26 Oct 2026 00:16:08 +0000 Subject: [PATCH] stats: world opinions moves to /stats/opinions/world, the energy tracker merges into /stats/energy (live NED section), 301s from /projects/world-opinions and /projects/energy, tracker-style icons for the stats header, nav, sub-tabs and home grid --- scripts/stats/world-opinions-seed.json | 1677 ----------------- src/lib/components/CatalogGrid.svelte | 2 +- src/lib/components/SiteChrome.svelte | 29 +- src/lib/projects.ts | 35 +- src/lib/seo.ts | 9 +- src/lib/server/db/schema/stats.ts | 2 +- src/lib/server/world-opinions.ts | 2 +- .../stats/EnergyLive.svelte} | 490 ++--- src/lib/title.ts | 3 +- src/lib/tracker-icons.ts | 83 + src/lib/world-opinions/data.test.ts | 50 - src/lib/world-opinions/data.ts | 2 +- src/routes/+page.svelte | 6 +- src/routes/projects/energy/+page.server.ts | 4 - .../projects/energy/[...rest]/+server.ts | 7 + .../world-opinions/[...rest]/+server.ts | 7 + src/routes/sitemap.xml/+server.ts | 3 +- src/routes/stats/+layout.svelte | 85 +- src/routes/stats/+page.svelte | 45 +- src/routes/stats/energy/+page.server.ts | 3 + src/routes/stats/energy/+page.svelte | 48 +- src/routes/stats/opinions/+page.svelte | 5 +- .../opinions/world}/+page.server.ts | 0 .../opinions/world}/+page.svelte | 43 +- .../opinions/world}/world-opinions.css | 0 25 files changed, 405 insertions(+), 2235 deletions(-) delete mode 100644 scripts/stats/world-opinions-seed.json rename src/{routes/projects/energy/+page.svelte => lib/stats/EnergyLive.svelte} (69%) delete mode 100644 src/lib/world-opinions/data.test.ts delete mode 100644 src/routes/projects/energy/+page.server.ts create mode 100644 src/routes/projects/energy/[...rest]/+server.ts create mode 100644 src/routes/projects/world-opinions/[...rest]/+server.ts rename src/routes/{projects/world-opinions => stats/opinions/world}/+page.server.ts (100%) rename src/routes/{projects/world-opinions => stats/opinions/world}/+page.svelte (75%) rename src/routes/{projects/world-opinions => stats/opinions/world}/world-opinions.css (100%) diff --git a/scripts/stats/world-opinions-seed.json b/scripts/stats/world-opinions-seed.json deleted file mode 100644 index e90900d..0000000 --- a/scripts/stats/world-opinions-seed.json +++ /dev/null @@ -1,1677 +0,0 @@ -{ - "countries": [ - { - "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 - }, - "liberal": "market", - "divide": "economy" - }, - { - "id": "nl", - "iso": "528", - "name": "Netherlands", - "region": "western-europe", - "values": { - "x": 2, - "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 - }, - "liberal": "market", - "divide": "economy" - }, - { - "id": "gr", - "iso": "300", - "name": "Greece", - "region": "eastern-europe", - "values": { - "x": 0.2, - "y": 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 - }, - "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 - }, - "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, - "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, - "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, - "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, - "y": -1.4 - } - }, - { - "id": "bd", - "iso": "050", - "name": "Bangladesh", - "region": "south-asia", - "values": { - "x": -1, - "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, - "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, - "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" - } - ], - "parties": [ - { - "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 - }, - { - "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 - }, - { - "country": "nl", - "short": "D66", - "name": "Democraten 66", - "econ": 5.5, - "social": 1.5 - }, - { - "country": "nl", - "short": "CDA", - "name": "Christen-Democratisch Appèl", - "econ": 6, - "social": 6 - }, - { - "country": "nl", - "short": "VVD", - "name": "Volkspartij voor Vrijheid en Democratie", - "econ": 7.5, - "social": 5, - "note": "Dutch \"liberals\": market-liberal, center-right" - }, - { - "country": "nl", - "short": "BBB", - "name": "BoerBurgerBeweging", - "econ": 6, - "social": 7.5 - }, - { - "country": "nl", - "short": "PVV", - "name": "Partij voor de Vrijheid", - "econ": 4.5, - "social": 9, - "note": "Radical right on immigration, centrist on welfare" - }, - { - "country": "de", - "short": "Linke", - "name": "Die Linke", - "econ": 1, - "social": 2.5 - }, - { - "country": "de", - "short": "Grüne", - "name": "Bündnis 90/Die Grünen", - "econ": 3, - "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, - "social": 7 - }, - { - "country": "de", - "short": "FDP", - "name": "Freie Demokratische Partei", - "econ": 8.5, - "social": 3, - "note": "German \"liberals\": socially open, strongly pro-market" - }, - { - "country": "de", - "short": "CDU", - "name": "CDU/CSU", - "econ": 6.5, - "social": 6 - }, - { - "country": "de", - "short": "AfD", - "name": "Alternative für Deutschland", - "econ": 7, - "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, - "social": 2.5 - }, - { - "country": "gb", - "short": "Con", - "name": "Conservatives", - "econ": 7, - "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, - "social": 2.5 - }, - { - "country": "fr", - "short": "PS", - "name": "Parti Socialiste", - "econ": 3, - "social": 3 - }, - { - "country": "fr", - "short": "RE", - "name": "Renaissance", - "econ": 6.5, - "social": 4 - }, - { - "country": "fr", - "short": "LR", - "name": "Les Républicains", - "econ": 7.5, - "social": 7 - }, - { - "country": "fr", - "short": "RN", - "name": "Rassemblement National", - "econ": 4.5, - "social": 9, - "note": "Economically left of Macron: lower retirement age, protectionism" - }, - { - "country": "se", - "short": "V", - "name": "Vänsterpartiet", - "econ": 1, - "social": 2 - }, - { - "country": "se", - "short": "S", - "name": "Socialdemokraterna", - "econ": 3, - "social": 3.5 - }, - { - "country": "se", - "short": "C", - "name": "Centerpartiet", - "econ": 7, - "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 - }, - { - "country": "pl", - "short": "Lewica", - "name": "Lewica", - "econ": 2, - "social": 2 - }, - { - "country": "pl", - "short": "KO", - "name": "Koalicja Obywatelska", - "econ": 6.5, - "social": 4 - }, - { - "country": "pl", - "short": "PiS", - "name": "Prawo i Sprawiedliwość", - "econ": 3.5, - "social": 9, - "note": "Culturally right, economically left (big family benefits)" - }, - { - "country": "pl", - "short": "Konf", - "name": "Konfederacja", - "econ": 9, - "social": 9 - }, - { - "country": "jp", - "short": "JCP", - "name": "Japanese Communist Party", - "econ": 1, - "social": 3 - }, - { - "country": "jp", - "short": "CDP", - "name": "Constitutional Democratic Party", - "econ": 4, - "social": 3.5 - }, - { - "country": "jp", - "short": "Komeito", - "name": "Komeito", - "econ": 5, - "social": 5.5 - }, - { - "country": "jp", - "short": "LDP", - "name": "Liberal Democratic Party", - "econ": 6, - "social": 7, - "note": "Called \"Liberal\", governs as conservatives since 1955" - }, - { - "country": "jp", - "short": "Ishin", - "name": "Nippon Ishin no Kai", - "econ": 7.5, - "social": 6 - }, - { - "country": "jp", - "short": "Sansei", - "name": "Sanseitō", - "econ": 6, - "social": 9 - }, - { - "country": "kr", - "short": "Justice", - "name": "Justice Party", - "econ": 1.5, - "social": 1.5 - }, - { - "country": "kr", - "short": "DPK", - "name": "Democratic Party of Korea", - "econ": 4, - "social": 4.5, - "note": "The \"progressive\" camp, cautious on social issues" - }, - { - "country": "kr", - "short": "PPP", - "name": "People Power Party", - "econ": 7, - "social": 7 - }, - { - "country": "in", - "short": "INC", - "name": "Indian National Congress", - "econ": 4.5, - "social": 4 - }, - { - "country": "in", - "short": "BJP", - "name": "Bharatiya Janata Party", - "econ": 6, - "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, - "social": 6 - }, - { - "country": "br", - "short": "PL", - "name": "Partido Liberal", - "econ": 7.5, - "social": 9 - }, - { - "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 - }, - { - "country": "ca", - "short": "CPC", - "name": "Conservative Party of Canada", - "econ": 7.5, - "social": 7 - } - ], - "profiles": [ - { - "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" - ] - } - ], - "narratives": [ - { - "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" - } - ] - } - ], - "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/" - } - ] -} \ No newline at end of file diff --git a/src/lib/components/CatalogGrid.svelte b/src/lib/components/CatalogGrid.svelte index c5a9140..5e67652 100644 --- a/src/lib/components/CatalogGrid.svelte +++ b/src/lib/components/CatalogGrid.svelte @@ -61,7 +61,7 @@ {#snippet body(project: Item)} {#if tracker} - + {:else} all {menu.label}
@@ -438,12 +440,15 @@ - {#if item.href === '/trackers'} - + {#if item.href !== '/projects'} + {:else} {/each} + {:else if item.href === '/stats'} + {:else}
{#each menus[item.href].items as project (project.name)} diff --git a/src/lib/projects.ts b/src/lib/projects.ts index e277b84..e256a3c 100644 --- a/src/lib/projects.ts +++ b/src/lib/projects.ts @@ -18,12 +18,6 @@ export const projects = [ href: '/projects/derby', external: false }, - { - name: 'world opinions (warning: AI slop)', - subtitle: 'ideologies, values and biases across countries', - href: '/projects/world-opinions', - external: false - }, { name: 'language learner (warning: AI slop)', subtitle: 'Dutch, Korean and English, from zero to fluent', @@ -36,48 +30,66 @@ export const projects = [ export const statsPages = [ { name: 'economy', + icon: 'stats-economy', subtitle: 'rates, inflation, bond spreads, GDP and debt', href: '/stats/economy', external: false }, { name: 'energy', - subtitle: 'tariffs, oil and gas, renewables, heat pumps and EVs', + icon: 'stats-energy', + subtitle: 'live grid mix and gas storage, tariffs, oil and gas, renewables, heat pumps and EVs', href: '/stats/energy', external: false }, { name: 'gouda', + icon: 'stats-gouda', subtitle: 'population, housing and neighbourhoods in Gouda', href: '/stats/gouda', external: false }, { name: 'countries', + icon: 'stats-countries', subtitle: 'the Netherlands, Curaçao, the US, Korea, China and more side by side', href: '/stats/countries', external: false }, { name: 'elections', + icon: 'stats-elections', subtitle: 'Dutch elections since 1918, Gouda per polling station, US presidential', href: '/stats/elections', external: false }, { name: 'history', + icon: 'stats-history', subtitle: 'the long run, a sourced timeline, politics and culture of five places', href: '/stats/history', external: false }, { name: 'opinions', - subtitle: 'polls, Eurobarometer, happiness, search trends and party positions', + icon: 'stats-opinions', + subtitle: 'polls, Eurobarometer, happiness, party positions and world opinions', href: '/stats/opinions', external: false } ] as const; +/** Sub-tabs of a /stats section (second row under the stats nav), keyed by the section path. */ +export const statsSubPages: Record< + string, + readonly { name: string; href: string; icon: string }[] +> = { + '/stats/opinions': [ + { name: 'polls & surveys', href: '/stats/opinions', icon: 'stats-polls' }, + { name: 'world', href: '/stats/opinions/world', icon: 'stats-world' } + ] +}; + type Tracker = { name: string; subtitle: string; @@ -381,13 +393,6 @@ const allTrackers: readonly (Tracker & { private: boolean })[] = [ external: false, private: true }, - { - name: 'energy', - subtitle: 'Dutch electricity mix, utilization and gas storage', - href: '/projects/energy', - external: false, - private: false - }, { name: 'games', subtitle: 'every pokemon in every game, and call of duty camo mastery', diff --git a/src/lib/seo.ts b/src/lib/seo.ts index fa5ab7a..2d41827 100644 --- a/src/lib/seo.ts +++ b/src/lib/seo.ts @@ -38,7 +38,7 @@ const publicPages: Record = { '/trackers': { title: 'Trackers', description: - 'Stan Runge’s trackers: games, money, houses, books, Counter-Strike prices, Dutch energy, Animaldex and lists.' + 'Stan Runge’s trackers: games, money, houses, books, Counter-Strike prices, Animaldex and lists.' }, '/trackers/books': { title: 'Books', @@ -104,11 +104,6 @@ const publicPages: Record = { description: 'Compare historical Counter-Strike market prices by listing, weapon, skin, wear, and other item dimensions.' }, - '/projects/energy': { - title: 'Energy', - description: - 'Dutch electricity generation mix, source utilization, consumption trends, CO₂ intensity and gas storage, from NED.nl data.' - }, '/projects/derby': { title: 'Derby Lens', description: 'Estimate horse racing odds and compare them with market prices.' @@ -122,7 +117,7 @@ const publicPages: Record = { description: 'Learn Dutch, Korean and English from beginner to advanced, with lessons and endless daily challenges.' }, - '/projects/world-opinions': { + '/stats/opinions/world': { title: 'World Opinions', description: 'How political words, parties, values and national histories differ across the world.' diff --git a/src/lib/server/db/schema/stats.ts b/src/lib/server/db/schema/stats.ts index 8c5d71a..8af5873 100644 --- a/src/lib/server/db/schema/stats.ts +++ b/src/lib/server/db/schema/stats.ts @@ -523,7 +523,7 @@ export const statsElecTurnout = statsSchema.table( ); // ── Step 3: world opinions (§4, A15) ───────────────────────────────────────────────────── -// The /projects/world-opinions guide reads these. Curated rows (seeded from the former +// The /stats/opinions/world 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', { diff --git a/src/lib/server/world-opinions.ts b/src/lib/server/world-opinions.ts index 7debca8..a82388b 100644 --- a/src/lib/server/world-opinions.ts +++ b/src/lib/server/world-opinions.ts @@ -12,7 +12,7 @@ import type { WorldOpinionsData } from '#lib/world-opinions/data.js'; -// /projects/world-opinions content from stats.wo_* (seeded from the former data.ts). Rebuilds +// /stats/opinions/world 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 { return cached('world-opinions', async () => { diff --git a/src/routes/projects/energy/+page.svelte b/src/lib/stats/EnergyLive.svelte similarity index 69% rename from src/routes/projects/energy/+page.svelte rename to src/lib/stats/EnergyLive.svelte index d56c830..21904a9 100644 --- a/src/routes/projects/energy/+page.svelte +++ b/src/lib/stats/EnergyLive.svelte @@ -1,6 +1,6 @@ - - Energy · Stan Runge - - - -
-
-
-

grid archive / netherlands

-

Energy

-

- Where Dutch electricity comes from hour by hour, how hard each source is running, and how - full the gas storages are. Data from NED.nl, collected hourly. -

-
-
- {#if now} -
{percent.format(now.renewableShare)}renewable now
- {#if now.ef !== null} -
{whole.format(now.ef * 1000)}g CO₂ / kWh
- {/if} - {/if} - {#if latestGas} -
{one.format(latestGas.fill)}%gas storage full
- {/if} -
-
- - {#if !now} -
No electricity data has been collected yet.
- {:else} -
-
-
-

Current mix

- {formatHour.format(now.t)} · {whole.format(now.total)} MWh -
- -
-
-
Low-carbon
-
{percent.format(now.lowCarbonShare)}
-
-
-
Fossil
-
{percent.format(now.fossilShare)}
-
- {#if now.load !== null} -
-
{now.load > now.total ? 'Net import' : 'Net export'}
-
{whole.format(Math.abs(now.total - now.load))} MWh
-
- {/if} -
-
-
-
-

Utilization

- share of installed capacity in use -
- -

- NED's load factor per source. Nuclear and "other" are reported near 100% almost - constantly, so treat those as estimates. -

-
-
- -
-
-

Generation and consumption

- - - {#each Object.entries(rangeLabels) as [value, label] (value)} - {label} - {/each} - - -
- Live grid and gas storage +

+ Where Dutch electricity comes from hour by hour, how hard each source is running, and how full the + gas storages are. Collected hourly from NED.nl; the yearly CBS figures further down remain the + official totals. +

+
+ {#if now} + + {#if now.ef !== null} + -

- Stacked: generation by source. Dashed: consumption (gap above it is export). Red: grid CO₂ - intensity. * = incomplete (still running or hours missing). - {#if range === 'month' && daily.length < 28} - Hourly collection started {formatDate.format(energy.coverage.firstHour ?? now.t)}, so this - fills up over the coming weeks. - {/if} -

-
- - {#if forecast.length} -
-
-

Next days

- NED forecast for wind, solar, generation and consumption -
- -
{/if} + {/if} - -
-
-
-

Gas storage by year

- {#if latestGas} - {one.format(latestGas.twh)} TWh on {formatDate.format(new Date(latestGas.day))} - {/if} -
- -
-
-
-

Gas vs electricity

- monthly -
- -

- NED has no gas consumption series here, so this compares how much electricity is made from - gas with how full storage is. -

-
-
- - {#if energy.gasSites.length} -
-
-

Storage sites

- latest day per site -
- -
+ {#if latestGas} + {/if} - -
-

- {#if energy.coverage.firstHour && energy.coverage.lastHour} - Hourly data {formatHour.format(energy.coverage.firstHour)} – {formatHour.format( - energy.coverage.lastHour - )} ({whole.format(energy.coverage.hours)} hours{missingHours - ? `, ${missingHours} missing in the last week` - : ''}{incompleteHours.length - ? `, ${incompleteHours.length} still incomplete and left out` - : ''}). - {/if} - {#if monthly.length} - Monthly from {formatMonth.format(monthly[0].t)}, yearly from {formatYear.format( - yearly[0]?.t ?? monthly[0].t - )}. - {/if} - {#if energy.gas.length} - Gas storage from {formatDate.format(new Date(energy.gas[0].day))}. - {/if} -

-

- Source: NED.nl. - {#if energy.collector.lastOk} - Last successful collection {relativeTime(energy.collector.lastOk, clock)}{energy.collector - .failedWeek - ? ` · ${energy.collector.failedWeek} failed runs this week` - : ''}. - {/if} -

-
+{#if !now} +

No electricity data has been collected yet.

+{:else} +
+ + {#if now.load !== null} +

+ {now.load > now.total ? 'Net import' : 'Net export'} + {whole.format(Math.abs(now.total - now.load))} MWh +

+ {/if} +
+ + + + + {#each Object.entries(rangeLabels) as [value, label] (value)} + {label} + {/each} + + + + {#if forecast.length} + + {/if} +
+{/if} + +
+ + + {#if energy.gasSites.length} + + {/if} +
+ +

+ {#if energy.coverage.firstHour && energy.coverage.lastHour} + Hourly data {formatHour.format(energy.coverage.firstHour)} – {formatHour.format( + energy.coverage.lastHour + )} ({whole.format(energy.coverage.hours)} hours{missingHours + ? `, ${missingHours} missing in the last week` + : ''}{incompleteHours.length + ? `, ${incompleteHours.length} still incomplete and left out` + : ''}). + {/if} + {#if monthly.length} + Monthly from {formatMonth.format(monthly[0].t)}, yearly from {formatYear.format( + yearly[0]?.t ?? monthly[0].t + )}. + {/if} + {#if energy.gas.length} + Gas storage from {formatDate.format(new Date(energy.gas[0].day))}. + {/if} + {#if energy.collector.lastOk} + Last successful collection {relativeTime(new Date(energy.collector.lastOk), clock)}{energy + .collector.failedWeek + ? ` · ${energy.collector.failedWeek} failed runs this week` + : ''}. + {/if} +

+ diff --git a/src/lib/title.ts b/src/lib/title.ts index c4f5b0a..e4aea7e 100644 --- a/src/lib/title.ts +++ b/src/lib/title.ts @@ -37,12 +37,11 @@ export function getTitle(path: string, environment: SiteEnvironment = null): str '/projects/animaldex/admin': 'Animaldex admin', '/projects/derby': 'Derby Lens', '/projects/cs-prices': 'CS Prices', - '/projects/energy': 'Energy', '/trackers/games': 'Game Tracker', '/projects/language-learner': 'Language Learner', '/projects/spotify-beatmap-finder': 'osu! map finder', '/projects/progress': 'Progress', - '/projects/world-opinions': 'World Opinions', + '/stats/opinions/world': 'World Opinions', '/trackers/money': 'Money' }; const cleaned = path.replace(/\/$/, '') || '/'; diff --git a/src/lib/tracker-icons.ts b/src/lib/tracker-icons.ts index 8b88a82..5962e00 100644 --- a/src/lib/tracker-icons.ts +++ b/src/lib/tracker-icons.ts @@ -370,6 +370,89 @@ export const trackerIcons: Record = { 'M7 22a4 4 0 0 1-4-4 1 1 0 0 1 1-1h16a1 1 0 0 1 1 1 4 4 0 0 1-4 4z', 'M9.159 2.46a1 1 0 0 1 1.521-.193l9.977 8.98A1 1 0 0 1 20 13H4a1 1 0 0 1-.824-1.567z' ] + }, + // /stats sections (header stats menu, home page, stats nav). Keys are `stats-
`. + 'stats-overview': { + from: '#3b82f6', + to: '#6366f1', + ink: WHITE, + paths: ['M3 3v16a2 2 0 0 0 2 2h16', 'm19 9-5 5-4-4-3 3'] + }, + 'stats-economy': { + from: '#10b981', + to: '#0d9488', + ink: WHITE, + paths: ['M16 7h6v6', 'm22 7-8.5 8.5-5-5L2 17'] + }, + 'stats-energy': { + from: '#f97316', + to: '#e11d48', + ink: WHITE, + paths: ['M13 2 3 14h9l-1 8 10-12h-9l1-8z'] + }, + 'stats-gouda': { + from: '#eab308', + to: '#f97316', + ink: WHITE, + paths: [ + 'M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0', + c(12, 10, 3) + ] + }, + 'stats-countries': { + from: '#a855f7', + to: '#ec4899', + ink: WHITE, + paths: [c(12, 12, 10), 'M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20', 'M2 12h20'] + }, + 'stats-curacao': { + from: '#22d3ee', + to: '#0e7490', + ink: WHITE, + paths: [ + 'M13 8c0-2.76-2.46-5-5.5-5S2 5.24 2 8h2l1-1 1 1h4', + 'M13 7.14A5.82 5.82 0 0 1 16.5 6c3.04 0 5.5 2.24 5.5 5h-3l-1-1-1 1h-3', + 'M5.89 9.71c-2.15 2.15-2.3 5.47-.35 7.43l4.24-4.25.7-.7.71-.71 2.12-2.12c-1.95-1.96-5.27-1.8-7.42.35', + 'M11 15.5c.5 2.5-.17 4.5-1 6.5h4c2-5.5-.5-12-1-14' + ] + }, + 'stats-elections': { + from: '#06b6d4', + to: '#3b82f6', + ink: WHITE, + paths: ['m9 12 2 2 4-4', 'M5 7c0-1.1.9-2 2-2h10a2 2 0 0 1 2 2v12H5V7Z', 'M22 19H2'] + }, + 'stats-history': { + from: '#b45309', + to: '#eab308', + ink: WHITE, + paths: [ + 'M15 12h-5', + 'M15 8h-5', + 'M19 17V5a2 2 0 0 0-2-2H4', + 'M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3' + ] + }, + 'stats-opinions': { + from: '#ec4899', + to: '#f97316', + ink: WHITE, + paths: ['M7.9 20A9 9 0 1 0 4 16.1L2 22Z'] + }, + 'stats-polls': { + from: '#f472b6', + to: '#db2777', + ink: WHITE, + paths: ['M3 3v16a2 2 0 0 0 2 2h16', 'M18 17V9', 'M13 17V5', 'M8 17v-3'] + }, + 'stats-world': { + from: '#38bdf8', + to: '#0369a1', + ink: WHITE, + paths: [ + c(12, 12, 10), + 'm16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z' + ] } }; diff --git a/src/lib/world-opinions/data.test.ts b/src/lib/world-opinions/data.test.ts deleted file mode 100644 index cc89f3a..0000000 --- a/src/lib/world-opinions/data.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import world from 'world-atlas/countries-110m.json'; -import seed from '../../../scripts/stats/world-opinions-seed.json'; -import { buildWorldOpinions, type WorldOpinionsData } from './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); - }); - - it('matches every country to a shape on the map', () => { - const shapes = new Set( - world.objects.countries.geometries.map((shape: { id?: string }) => String(shape.id)) - ); - for (const country of countries) expect(shapes.has(country.iso), country.name).toBe(true); - }); - - it('only references known countries', () => { - const referenced = [ - ...profiles.map((profile) => profile.id), - ...partyCountries, - ...parties.map((party) => party.country), - ...narratives.flatMap((narrative) => narrative.views.map((view) => view.country)) - ]; - for (const id of referenced) expect(countryById.has(id), id).toBe(true); - }); - - it('keeps party positions on the 0–10 scale with unique keys', () => { - const keys = new Set(parties.map((party) => `${party.country}-${party.short}`)); - expect(keys.size).toBe(parties.length); - for (const party of parties) { - for (const value of [party.econ, party.social]) { - expect(value).toBeGreaterThanOrEqual(0); - expect(value).toBeLessThanOrEqual(10); - } - } - for (const id of partyCountries) { - expect( - parties.some((party) => party.country === id), - id - ).toBe(true); - } - }); -}); diff --git a/src/lib/world-opinions/data.ts b/src/lib/world-opinions/data.ts index 74124bd..80221f1 100644 --- a/src/lib/world-opinions/data.ts +++ b/src/lib/world-opinions/data.ts @@ -2,7 +2,7 @@ // 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 +// stats_wo_seed migration) and is loaded by /stats/opinions/world; this file keeps // the types, the label dictionaries and the lookups the components share. import { getContext, setContext } from 'svelte'; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 52fed43..e3408ef 100755 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -152,7 +152,11 @@ >stats
diff --git a/src/routes/projects/energy/+page.server.ts b/src/routes/projects/energy/+page.server.ts deleted file mode 100644 index 4b01d10..0000000 --- a/src/routes/projects/energy/+page.server.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { loadEnergy } from '#lib/server/energy.js'; -import type { PageServerLoad } from './$types'; - -export const load: PageServerLoad = async () => ({ energy: await loadEnergy() }); diff --git a/src/routes/projects/energy/[...rest]/+server.ts b/src/routes/projects/energy/[...rest]/+server.ts new file mode 100644 index 0000000..90d2080 --- /dev/null +++ b/src/routes/projects/energy/[...rest]/+server.ts @@ -0,0 +1,7 @@ +import { redirect } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +// Merged into /stats (2026-10-26): every old URL, subpaths included, moves permanently. +export const GET: RequestHandler = ({ url }) => { + redirect(301, `/stats/energy${url.search}#live`); +}; diff --git a/src/routes/projects/world-opinions/[...rest]/+server.ts b/src/routes/projects/world-opinions/[...rest]/+server.ts new file mode 100644 index 0000000..5ca8712 --- /dev/null +++ b/src/routes/projects/world-opinions/[...rest]/+server.ts @@ -0,0 +1,7 @@ +import { redirect } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +// Merged into /stats (2026-10-26): every old URL, subpaths included, moves permanently. +export const GET: RequestHandler = ({ url }) => { + redirect(301, `/stats/opinions/world${url.search}`); +}; diff --git a/src/routes/sitemap.xml/+server.ts b/src/routes/sitemap.xml/+server.ts index a608f31..c88af49 100644 --- a/src/routes/sitemap.xml/+server.ts +++ b/src/routes/sitemap.xml/+server.ts @@ -22,6 +22,7 @@ const staticPaths = [ '/stats/countries/CUW', '/stats/elections', '/stats/opinions', + '/stats/opinions/world', '/stats/history', '/stats/countries/NLD', '/stats/countries/USA', @@ -47,10 +48,8 @@ const staticPaths = [ '/projects/game-tracker/osu', '/projects/game-tracker/pokemon/locations', '/projects/animaldex', - '/projects/energy', '/projects/derby', '/projects/spotify-beatmap-finder', - '/projects/world-opinions', '/projects/language-learner', ...COURSES.map((c) => `/projects/language-learner/${c.code}`) ]; diff --git a/src/routes/stats/+layout.svelte b/src/routes/stats/+layout.svelte index a7a3bc9..8c83f75 100644 --- a/src/routes/stats/+layout.svelte +++ b/src/routes/stats/+layout.svelte @@ -1,44 +1,43 @@
+ {#if subTabs} + + {/if} {@render children()}
@@ -79,37 +78,13 @@ border-color: var(--site-text); color: var(--site-bg); } - .tab-icon { - display: inline-grid; - place-items: center; - width: 1.4rem; - height: 1.4rem; - border-radius: 999px; - color: #fff; + .stats-subtabs { + margin-top: -0.9rem; + border-bottom-style: dashed; } - .tab-overview { - background: linear-gradient(135deg, #3b82f6, #6366f1); - } - .tab-economy { - background: linear-gradient(135deg, #10b981, #0d9488); - } - .tab-energy { - background: linear-gradient(135deg, #f97316, #e11d48); - } - .tab-gouda { - background: linear-gradient(135deg, #eab308, #f97316); - } - .tab-countries { - background: linear-gradient(135deg, #a855f7, #ec4899); - } - .tab-elections { - background: linear-gradient(135deg, #06b6d4, #3b82f6); - } - .tab-history { - background: linear-gradient(135deg, #b45309, #eab308); - } - .tab-opinions { - background: linear-gradient(135deg, #ec4899, #f97316); + .stats-subtabs a { + font-size: 0.8rem; + padding: 0.25rem 0.65rem 0.25rem 0.3rem; } /* Shared page furniture for every /stats page. */ .stats-page :global(.stats-intro) { diff --git a/src/routes/stats/+page.svelte b/src/routes/stats/+page.svelte index 2aae6cc..64b530c 100644 --- a/src/routes/stats/+page.svelte +++ b/src/routes/stats/+page.svelte @@ -4,15 +4,8 @@ import StatChart from '#lib/stats/StatChart.svelte'; import { change, latest, lineOption, spread, type StatPoint } from '#lib/stats/chart.js'; import { ago, compact, num, period } from '#lib/stats/format.js'; - import Landmark from 'lucide-svelte/icons/landmark'; - import Flame from 'lucide-svelte/icons/flame'; - import House from 'lucide-svelte/icons/house'; - import Globe from 'lucide-svelte/icons/globe'; - import Palmtree from 'lucide-svelte/icons/tree-palm'; - import Vote from 'lucide-svelte/icons/vote'; - import MessageCircle from 'lucide-svelte/icons/message-circle'; - import ScrollText from 'lucide-svelte/icons/scroll-text'; import ArrowRight from 'lucide-svelte/icons/arrow-right'; + import TrackerIcon from '#lib/components/TrackerIcon.svelte'; let { data } = $props(); const s = $derived(data.series); @@ -50,57 +43,49 @@ { href: '/stats/economy', title: 'Economy', - icon: Landmark, - grad: 'linear-gradient(135deg,#3b82f6,#a855f7)', + icon: 'stats-economy', text: 'Central bank rates, inflation, bond yields and spreads, stock markets, growth, government debt and deficits, migration.' }, { href: '/stats/energy', title: 'Energy', - icon: Flame, - grad: 'linear-gradient(135deg,#eab308,#f97316)', + icon: 'stats-energy', text: 'Dutch household tariffs, Europe vs the US, oil and gas, import dependence, renewables, heat pumps, solar and EVs.' }, { href: '/stats/gouda', title: 'Gouda', - icon: House, - grad: 'linear-gradient(135deg,#10b981,#06b6d4)', + icon: 'stats-gouda', text: 'Population since 1942, house prices, housing stock, municipal levies and neighbourhoods, next to the surrounding towns.' }, { href: '/stats/countries', title: 'Countries', - icon: Globe, - grad: 'linear-gradient(135deg,#a855f7,#ec4899)', + icon: 'stats-countries', text: 'The Netherlands, Curaçao, the US, South Korea, China and more side by side: income, growth, inflation, debt and life expectancy.' }, { href: '/stats/countries/CUW', title: 'Curaçao', - icon: Palmtree, - grad: 'linear-gradient(135deg,#06b6d4,#3b82f6)', + icon: 'stats-curacao', text: 'Population, income, inflation, tourism, trade and government finances of Curaçao.' }, { href: '/stats/elections', title: 'Elections', - icon: Vote, - grad: 'linear-gradient(135deg,#06b6d4,#3b82f6)', + icon: 'stats-elections', text: 'Every Dutch general election since 1918, Gouda down to the polling station, and the US presidential vote by state.' }, { href: '/stats/history', title: 'History and culture', - icon: ScrollText, - grad: 'linear-gradient(135deg,#b45309,#eab308)', + icon: 'stats-history', text: 'The Netherlands, Gouda, Curaçao, the US and Korea over the long run, a sourced timeline, office holders, religion, holidays, UNESCO sites and famous people.' }, { href: '/stats/opinions', title: 'Opinions', - icon: MessageCircle, - grad: 'linear-gradient(135deg,#ec4899,#f97316)', + icon: 'stats-opinions', text: 'Seat and approval polls, Eurobarometer worries and trust, world happiness, search trends, news themes and party positions.' } ] as const; @@ -203,9 +188,8 @@
{#each SECTIONS as sec (sec.href)} - {@const Icon = sec.icon} - + {sec.title} {sec.text} @@ -337,15 +321,6 @@ .section-card:hover { border-color: var(--site-text); } - .section-icon { - display: inline-grid; - place-items: center; - flex: none; - width: 2.5rem; - height: 2.5rem; - border-radius: 0.75rem; - color: #fff; - } .section-body { display: grid; gap: 0.2rem; diff --git a/src/routes/stats/energy/+page.server.ts b/src/routes/stats/energy/+page.server.ts index 44aeb6b..42ae5f3 100644 --- a/src/routes/stats/energy/+page.server.ts +++ b/src/routes/stats/energy/+page.server.ts @@ -1,4 +1,5 @@ import { cached, loadSeries } from '#lib/server/stats.js'; +import { loadEnergy } from '#lib/server/energy.js'; import { epSeriesIds } from '#lib/stats/energylabels.js'; import type { PageServerLoad } from './$types'; @@ -6,6 +7,8 @@ const EU = ['NL', 'DE', 'FR', 'BE', 'IT', 'ES', 'PL', 'SE', 'DK', 'AT', 'EU27_20 const RENEW = ['total', 'wind-onshore', 'wind-offshore', 'solar', 'biomass', 'hydro']; export const load: PageServerLoad = async () => ({ + // Live NED grid + gas storage (read-only, ned-collector's `energy` schema). + energy: await cached('stats:energy:live', loadEnergy), series: await cached('stats:energy', () => loadSeries([ ...['variable', 'tax', 'transport', 'fixed', 'taxcredit', 'dynamic'].map( diff --git a/src/routes/stats/energy/+page.svelte b/src/routes/stats/energy/+page.svelte index 252106b..e7d3039 100644 --- a/src/routes/stats/energy/+page.svelte +++ b/src/routes/stats/energy/+page.svelte @@ -1,5 +1,4 @@ -
-
-

world opinions

-

- Same words, different politics. Hover the map, click a country. -

-
+ + world opinions · stats · Stan Runge + + +
+ stats / opinions / world +

World opinions

+

+ Same words, different politics. Hover the map, click a country. For measured party positions + (Chapel Hill Expert Survey, V-Party) see where Dutch parties stand; for polls, happiness and Eurobarometer see opinions. +

+
+ +
{#each modes as item (item.id)} @@ -70,20 +83,26 @@
-
-

party compass

-

Every party on one shared scale.

+
+

Party compass

+

+ Every party on one shared scale, hand-placed for comparison. Measured CHES and V-Party + positions for Dutch parties are on opinions. +

-

values map

+

Values map

Both value axes at once.

-

one event, many stories

+

One event, many stories

{#each narratives as item, index (item.event)}