mirror of
https://github.com/olehomelchenko/chart-sins.git
synced 2026-08-08 02:22:43 +00:00
Add real-world specimens and cross-links between sins
Two new levers on a sin page. `wild[]` renders credited, linked specimens between the explanation and the citations — first the argument, then proof people actually publish this, then the sources. `related[]` cross-links neighbouring sins and is resolved at build, so a typo fails the build instead of shipping a dead link. Both arrive with the first specimen: a Hungarian primary graphic whose three slices read 38/22/19 — 79% — drawn as a full circle, so every share is inflated by about 1.27x. Measured off the image, the 38% slice occupies 173.6 degrees, or 48.3% of the pie. Our published pie sin is about too many slices, so it didn't cover this; "Pie Slices That Don't Sum to the Whole" is a new severity-5 page whose before/after is two pies differing only in whether the remainder is present. That sin is an omission, so `compareNote` replaces the standard "same numbers, both charts" line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ULE5RRxdQE1ebwefEd2eCM
This commit is contained in:
@@ -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
|
||||
|
||||
+47
-12
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 && (
|
||||
<nav class="related" aria-labelledby="related-h">
|
||||
<h2 id="related-h" class="related__title">Often found together</h2>
|
||||
<ul class="related__list">
|
||||
{sins.map((s) => (
|
||||
<li class="related__item">
|
||||
<a class="related__link" href={href(`sins/${s.id}/`)}>
|
||||
<span class="related__name">{s.data.title}</span>
|
||||
<span class="related__summary">{s.data.summary}</span>
|
||||
</a>
|
||||
<span class="related__sev" aria-label={`Severity ${s.data.severity} of 5`}>
|
||||
{severityUnits(s.data.severity).map((on) => (
|
||||
<i class={`related__unit${on ? ' related__unit--on' : ''}`} />
|
||||
))}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -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 && (
|
||||
<section class="wild" aria-labelledby="wild-h">
|
||||
<h2 id="wild-h" class="wild__title">Caught in the wild</h2>
|
||||
<p class="wild__lede">
|
||||
{examples.length === 1
|
||||
? 'Not a mock-up. Someone published this.'
|
||||
: 'Not mock-ups. Someone published each of these.'}
|
||||
</p>
|
||||
<ol class="wild__list">
|
||||
{examples.map((ex) => (
|
||||
<li class="wild__item">
|
||||
{ex.image && (
|
||||
<figure class="wild__figure">
|
||||
<img
|
||||
class="wild__img"
|
||||
src={href(`wild/${ex.image}`)}
|
||||
alt={ex.alt ?? ex.caption}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</figure>
|
||||
)}
|
||||
<p class="wild__caption">{ex.caption}</p>
|
||||
<p class="wild__note">{ex.note}</p>
|
||||
<p class="wild__credit">
|
||||
<span>{ex.credit}</span>
|
||||
{ex.sourceUrl && (
|
||||
<>
|
||||
{' · '}
|
||||
<a href={ex.sourceUrl} rel="noopener">
|
||||
{ex.sourceLabel ?? 'source'}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -27,6 +27,29 @@ const sins = defineCollection({
|
||||
// Basenames of Vega-Lite specs in src/charts/ (without .json).
|
||||
badChart: z.string(),
|
||||
fixedChart: z.string(),
|
||||
// Overrides the line under the before/after pair. Default asserts the two
|
||||
// charts share one dataset; set it when the sin *is* an omission and that
|
||||
// wording would be wrong.
|
||||
compareNote: z.string().optional(),
|
||||
// Real specimens of this sin, published by someone else and found in the
|
||||
// wild. Zero, one or many. Rendered above the citations. Always credit the
|
||||
// original and link where it was collected when we can.
|
||||
wild: z
|
||||
.array(
|
||||
z.object({
|
||||
caption: z.string(),
|
||||
note: z.string(),
|
||||
image: z.string().optional(),
|
||||
alt: z.string().optional(),
|
||||
credit: z.string(),
|
||||
sourceUrl: z.string().optional(),
|
||||
sourceLabel: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.default([]),
|
||||
// Slugs of other sins worth reading next — a chart usually commits more
|
||||
// than one. Validated at build; an unknown slug throws.
|
||||
related: z.array(z.string()).default([]),
|
||||
// Optional publish date; unset entries sort last.
|
||||
date: z.coerce.date().optional(),
|
||||
draft: z.boolean().default(false),
|
||||
|
||||
@@ -8,6 +8,9 @@ tags: ["line chart", "aspect ratio", "exaggeration"]
|
||||
badChart: "aspect-ratio-bad"
|
||||
fixedChart: "aspect-ratio-fixed"
|
||||
date: 2026-08-05
|
||||
related:
|
||||
- truncated-y-axis
|
||||
- dual-axis-correlation
|
||||
citations:
|
||||
- key: shapeparameter
|
||||
note: "The paper that made this measurable: a graph's width-to-height ratio changes how slopes are judged, and gave us “bank the average slope toward 45°.”"
|
||||
|
||||
@@ -8,6 +8,9 @@ tags: ["dual axis", "correlation", "line chart"]
|
||||
badChart: "dual-axis-bad"
|
||||
fixedChart: "dual-axis-fixed"
|
||||
date: 2026-08-05
|
||||
related:
|
||||
- aspect-ratio
|
||||
- truncated-y-axis
|
||||
citations:
|
||||
- key: datatoviz
|
||||
note: "Lists “using two Y axes” as a top caveat — a way to manipulate the story."
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: "Pie Slices That Don't Sum to the Whole"
|
||||
summary: "Four-fifths of a result, drawn as if it were all of it — so every share on the chart comes out too big."
|
||||
poke: "Your slices don't add up to the whole, but they fill the circle anyway. Every number on that chart is bigger than the truth."
|
||||
category: "Impossible Wholes"
|
||||
severity: 5
|
||||
tags: ["pie chart", "part-to-whole", "share"]
|
||||
badChart: "incomplete-pie-bad"
|
||||
fixedChart: "incomplete-pie-fixed"
|
||||
compareNote: "One set of results in both charts. The sinful one just leaves a slice out."
|
||||
date: 2026-08-05
|
||||
wild:
|
||||
- caption: "Hungarian opposition primary, 2021 — results “in the countryside.”"
|
||||
note: "The three labelled slices read 38%, 22% and 19%: 79% in all. The remaining 21% — every other candidate — isn't drawn, and the circle is full anyway, so each share was quietly scaled up by a factor of about 1.27. Measuring the image, the 38% slice occupies 173.6° of arc, which is 48.3% of the pie. The graphic argues its own candidate is the one who can win outside the capital, and it draws her a whisker short of half the vote when she polled 38%."
|
||||
image: "hu-primary-countryside.jpg"
|
||||
alt: "Pie chart headed AZ ELLENZÉKI ELŐVÁLASZTÁS VÉGEREDMÉNYE VIDÉKEN, with three slices labelled Dobrev Klára 38%, Karácsony Gergely 22%, and Márki-Zay Péter 19%, filling the whole circle."
|
||||
credit: "Campaign graphic by Demokratikus Koalíció / Dobrev Klára"
|
||||
sourceLabel: "collected by WTF Visualizations"
|
||||
sourceUrl: "https://viz.wtf/post/664318320965795840/primary-election-result"
|
||||
related:
|
||||
- too-many-pie-slices
|
||||
citations:
|
||||
- key: datawrapper
|
||||
note: "Their pie-chart guidance starts from the premise this chart breaks: “one pie chart can only show one total and its shares.”"
|
||||
- key: munzner
|
||||
note: "Part-to-whole is a composition of a whole. Remove part of the whole and the encoding no longer means anything."
|
||||
- key: fewpies
|
||||
note: "Few's case against the pie, which lands hardest when the circle isn't even a real total."
|
||||
- key: vizwtf
|
||||
note: "Where the specimen above was collected — the archive exists because this ships to real audiences."
|
||||
- key: datatoviz
|
||||
note: "Its pie-chart caveats cover the arithmetic a pie quietly assumes."
|
||||
---
|
||||
|
||||
## The sin
|
||||
|
||||
A pie chart makes exactly one promise: the circle is everything, and each wedge
|
||||
is its share of that everything. Every reading depends on it. "Nearly half"
|
||||
means nearly half *of the total* — otherwise the phrase is empty.
|
||||
|
||||
So when the parts don't add up to the whole, the tool has two options, and both
|
||||
are bad. Either it leaves a gap in the circle — which nobody ships — or it
|
||||
scales the parts up until they close it. Charting libraries do the second
|
||||
silently, because they compute each angle as a fraction of *the values you
|
||||
gave them*, not of the total you meant.
|
||||
|
||||
Above, three candidates hold 38%, 22% and 19% of the vote. That's 79%. The
|
||||
remaining 21% went to people not shown, and dropping them doesn't shrink the
|
||||
chart — it inflates everyone left in it. The leading candidate's 38% is drawn as
|
||||
48% of the circle.
|
||||
|
||||
## Why it deceives
|
||||
|
||||
Nothing signals the omission. The circle is closed, the percentages are printed
|
||||
correctly next to the slices, and the labels and the geometry disagree with each
|
||||
other in a way almost nobody checks — because checking means adding three
|
||||
numbers and noticing they don't reach 100.
|
||||
|
||||
The reader is doing what the chart asked: comparing areas. The areas are wrong.
|
||||
And because every slice is inflated by the same factor, the chart stays
|
||||
internally consistent — it looks exactly like a well-formed pie right up until
|
||||
you do the arithmetic.
|
||||
|
||||
This is worse than a hard-to-read chart. A crowded pie makes you guess; this one
|
||||
answers confidently and wrong.
|
||||
|
||||
## The repentance
|
||||
|
||||
Include the whole whole. If the small categories are too numerous or too dull to
|
||||
list, that's what an **"Other" wedge** is for — it costs one slice and one label,
|
||||
and it makes every other slice true. The fixed chart above does nothing more
|
||||
clever than that.
|
||||
|
||||
If you can't account for the remainder — a poll with undecideds, a total that
|
||||
genuinely isn't known — then you don't have a part-to-whole relationship, and a
|
||||
pie is the wrong form. Use a bar chart, where each value stands on its own
|
||||
baseline and nothing has to sum to anything.
|
||||
|
||||
And if you're tempted to drop the remainder because it makes your number look
|
||||
better: that's not a charting decision.
|
||||
@@ -8,6 +8,9 @@ tags: ["axis", "area chart", "direction"]
|
||||
badChart: "inverted-axis-bad"
|
||||
fixedChart: "inverted-axis-fixed"
|
||||
date: 2026-08-05
|
||||
related:
|
||||
- truncated-y-axis
|
||||
- aspect-ratio
|
||||
citations:
|
||||
- key: callingbullshit
|
||||
note: "Their “Misleading Axes” module collects the axis manipulations, this one included."
|
||||
|
||||
@@ -8,6 +8,8 @@ tags: ["pie chart", "part-to-whole", "comparison"]
|
||||
badChart: "pie-overload-bad"
|
||||
fixedChart: "pie-overload-fixed"
|
||||
date: 2026-08-05
|
||||
related:
|
||||
- incomplete-pie
|
||||
citations:
|
||||
- key: fewpies
|
||||
note: "“Save the Pies for Dessert” — the definitive, free takedown of the overloaded pie."
|
||||
|
||||
@@ -8,6 +8,9 @@ tags: ["bar chart", "axis", "exaggeration"]
|
||||
badChart: "truncated-axis-bad"
|
||||
fixedChart: "truncated-axis-fixed"
|
||||
date: 2026-08-05
|
||||
related:
|
||||
- aspect-ratio
|
||||
- inverted-y-axis
|
||||
citations:
|
||||
- key: callingbullshit
|
||||
note: "Their “Misleading Axes” module is the standard explainer for exactly this move."
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import VegaChart from '../../components/VegaChart.astro';
|
||||
import WildExamples from '../../components/WildExamples.astro';
|
||||
import RelatedSins from '../../components/RelatedSins.astro';
|
||||
import Citations from '../../components/Citations.astro';
|
||||
import ShareLink from '../../components/ShareLink.astro';
|
||||
import { getChartSpec, severityUnits, tagModifier } from '../../lib/charts';
|
||||
@@ -11,11 +13,23 @@ export async function getStaticPaths() {
|
||||
const sins = await getCollection('sins', ({ data }) => !data.draft);
|
||||
return sins.map((sin) => ({
|
||||
params: { slug: sin.id },
|
||||
props: { sin },
|
||||
props: { sin, all: sins },
|
||||
}));
|
||||
}
|
||||
|
||||
const { sin } = Astro.props;
|
||||
const { sin, all } = Astro.props;
|
||||
|
||||
// Resolve `related` slugs against the collection, failing the build on a typo
|
||||
// or a slug that was renamed out from under us.
|
||||
const related = sin.data.related.map((slug) => {
|
||||
const target = all.find((s) => s.id === slug);
|
||||
if (!target) {
|
||||
throw new Error(
|
||||
`Sin "${sin.id}" lists related sin "${slug}", which doesn't exist. Known: ${all.map((s) => s.id).join(', ')}`,
|
||||
);
|
||||
}
|
||||
return target;
|
||||
});
|
||||
const { Content } = await render(sin);
|
||||
|
||||
const badSpec = getChartSpec(sin.data.badChart);
|
||||
@@ -59,14 +73,18 @@ const ogImage = new URL(href(`og/${sin.id}.png`), Astro.site).toString();
|
||||
<VegaChart spec={fixedSpec} title={`Honest chart: ${sin.data.title}`} />
|
||||
</div>
|
||||
</div>
|
||||
<p class="compare__note">Same numbers, both charts. Only the design changed.</p>
|
||||
<p class="compare__note">{sin.data.compareNote ?? 'Same numbers, both charts. Only the design changed.'}</p>
|
||||
|
||||
<div class="prose">
|
||||
<Content />
|
||||
</div>
|
||||
|
||||
<WildExamples examples={sin.data.wild} />
|
||||
|
||||
<Citations citations={sin.data.citations} />
|
||||
|
||||
<RelatedSins sins={related} />
|
||||
|
||||
<ShareLink url={pageUrl} />
|
||||
|
||||
<p class="more-refs">
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user