Changelog 0ea63bctree, with Claude Opus 5.5
Regions: Schengen, Nordic, ASEAN, Gulf, CARICOM, Mercosur and G7
Seven groupings beside the EU, memberships as of 2026 (Timor-Leste in ASEAN, Bolivia in Mercosur; Montserrat, with no profile yet, left out of CARICOM). The regions index chart now switches between continents and groups of countries, each with the world line, so neither is crowded.
3 files of code, design or docs +71 −24 lines
Code, design and documents 3 files
src/lib/regions.js +36 −1
@@ -6,5 +6,5 @@ import { countries, places, changes, historyOf, worldAvg, REC_ORDER, MED_LABEL } // Groupings beyond the continents. `members` are country file names; a new grouping needs only -// an entry here. EU: the 27 member states since 2020. +// an entry here. Memberships as of 2026; `of` is the continent, left out for one that spans several. const GROUPINGS = [ { @@ -13,4 +13,39 @@ const GROUPINGS = [ members: ["at", "be", "bg", "hr", "cy", "cz", "dk", "ee", "fi", "fr", "de", "gr", "hu", "ie", "it", "lv", "lt", "lu", "mt", "nl", "pl", "pt", "ro", "sk", "si", "es", "se"], }, + { + slug: "schengen", name: "Schengen Area", short: "Schengen", of: "Europe", + about: "The 29 countries without border checks between them, Bulgaria and Romania fully since 2025: a traveller crosses from a country that tolerates cannabis into one that punishes it without a passport check.", + members: ["at", "be", "bg", "hr", "cz", "dk", "ee", "fi", "fr", "de", "gr", "hu", "is", "it", "lv", "li", "lt", "lu", "mt", "nl", "no", "pl", "pt", "ro", "sk", "si", "es", "se", "ch"], + }, + { + slug: "nordic", name: "Nordic countries", short: "Nordic", of: "Europe", + about: "Denmark, Finland, Iceland, Norway and Sweden, with Greenland and the Faroe Islands.", + members: ["dk", "fi", "is", "no", "se", "gl", "fo"], + }, + { + slug: "asean", name: "ASEAN", short: "ASEAN", of: "Asia", + about: "The 11 members of the Association of Southeast Asian Nations, Timor-Leste the newest since 2025.", + members: ["bn", "kh", "id", "la", "my", "mm", "ph", "sg", "th", "vn", "tl"], + }, + { + slug: "gcc", name: "Gulf Cooperation Council", short: "Gulf", of: "Asia", + about: "The six monarchies of the Gulf Cooperation Council: Bahrain, Kuwait, Oman, Qatar, Saudi Arabia and the United Arab Emirates.", + members: ["ae", "bh", "kw", "om", "qa", "sa"], + }, + { + slug: "caricom", name: "Caribbean Community", short: "CARICOM", of: "North America", + about: "The full members of the Caribbean Community, from the Bahamas to Suriname and Guyana. Montserrat, a British territory and also a member, has no profile here yet.", + members: ["ag", "bs", "bb", "bz", "dm", "gd", "gy", "ht", "jm", "kn", "lc", "vc", "sr", "tt"], + }, + { + slug: "mercosur", name: "Mercosur", short: "Mercosur", of: "South America", + about: "The full members of the Southern Common Market: Argentina, Bolivia, Brazil, Paraguay and Uruguay. Venezuela's membership is suspended.", + members: ["ar", "bo", "br", "py", "uy"], + }, + { + slug: "g7", name: "Group of Seven", short: "G7", + about: "The seven large economies of the G7: Canada, France, Germany, Italy, Japan, the United Kingdom and the United States.", + members: ["ca", "fr", "de", "it", "jp", "gb", "us"], + }, ];
src/pages/region/index.astro +34 −23
@@ -20,17 +20,18 @@ const Y0 = 1900, Y1 = +lastUpdated.slice(0, 4); const yearEnd = (history, year) => { const t = Date.UTC(year, 11, 31, 23, 59); let v = history[0].before; for (const p of history) { if (dateTime(p.date) > t) break; v = p.after; } return v; }; const years = Array.from({ length: Y1 - Y0 + 1 }, (_, i) => Y0 + i); -const lines = [ - ...regions.map((r) => ({ slug: r.slug, name: r.short, values: years.map((y) => yearEnd(r.series.history, y)) })), - { slug: "world", name: "World", values: years.map((y) => yearEnd(averageHistory, y)), world: true }, -]; +const world = { slug: "world", name: "World", values: years.map((y) => yearEnd(averageHistory, y)), world: true }; const CW = 960, CH = 360, PL = 34, PR = 120, PT = 12, PB = 28; -const top = Math.ceil(Math.max(...lines.flatMap((l) => l.values)) / 10) * 10; const cx = (y) => PL + ((y - Y0) / (Y1 - Y0)) * (CW - PL - PR); -const cy = (v) => PT + (1 - v / top) * (CH - PT - PB); -for (const l of lines) l.d = l.values.map((v, i) => `${i ? "L" : "M"}${cx(years[i]).toFixed(1)},${cy(v).toFixed(1)}`).join(""); -// the names at the right end, pushed apart where lines end close together -const labels = [...lines].sort((a, b) => b.values.at(-1) - a.values.at(-1)).map((l) => ({ l, y: cy(l.values.at(-1)) })); -for (let i = 1; i < labels.length; i++) labels[i].y = Math.max(labels[i].y, labels[i - 1].y + 15); -const gridY = Array.from({ length: top / 10 + 1 }, (_, i) => i * 10); +// two charts behind a switch, continents and groups of countries, each with the world line +const sets = [["continent", "Continents"], ["grouping", "Groups of countries"]].map(([kind, label]) => { + const lines = [...regions.filter((r) => r.kind === kind).map((r) => ({ slug: r.slug, name: r.short, values: years.map((y) => yearEnd(r.series.history, y)) })), world]; + const top = Math.ceil(Math.max(...lines.flatMap((l) => l.values)) / 10) * 10; + const cy = (v) => PT + (1 - v / top) * (CH - PT - PB); + const drawn = lines.map((l) => ({ ...l, d: l.values.map((v, i) => `${i ? "L" : "M"}${cx(years[i]).toFixed(1)},${cy(v).toFixed(1)}`).join("") })); + // the names at the right end, pushed apart where lines end close together + const labels = [...drawn].sort((a, b) => b.values.at(-1) - a.values.at(-1)).map((l) => ({ l, y: cy(l.values.at(-1)) })); + for (let i = 1; i < labels.length; i++) labels[i].y = Math.max(labels[i].y, labels[i - 1].y + 15); + return { kind, label, lines: drawn, labels, cy, gridY: Array.from({ length: top / 10 + 1 }, (_, i) => i * 10) }; +}); const gridX = years.filter((y) => y % 20 === 0); --- @@ -49,16 +50,21 @@ const gridX = years.filter((y) => y % 20 === 0); <p class="sub">The average score of each region's countries at the end of each year, rebuilt from the recorded history. Point at a line to pick it out.</p> </div></div> - <figure class="rgp-trend" data-rgp-trend> - <svg viewBox={`0 0 ${CW} ${CH}`} role="img" aria-label={`Average score by region, ${Y0} to ${Y1}: ${lines.map((l) => `${l.name} ${l.values.at(-1).toFixed(1)}`).join(", ")}`}> - {gridY.map((v) => <g class="rgp-grid"><line x1={PL} x2={CW - PR} y1={cy(v)} y2={cy(v)} /><text x={PL - 8} y={cy(v) + 4} text-anchor="end">{v}</text></g>)} - {gridX.map((y) => <text class="rgp-axis" x={cx(y)} y={CH - 8} text-anchor="middle">{y}</text>)} - {lines.map((l) => <path class:list={["rgp-line", { "rgp-world": l.world }]} d={l.d} data-r={l.slug}><title>{l.name}: {l.values.at(-1).toFixed(1)} in {Y1}</title></path>)} - {labels.map(({ l, y }) => ( - <a href={l.world ? `${base}charts/` : `${base}region/${l.slug}/`} data-r={l.slug} class="rgp-label"> - <text x={CW - PR + 8} y={y + 4}>{l.name} <tspan>{l.values.at(-1).toFixed(1)}</tspan></text> - </a> - ))} - </svg> - </figure> + <div class="avg-switch rgp-switch" role="group" aria-label="Regions shown"> + {sets.map((t, i) => <button type="button" data-set={t.kind} aria-pressed={i === 0 ? "true" : "false"}>{t.label}</button>)} + </div> + {sets.map((t, i) => ( + <figure class="rgp-trend" data-rgp-trend data-set={t.kind} hidden={i > 0}> + <svg viewBox={`0 0 ${CW} ${CH}`} role="img" aria-label={`Average score, ${t.label.toLowerCase()}, ${Y0} to ${Y1}: ${t.lines.map((l) => `${l.name} ${l.values.at(-1).toFixed(1)}`).join(", ")}`}> + {t.gridY.map((v) => <g class="rgp-grid"><line x1={PL} x2={CW - PR} y1={t.cy(v)} y2={t.cy(v)} /><text x={PL - 8} y={t.cy(v) + 4} text-anchor="end">{v}</text></g>)} + {gridX.map((y) => <text class="rgp-axis" x={cx(y)} y={CH - 8} text-anchor="middle">{y}</text>)} + {t.lines.map((l) => <path class:list={["rgp-line", { "rgp-world": l.world }]} d={l.d} data-r={l.slug}><title>{l.name}: {l.values.at(-1).toFixed(1)} in {Y1}</title></path>)} + {t.labels.map(({ l, y }) => ( + <a href={l.world ? `${base}charts/` : `${base}region/${l.slug}/`} data-r={l.slug} class="rgp-label"> + <text x={CW - PR + 8} y={y + 4}>{l.name} <tspan>{l.values.at(-1).toFixed(1)}</tspan></text> + </a> + ))} + </svg> + </figure> + ))} </section> @@ -95,3 +101,8 @@ const gridX = years.filter((y) => y % 20 === 0); fig.addEventListener("mouseleave", () => { fig.classList.remove("picking"); set(null); }); } + // the switch between continents and groups of countries + for (const btn of document.querySelectorAll(".rgp-switch button")) btn.addEventListener("click", () => { + btn.parentElement.querySelectorAll("button").forEach((b) => b.setAttribute("aria-pressed", String(b === btn))); + document.querySelectorAll("[data-rgp-trend]").forEach((f) => { f.hidden = f.dataset.set !== btn.dataset.set; }); + }); </script>
src/styles/global.css +1 −0
@@ -3623,2 +3623,3 @@ td.rgp-seg i { display: inline-block; width: 7px; height: 7px; margin-right: 5px .rgp-line.on { stroke: var(--text); stroke-width: 2.6; } .rgp-label.on text { fill: var(--text); } +.rgp-switch { margin: 0 0 14px; }