Cannabis Freedom Index

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; }

    ↑↓ to move, ↵ to open.