diff --git a/CLAUDE.md b/CLAUDE.md
index d06f934..a5e9fcb 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -88,6 +88,14 @@ Each sin page follows one shape: **poke → proof → why → the fix → receip
`badChart`/`fixedChart` are the spec basenames (no `.json`). `citations[].key`
must exist in `src/lib/references.ts` (build throws otherwise).
3. New reference? Add it once to `src/lib/references.ts`, then cite its key.
+ Optional frontmatter worth knowing about:
+ - `wild[]` — real specimens of the sin, rendered above the citations. Always
+ `credit` the original and link `sourceUrl` where we found it. A chart that
+ commits two sins goes on both pages with a different `note` on each.
+ - `related[]` — slugs of neighbouring sins; validated at build, so a typo or
+ a renamed slug fails the build rather than shipping a dead link.
+ - `compareNote` — overrides "Same numbers, both charts," which is wrong on a
+ sin whose whole point is an omission.
4. `npm run dev` and check. The OG card for the sin is generated automatically.
## Design & rendering
diff --git a/docs/content-backlog.md b/docs/content-backlog.md
index 8576dc2..c141739 100644
--- a/docs/content-backlog.md
+++ b/docs/content-backlog.md
@@ -17,7 +17,7 @@ runs 2–5 on purpose.
## Sources in play
-`src/lib/references.ts` currently holds 24 entries, 20 of them cited by the five
+`src/lib/references.ts` currently holds 24 entries, 21 of them cited by the 6
published sins. This table is a **convenience, not a scoreboard** — it's here so
you can spot a source that fits a sin you're drafting, not so we can drive the
right-hand column to zero blanks. An uncited reference is not a debt.
@@ -29,26 +29,26 @@ seven sources, give it seven and add whatever's missing to the file.
| --- | --- |
| `ftvisvocab` | — |
| `healy` | — |
-| `munzner` | — |
| `schwabish` | — |
| `fewdualaxes` | dual-axis-correlation |
-| `fewpies` | too-many-pie-slices |
| `huff` | truncated-y-axis |
| `isenberg2011` | dual-axis-correlation |
+| `munzner` | incomplete-pie |
| `shapeparameter` | aspect-ratio |
| `showmenumbers` | too-many-pie-slices |
| `spurious` | dual-axis-correlation |
| `swd` | dual-axis-correlation |
| `tufte` | truncated-y-axis |
-| `vizwtf` | inverted-y-axis |
| `callingbullshit` | inverted-y-axis, truncated-y-axis |
-| `datatoviz` | dual-axis-correlation, too-many-pie-slices |
-| `datawrapper` | dual-axis-correlation, too-many-pie-slices |
| `eagereyes` | aspect-ratio, too-many-pie-slices |
+| `fewpies` | incomplete-pie, too-many-pie-slices |
| `junkcharts` | inverted-y-axis, too-many-pie-slices |
| `truthfulart` | aspect-ratio, dual-axis-correlation |
+| `vizwtf` | incomplete-pie, inverted-y-axis |
| `wilke` | too-many-pie-slices, truncated-y-axis |
| `clevelandmcgill` | aspect-ratio, too-many-pie-slices, truncated-y-axis |
+| `datatoviz` | dual-axis-correlation, incomplete-pie, too-many-pie-slices |
+| `datawrapper` | dual-axis-correlation, incomplete-pie, too-many-pie-slices |
| `economistmistakes` | dual-axis-correlation, inverted-y-axis, truncated-y-axis |
| `howchartslie` | aspect-ratio, dual-axis-correlation, inverted-y-axis, truncated-y-axis |
@@ -74,7 +74,32 @@ we can build.
**Citations:** `huff` (the original con), `callingbullshit`, `howchartslie`,
`junkcharts`.
-### 2. Pie Slices That Sum to More Than 100% — *Impossible Wholes*, severity 5
+### 2. Results From a Hand-Picked Subgroup — *Deceptive Framing*, severity 4
+
+**Poke:** "You didn't pick the wrong chart, you picked the wrong people. The
+number is real; the group it describes was chosen after the fact."
+
+The population-shaped twin of entry 1. Rather than trimming the time window, you
+report the slice of the audience, region or segment where the number flatters —
+often labelled honestly, which is what makes it feel above board.
+
+The published `incomplete-pie` specimen is **also an example of this one**: it
+reports the primary "in the countryside," excluding the capital, on a graphic
+whose banner argues the capital can't win. Reuse the same image here with a note
+about this failure mode — that's what the `wild` array is for, and it's the case
+for keeping these as two pages rather than one.
+
+Worth its own page rather than folding into entry 1: the tell is different (no
+axis to inspect — you have to know what got left out), and the fix is different
+(report the whole population, or every subgroup side by side, not just the
+flattering one).
+
+**Charts:** bad = one subgroup's result. Fixed = every subgroup plus the overall
+figure, same underlying data.
+
+**Citations:** `callingbullshit`, `howchartslie`, `huff`, `vizwtf`.
+
+### 3. Pie Slices That Sum to More Than 100% — *Impossible Wholes*, severity 5
**Poke:** "Your slices add up to 180%. A pie has one job — dividing a whole —
and this data has no whole to divide."
@@ -97,7 +122,7 @@ pie-chart post says "one pie chart can only show one total and its shares," and
it does *not* separately argue that parts must be mutually exclusive. Don't
stretch it further than that. Plus `datatoviz`, `junkcharts`.
-### 3. A Pie Chart of Rates or Averages — *Impossible Wholes*, severity 5
+### 4. A Pie Chart of Rates or Averages — *Impossible Wholes*, severity 5
**Poke:** "You made a pie out of averages. Adding them together produces a
number that means nothing, and that number is your denominator."
@@ -120,7 +145,7 @@ same averages, plus the volume behind each one so the weighting is visible.
or it is nothing), `datawrapper` (same one-total premise as above), `schwabish`,
`ftvisvocab`.
-### 4. Group Averages Shown Without the Distribution — *Hiding the Data*, severity 4
+### 5. Group Averages Shown Without the Distribution — *Hiding the Data*, severity 4
**Poke:** "Four bars, four averages, and no idea whether the difference is real."
@@ -133,7 +158,7 @@ observations, means still marked.
**Citations:** `wilke`, `healy`, `schwabish`, `datatoviz`.
-### 5. Bubbles Sized by Radius Instead of Area — *Perceptual Traps*, severity 4
+### 6. Bubbles Sized by Radius Instead of Area — *Perceptual Traps*, severity 4
**Poke:** "You doubled the number and quadrupled the ink."
@@ -148,7 +173,7 @@ position does the work.
**Citations:** `munzner`, `clevelandmcgill`, `tufte`, `howchartslie`.
-### 6. Stacked Segments Without a Shared Baseline — *Perceptual Traps*, severity 3
+### 7. Stacked Segments Without a Shared Baseline — *Perceptual Traps*, severity 3
**Poke:** "Only the bottom band of a stacked chart has a straight edge to
measure against. The rest are guesses."
@@ -175,7 +200,7 @@ out to its own zero baseline), totals preserved.
**Citations:** `datawrapper`, `wilke`, `munzner`, `ftvisvocab`.
-### 7. Categories Sorted Alphabetically Instead of by Value — *Sloppy Craft*, severity 2
+### 8. Categories Sorted Alphabetically Instead of by Value — *Sloppy Craft*, severity 2
**Poke:** "Your categories are sorted by name. Nobody wants to know which
region starts with A."
@@ -210,6 +235,16 @@ rediscovering them:
too: the fixed chart needs a sequential ramp validated on `--chart-canvas` in
both themes. Don't draft this one until a real source is in hand.
+## Two new levers, as of the first wild specimen
+
+- **`wild`** — real examples, credited and linked, rendered above the citations.
+ Zero, one or many per sin. A specimen that commits two sins belongs on both
+ pages, with a different note on each; that's a reason to split sins, not merge
+ them.
+- **`related`** — cross-links between sins, validated at build. Charts rarely
+ break one rule at a time, and the person you sent a link to may need the
+ neighbouring page more than the one they landed on.
+
## Conventions reminder
Note what the two pie entries above do and don't say. Neither condemns the pie
diff --git a/docs/source-requests.md b/docs/source-requests.md
index 9acfac4..7892633 100644
--- a/docs/source-requests.md
+++ b/docs/source-requests.md
@@ -37,6 +37,8 @@ Two standing rules for step 3:
| 11 | Robert Kosara, *Understanding Pie Charts* (eagereyes) | URL, and what his experiments actually found. We lean on this to justify not condemning pie charts outright (claim V7). |
| 12 | The Economist, "Mistakes, we've drawn a few" | **Canonical URL only** — the content arrived and is settled below, but `economistmistakes` still ships without a link. |
+| 13 | `viz.wtf/post/664318320965795840/primary-election-result` | The post's caption and date. viz.wtf 403s automated fetches, so the specimen on `incomplete-pie` is credited from the logos legible in the image itself (Demokratikus Koalíció / Dobrev Klára) rather than from the post. The arithmetic and the measured arc angles are ours and don't depend on it. |
+
## Claims to verify
Live on the site, resting on recall rather than a source anyone opened.
diff --git a/public/wild/hu-primary-countryside.jpg b/public/wild/hu-primary-countryside.jpg
new file mode 100644
index 0000000..870e0f5
Binary files /dev/null and b/public/wild/hu-primary-countryside.jpg differ
diff --git a/src/charts/incomplete-pie-bad.json b/src/charts/incomplete-pie-bad.json
new file mode 100644
index 0000000..516ff42
--- /dev/null
+++ b/src/charts/incomplete-pie-bad.json
@@ -0,0 +1,24 @@
+{
+ "title": "Vote Share by Candidate (%)",
+ "width": 300,
+ "height": 260,
+ "data": {
+ "values": [
+ { "candidate": "Alvarez", "share": 38 },
+ { "candidate": "Boone", "share": 22 },
+ { "candidate": "Chen", "share": 19 },
+ { "candidate": "Everyone else", "share": 21 }
+ ]
+ },
+ "transform": [{ "filter": "datum.candidate !== 'Everyone else'" }],
+ "mark": { "type": "arc" },
+ "encoding": {
+ "theta": { "field": "share", "type": "quantitative" },
+ "color": {
+ "field": "candidate",
+ "type": "nominal",
+ "sort": ["Alvarez", "Boone", "Chen", "Everyone else"],
+ "legend": { "title": "Candidate" }
+ }
+ }
+}
diff --git a/src/charts/incomplete-pie-fixed.json b/src/charts/incomplete-pie-fixed.json
new file mode 100644
index 0000000..ac1822c
--- /dev/null
+++ b/src/charts/incomplete-pie-fixed.json
@@ -0,0 +1,23 @@
+{
+ "title": "Vote Share by Candidate (%)",
+ "width": 300,
+ "height": 260,
+ "data": {
+ "values": [
+ { "candidate": "Alvarez", "share": 38 },
+ { "candidate": "Boone", "share": 22 },
+ { "candidate": "Chen", "share": 19 },
+ { "candidate": "Everyone else", "share": 21 }
+ ]
+ },
+ "mark": { "type": "arc" },
+ "encoding": {
+ "theta": { "field": "share", "type": "quantitative" },
+ "color": {
+ "field": "candidate",
+ "type": "nominal",
+ "sort": ["Alvarez", "Boone", "Chen", "Everyone else"],
+ "legend": { "title": "Candidate" }
+ }
+ }
+}
diff --git a/src/components/RelatedSins.astro b/src/components/RelatedSins.astro
new file mode 100644
index 0000000..74d22c0
--- /dev/null
+++ b/src/components/RelatedSins.astro
@@ -0,0 +1,36 @@
+---
+// "While you're here" — the other sins a chart like this usually commits.
+// Charts rarely break one rule at a time, and the person you sent this to may
+// need the neighbouring page more than this one. Renders nothing when empty.
+import { href } from '../lib/url';
+import { severityUnits } from '../lib/charts';
+
+interface Props {
+ sins: { id: string; data: { title: string; summary: string; severity: number } }[];
+}
+
+const { sins } = Astro.props;
+---
+
+{
+ sins.length > 0 && (
+
+ )
+}
diff --git a/src/components/WildExamples.astro b/src/components/WildExamples.astro
new file mode 100644
index 0000000..6697bde
--- /dev/null
+++ b/src/components/WildExamples.astro
@@ -0,0 +1,65 @@
+---
+// Real specimens of a sin, published by someone else. Sits between the
+// explanation and the citations: first the argument, then proof it happens,
+// then the sources. Renders nothing when a sin has no examples yet.
+import { href } from '../lib/url';
+
+interface WildExample {
+ caption: string;
+ note: string;
+ image?: string;
+ alt?: string;
+ credit: string;
+ sourceUrl?: string;
+ sourceLabel?: string;
+}
+
+interface Props {
+ examples: WildExample[];
+}
+
+const { examples } = Astro.props;
+---
+
+{
+ examples.length > 0 && (
+
+ {examples.length === 1
+ ? 'Not a mock-up. Someone published this.'
+ : 'Not mock-ups. Someone published each of these.'}
+ {ex.caption} {ex.note}
+ {ex.credit}
+ {ex.sourceUrl && (
+ <>
+ {' · '}
+
+ {ex.sourceLabel ?? 'source'}
+
+ >
+ )}
+ Caught in the wild
+
+ {examples.map((ex) => (
+
+
+
Same numbers, both charts. Only the design changed.
+{sin.data.compareNote ?? 'Same numbers, both charts. Only the design changed.'}
diff --git a/src/styles/global.css b/src/styles/global.css index 1e49447..c659dbb 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -675,6 +675,116 @@ a.receipt__work { font-size: 0.875rem; } +/* often found together — cross-links between sins */ +.related { + margin-top: var(--spacing-09); + padding-top: var(--spacing-07); + border-top: 1px solid var(--border-subtle); +} +.related__title { + font-size: 1.5rem; + font-weight: 400; + margin: 0 0 var(--spacing-05); + letter-spacing: -0.01em; +} +.related__list { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 1px; + background: var(--border-subtle); + border: 1px solid var(--border-subtle); +} +.related__item { + background: var(--layer-01); + padding: var(--spacing-05) var(--spacing-06); + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-05); +} +.related__link { + text-decoration: none; + display: grid; + gap: var(--spacing-02); +} +.related__name { + font-weight: 600; + color: var(--link-primary); +} +.related__link:hover .related__name { + text-decoration: underline; +} +.related__summary { + font-size: 0.875rem; + color: var(--text-secondary); +} +.related__sev { + display: inline-flex; + gap: 3px; + flex: none; +} +.related__unit { + width: 6px; + height: 14px; + background: var(--border-subtle); +} +.related__unit--on { + background: var(--support-error); +} + +/* caught in the wild — real specimens */ +.wild { + margin-top: var(--spacing-09); + padding-top: var(--spacing-07); + border-top: 1px solid var(--border-subtle); +} +.wild__title { + font-size: 1.5rem; + font-weight: 400; + margin: 0; + letter-spacing: -0.01em; +} +.wild__lede { + color: var(--text-secondary); + margin: var(--spacing-03) 0 var(--spacing-06); +} +.wild__list { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: var(--spacing-06); +} +.wild__item { + background: var(--layer-01); + border: 1px solid var(--border-subtle); + padding: var(--spacing-05) var(--spacing-06) var(--spacing-06); +} +.wild__figure { + margin: 0 0 var(--spacing-05); + background: var(--chart-canvas); +} +.wild__img { + display: block; + width: 100%; + height: auto; +} +.wild__caption { + margin: 0 0 var(--spacing-03); + font-weight: 600; +} +.wild__note { + margin: 0 0 var(--spacing-04); + color: var(--text-secondary); +} +.wild__credit { + margin: 0; + font-size: 0.75rem; + color: var(--text-helper); +} + /* references page */ .refs { padding-block: var(--spacing-08) var(--spacing-10);