Cannabis Freedom Index

Changelog 82a4635

#191 leave use-to-freedom ratios undefined at zero

6 files of code, design or docs +31 −6 lines

Code, design and documents 6 files

docs/research/issue-corrections-186-204-2026-10-27.md +8 −0
@@ -18,2 +18,10 @@ series requires matched local numerator/denominator counts over identical comple
 intervals, validated historical API availability and rate limits; it must use a
 separate definition/file. Do not rescale these percentages to pretend to recover it.
+
+## #191 — Exact use/freedom quotient
+
+Zero index scores have an undefined quotient, represented by null and a hatched map
+with an explicit tooltip explanation. Positive scores below one retain their actual
+unrounded denominator. Regression covers scores 0, 0.5 and 1 plus missing prevalence.
+The map lede and legend state the zero-score exclusion. Survey ages/years and estimate
+markers remain qualified; this ratio is not an age-standardised comparison.

package.json +1 −1
@@ -14,5 +14,5 @@
     "history:test": "node --test scripts/history-scope.test.cjs",
     "seo:audit": "node scripts/seo-audit.mjs",
-    "answers:test": "node --test scripts/answers.test.mjs scripts/profile-claims.test.mjs",
+    "answers:test": "node --test scripts/answers.test.mjs scripts/profile-claims.test.mjs scripts/use-freedom-ratio.test.mjs",
     "survey:test": "node --test scripts/survey.test.mjs",
     "articles:backlog": "node scripts/article-backlog.cjs",

scripts/use-freedom-ratio.test.mjs +10 −0
@@ -0,0 +1,10 @@
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { useFreedomRatio } from '../src/lib/use-freedom-ratio.js';
+
+test('zero freedom is undefined; positive scores below one keep their real denominator (#191)', () => {
+  assert.equal(useFreedomRatio(5, 0), null);
+  assert.equal(useFreedomRatio(5, 0.5), 10);
+  assert.equal(useFreedomRatio(5, 1), 5);
+  assert.equal(useFreedomRatio(null, 1), null);
+});

src/components/HomePage.astro +6 −4
@@ -86,13 +86,14 @@ const MED_ORDER = ["yes", "limited", "no"];
 // What the browser needs for the map: scores only, no prose.
 // `ratio` is the share of people who use cannabis divided by the index score: how far use runs
-// ahead of freedom. A score below 1 counts as 1, so that a zero does not divide.
+// ahead of freedom. A zero score has no defined quotient and is shown as unavailable.
 // `culture` is how many atlas entries a country appears in, with the names of the first few
 // (most important first), for the map's Culture view.
 // `ds` holds the country's figures from the outside datasets (datasets.js), for the Society views.
+import { useFreedomRatio } from "../lib/use-freedom-ratio.js";
 const mapData = places.map((c) => {
   const { id, num, name, flag, raw, score, rank, s, use, planting, clubs, driving, visitors, rec, med } = c;
   const atlas = cultureOf(c);
   return { id, num, name, flag, raw, score, rank, s, planting, clubs, driving, visitors, rec, med, fe: estimateNote(c), prices: SHOW_PRICES ? flatLatestPricesFor(id) : {},
-    use: { ...use, ages: useAges(use), ratio: +(use.prevalence / Math.max(raw, 1)).toFixed(2), culture: atlas.length },
+    use: { ...use, ages: useAges(use), ratio: useFreedomRatio(use.prevalence, raw), culture: atlas.length },
     atlas: atlas.slice(0, 4).map((e) => e.title), ds: datasetMapValues(c) };
 });
@@ -111,5 +112,5 @@ const USE_SCALES = {
     label: "Use relative to freedom",
     steps: [[0, "Under 0.15"], [0.15, "0.15–0.3"], [0.3, "0.3–0.5"], [0.5, "0.5–1"], [1, "1 or more"]],
-    note: "Survey-age cannabis-use share divided by the index score. Ages and survey years vary. Rates are not age-standardised; do not compare countries directly.",
+    note: "Survey-age cannabis-use share divided by the unrounded index score. Undefined at score zero (hatched on the map). Ages and survey years vary. Rates are not age-standardised; do not compare countries directly.",
   },
   culture: {
@@ -937,6 +938,7 @@ const columnGroups = [["Shown by default", BASE_COLUMNS], ...["Score segments",
         const usersText = c.use.users == null ? "—" : `${c.use.estimate || c.use.users_approximate ? "~" : ""}${compact(c.use.users)}`;
         if (metric === "ratio") {
-          return head(c.flag, c.name, `${rateMark}${c.use.ratio}`, useColor(c))
+          return head(c.flag, c.name, c.use.ratio == null ? null : `${rateMark}${c.use.ratio}`, useColor(c))
             + row(`Share aged ${c.use.ages}`, `${rateMark}${c.use.prevalence}%`) + row("Index", `${c.score} / 100`)
+            + (c.use.ratio == null ? para("Ratio unavailable: the index score is zero.") : "")
             + para(USE_SCALES.ratio.note) + foot(c);
         }

src/lib/map-views.js +1 −1
@@ -23,5 +23,5 @@ export const MAP_VIEWS = [
     lede: "The share of people in each survey’s age band who used cannabis in the past year. Ages and years vary, and some surveys include children. Rates are not age-standardised; do not compare countries directly. The stronger the blue, the higher the reported share." },
   { slug: "use-against-freedom", metric: "ratio", title: "Cannabis use against cannabis freedom, by country",
-    lede: "The share of people who use cannabis divided by the country's index score: where use runs far ahead of what the law allows." },
+    lede: "The share of people who use cannabis divided by the country's unrounded index score (undefined at zero): where use runs far ahead of what the law allows." },
   { slug: "culture", metric: "culture", title: "Cannabis culture around the world",
     lede: "How much of each country's cannabis culture the atlas covers: the words for the plant, the customs and faiths it belongs to, and the people who shaped its laws. The stronger the blue, the more entries." },

src/lib/use-freedom-ratio.js +5 −0
@@ -0,0 +1,5 @@
+// A zero index has no finite quotient. Do not add a pseudocount to the score.
+export function useFreedomRatio(prevalence, score) {
+  if (!Number.isFinite(prevalence) || !Number.isFinite(score) || score <= 0) return null;
+  return +(prevalence / score).toFixed(2);
+}

    ↑↓ to move, ↵ to open.