Code, design and documents 10 files
src/components/CountryArticle.astro +1 −1
@@ -219,5 +219,5 @@ const circ = 2 * Math.PI * r;
</>
)}
- {updated && <p class="c-updated">Last updated: <time datetime={updated}>{updatedText}</time></p>}
+ {updated && <p class="c-updated">Last updated: <time datetime={updated}>{updatedText}</time>{d && ` · ${d.sources.length} sources`}{scored.length > 0 && ` · history: ${history.filter((ch) => ch.confidence.key === "checked").length} of ${history.length} entries checked`}</p>}
</div>
</article>
src/components/CountryChanges.astro +2 −1
@@ -13,5 +13,5 @@ const base = import.meta.env.BASE_URL;
<li>
<a href={`${base}history/${ch.id}/`}>
- <time datetime={ch.date}>{changeDate(ch)}</time>
+ <time datetime={ch.date}>{changeDate(ch)} <span class={`conf-mark c-${ch.confidence.key}`} title={`${ch.confidence.label}: ${ch.confidence.help}`}>{ch.confidence.mark}</span></time>
<b><span class="kind" title={ch.kind.label} aria-hidden="true">{ch.kind.emoji}</span><span class="visually-hidden">{ch.kind.label}: </span>{ch.title}</b>
{ch.score && <span class="c-change-score">
@@ -23,3 +23,4 @@ const base = import.meta.env.BASE_URL;
))}
</ul>
+ <p class="c-changes-note">{history.filter((ch) => ch.confidence.key === "checked").length} of {history.length} checked against a source</p>
</div>
src/components/CountryChart.astro +2 −3
@@ -2,5 +2,5 @@
// A country's index score over time: a step line that moves at each of its recorded changes.
// No script, so it also works when the article is loaded into the drawer.
-import { changeDate, dateTime, lastUpdated, ESTIMATE_WINDOW } from "../lib/countries.js";
+import { changeDate, dateTime, lastUpdated } from "../lib/countries.js";
const { history } = Astro.props; // the country's changes, newest first
@@ -8,7 +8,6 @@ const base = import.meta.env.BASE_URL;
const time = dateTime;
-const [estFrom, estTo] = ESTIMATE_WINDOW;
// Where a change sits in time. An estimated one runs across the whole window.
-const span0 = (ch) => (ch.estimate ? [Date.UTC(estFrom, 0, 1), Date.UTC(estTo, 0, 1)] : [time(ch.date), time(ch.date)]);
+const span0 = (ch) => (ch.estimate ? ch.window.map((y) => Date.UTC(y, 0, 1)) : [time(ch.date), time(ch.date)]);
const all = [...history].reverse();
src/components/HistoryList.astro +1 −1
@@ -46,5 +46,5 @@ const years = [...new Set(items.map((ch) => ch.date.slice(0, 4)))]
<p class="change-meta">
<time datetime={ch.date}>{formatDate(ch.date)}</time>
- <span>{ch.place.flag} {ch.place.name}</span>
+ <span>{ch.place.flag} {ch.place.name} <span class={`conf-mark c-${ch.confidence.key}`} title={`${ch.confidence.label}: ${ch.confidence.help}`}>{ch.confidence.mark}</span></span>
</p>
<h3><span class="kind" title={ch.kind.label} aria-hidden="true">{ch.kind.emoji}</span><span class="visually-hidden">{ch.kind.label}: </span>{ch.title}</h3>
src/content.config.ts +6 −1
@@ -131,5 +131,10 @@ const changes = defineCollection({
// at some point between the 1925 and 1961 treaties, and `date` is the start of that window
estimate: z.boolean().optional(),
- source,
+ // the years an estimated change is assumed to fall between, when better than the default window
+ window: z.tuple([z.number().int(), z.number().int()]).optional(),
+ // true once the entry's date and event have been compared with a source that was actually
+ // read. Without it the entry is listed as "not yet checked": it may rest on memory.
+ checked: z.boolean().optional(),
+ sources: z.array(source).min(1), // the more the better; the first is shown where there is room for one
})
.strict()
src/lib/countries.js +17 −9
@@ -132,4 +132,14 @@ export function subunitsOf(id) {
}
+// Where no first law is known for a country, its first restriction is an estimate: it is assumed
+// to have come by default between the 1925 Geneva convention and the 1961 Single Convention.
+// An entry can narrow this with its own `window`.
+export const ESTIMATE_WINDOW = [1925, 1961];
+// How far an entry can be trusted. Before-scores are editorial estimates in every case.
+export const CONFIDENCE = {
+ checked: { key: "checked", mark: "✓", label: "Checked", help: "The date and the event were compared with a source that was read" },
+ unchecked: { key: "unchecked", mark: "?", label: "Not yet checked", help: "Written from general knowledge; the source listed has not been checked against it" },
+ estimated: { key: "estimated", mark: "≈", label: "Estimated", help: "No first law is recorded; the date is an assumption" },
+};
// ── Changes: developments that moved a score, newest first ──
// Each entry records the segment scores before the change. The scores after it are the "before"
@@ -145,5 +155,5 @@ export const changes = changeEntries
// context only (a treaty, report or ruling): part of the history, but it moves no score
const kind = CHANGE_CATEGORIES[data.category]; // { emoji, label, help }
- if (!data.before) return { ...data, kind, country, place: country ?? { flag: "🌐", name: "International" }, source: pair(data.source), score: null, segments: [] };
+ if (!data.before) return { ...data, kind, country, place: country ?? { flag: "🌐", name: "International" }, sources: data.sources.map(pair), source: pair(data.sources[0]), confidence: CONFIDENCE[data.estimate ? "estimated" : data.checked ? "checked" : "unchecked"], score: null, segments: [] };
const next = changeEntries.slice(i + 1).find((x) => x.country === data.country && x.before);
const before = scoreList(data.before);
@@ -153,8 +163,9 @@ export const changes = changeEntries
...data,
kind,
+ window: data.estimate ? data.window ?? ESTIMATE_WINDOW : null, // the years an estimate is spread over
country,
place: country,
latest: !next, // the country's most recent change: "after" is its current score
- source: pair(data.source),
+ sources: data.sources.map(pair), source: pair(data.sources[0]), confidence: CONFIDENCE[data.estimate ? "estimated" : data.checked ? "checked" : "unchecked"],
raw: { before: scoreOf(before), after: scoreOf(after) },
score: { before: Math.round(scoreOf(before)), after: Math.round(scoreOf(after)) },
@@ -165,7 +176,4 @@ export const changes = changeEntries
.reverse();
-// Where no first law is known for a country, its first restriction is an estimate: it is assumed
-// to have come at some point between the 1925 Geneva convention and the 1961 Single Convention.
-export const ESTIMATE_WINDOW = [1925, 1961];
// A country's population at a moment in time, on a straight line between its snapshots.
@@ -184,5 +192,4 @@ function populationAt(c, t) {
// a step. An estimated one rises or falls in a straight line across the estimate window, so that
// a guess never shows up as a sudden drop on one day.
-const [estFrom, estTo] = ESTIMATE_WINDOW.map((y) => Date.UTC(y, 0, 1));
const scoredByCountry = new Map(countries.map((c) => [c, changes.filter((ch) => ch.country === c && ch.score).reverse()])); // oldest first
function scoreAt(c, t) {
@@ -192,6 +199,7 @@ function scoreAt(c, t) {
for (const ch of list) {
if (ch.estimate) {
- if (t <= estFrom) break;
- value = t >= estTo ? ch.raw.after : ch.raw.before + ((ch.raw.after - ch.raw.before) * (t - estFrom)) / (estTo - estFrom);
+ const [from, to] = ch.window.map((y) => Date.UTC(y, 0, 1));
+ if (t <= from) break;
+ value = t >= to ? ch.raw.after : ch.raw.before + ((ch.raw.after - ch.raw.before) * (t - from)) / (to - from);
} else if (t >= dateTime(ch.date)) value = ch.raw.after;
else break;
@@ -271,5 +279,5 @@ export const formatDate = (iso) =>
iso.length === 4 ? iso : new Date(`${iso.length === 7 ? iso + "-01" : iso}T00:00:00Z`).toLocaleDateString("en-GB", { day: iso.length === 7 ? undefined : "numeric", month: "long", year: "numeric", timeZone: "UTC" });
// How an entry's date is shown: an estimated one as its window.
-export const changeDate = (ch) => (ch.estimate ? `Between ${ESTIMATE_WINDOW[0]} and ${ESTIMATE_WINDOW[1]} (estimate)` : formatDate(ch.date));
+export const changeDate = (ch) => (ch.estimate ? `Between ${ch.window[0]} and ${ch.window[1]} (estimate)` : formatDate(ch.date));
src/lib/exports.js +3 −2
@@ -36,7 +36,8 @@ export const historyRows = changes.map((ch) => ({
score_before: ch.score?.before ?? "",
score_after: ch.score?.after ?? "",
+ confidence: ch.confidence.key,
date_estimated: Boolean(ch.estimate),
- source_title: ch.source[0],
- source_url: ch.source[1],
+ sources: ch.sources.map(([title]) => title).join(" | "),
+ source_urls: ch.sources.map(([, url]) => url).join(" | "),
}));
src/pages/data/index.astro +1 −1
@@ -9,5 +9,5 @@ const repo = "https://github.com/burningtree/cannabis-freedom-index";
const files = [
{ name: "scores", title: "Scores", rows: scoreRows, about: "One row per country or disputed territory: index score, the six segment scores, rank, tier, legal status, population and estimated cannabis users." },
- { name: "history", title: "History", rows: historyRows, about: "One row per history entry: date, country, category, title, description, the score before and after, and the source." },
+ { name: "history", title: "History", rows: historyRows, about: "One row per history entry: date, country, category, title, description, the score before and after, how far it has been checked, and its sources." },
];
---
src/pages/history/[id].astro +6 −2
@@ -25,5 +25,9 @@ const diff = (seg) => seg.after - seg.before;
<h1>{ch.title}</h1>
<p class="lede">{ch.description}</p>
- <p class="change-source">Source: <a href={ch.source[1]} target="_blank" rel="noopener noreferrer">{ch.source[0]}</a></p>
+ <p class={`confidence c-${ch.confidence.key}`}><span aria-hidden="true">{ch.confidence.mark}</span> <strong>{ch.confidence.label}.</strong> {ch.confidence.help}.{ch.score ? " The score before the change is an editorial estimate." : ""}</p>
+ <p class="change-source">{ch.sources.length > 1 ? "Sources" : "Source"}:</p>
+ <ul class="srcs change-srcs">
+ {ch.sources.map(([label, url]) => <li><a href={url} target="_blank" rel="noopener noreferrer">{label}</a></li>)}
+ </ul>
{ch.score && (
@@ -55,5 +59,5 @@ const diff = (seg) => seg.after - seg.before;
{!ch.score && <p class="change-note">{c ? `Listed for context. No score change is recorded for ${c.name} here.` : "International agreements are listed for context. They bind governments, not people, so they move no country's score by themselves."}</p>}
- {ch.estimate && <p class="change-note">This entry is an estimate. No first law is recorded for {c.name}, so its first restrictions are assumed to have come at some point between the 1925 Geneva convention and the 1961 Single Convention. On the charts the change is spread evenly across those years.</p>}
+ {ch.estimate && <p class="change-note">This entry is an estimate. No first law is recorded for {c.name}, so its first restrictions are assumed to have come at some point between {ch.window[0]} and {ch.window[1]}. On the charts the change is spread evenly across those years.</p>}
</article>
src/styles/global.css +10 −0
@@ -628,2 +628,12 @@ th.num-col { text-align: right; }
.footer-links a { color: var(--text-2); }
@media (max-width: 640px) { .log-place { white-space: normal; } }
+
+/* ── Confidence markers ─────────────────────────────── */
+.conf-mark { display: inline-block; min-width: 16px; margin-left: 4px; padding: 0 4px; border-radius: 999px; border: 1px solid var(--border); color: var(--muted); font-size: 11px; font-weight: 600; line-height: 15px; text-align: center; cursor: help; }
+.conf-mark.c-checked { color: var(--t5); border-color: color-mix(in srgb, var(--t5) 50%, var(--border)); }
+.confidence { margin: 14px 0 0; padding: 8px 12px; border: 1px solid var(--border); border-radius: 10px; color: var(--text-2); font-size: 13.5px; max-width: 62ch; }
+.confidence strong { color: var(--text); }
+.confidence.c-checked { border-color: color-mix(in srgb, var(--t5) 45%, var(--border)); }
+.confidence.c-checked > span { color: var(--t5); }
+.change-srcs { margin-top: 4px; font-size: 13.5px; }
+.c-changes-note { margin: 8px 0 0; color: var(--muted); font-size: 12px; }