Add two sins from wild specimens: category colours, rebinned scales

Both arrived as real charts before the sins existed, which is the better
order — the specimen makes the failure mode obvious.

"Bar Colours That Contradict Their Labels" (severity 2): categories that
already own a colour, drawn in a default palette, so Black is a green
bar. Nothing false, which is exactly why it's a 2 under our rubric — but
colour is pre-attentive, so the reader pays the cost on every glance.

"A Colour Scale That Changed Between Two Charts" (severity 4): the
Tagesschau incidence maps, rebinned from 25/50/100/250/500 to
20/35/50/100/200 three weeks apart while being presented as a
before-and-after. A colour key is an axis, and moving it mid-comparison
is the axis crime we don't police because a legend reads as formatting.

The bad chart there needed an explicit independent colour resolve —
Vega-Lite merges scales across a concat by default, which silently
unified the two legends and destroyed the entire demonstration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULE5RRxdQE1ebwefEd2eCM
This commit is contained in:
Claude
2026-08-05 20:56:26 +00:00
parent 673f265398
commit fd70ccfc13
9 changed files with 695 additions and 8 deletions
+14 -8
View File
@@ -17,7 +17,7 @@ runs 25 on purpose.
## Sources in play ## Sources in play
`src/lib/references.ts` currently holds 24 entries, 21 of them cited by the 6 `src/lib/references.ts` currently holds 24 entries, 22 of them cited by the 8
published sins. This table is a **convenience, not a scoreboard** — it's here so 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 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. right-hand column to zero blanks. An uncited reference is not a debt.
@@ -29,28 +29,28 @@ seven sources, give it seven and add whatever's missing to the file.
| --- | --- | | --- | --- |
| `ftvisvocab` | — | | `ftvisvocab` | — |
| `healy` | — | | `healy` | — |
| `schwabish` | — |
| `fewdualaxes` | dual-axis-correlation | | `fewdualaxes` | dual-axis-correlation |
| `huff` | truncated-y-axis | | `huff` | truncated-y-axis |
| `isenberg2011` | dual-axis-correlation | | `isenberg2011` | dual-axis-correlation |
| `munzner` | incomplete-pie | | `schwabish` | category-colors |
| `shapeparameter` | aspect-ratio | | `shapeparameter` | aspect-ratio |
| `showmenumbers` | too-many-pie-slices | | `showmenumbers` | too-many-pie-slices |
| `spurious` | dual-axis-correlation | | `spurious` | dual-axis-correlation |
| `swd` | dual-axis-correlation |
| `tufte` | truncated-y-axis | | `tufte` | truncated-y-axis |
| `callingbullshit` | inverted-y-axis, truncated-y-axis |
| `eagereyes` | aspect-ratio, too-many-pie-slices | | `eagereyes` | aspect-ratio, too-many-pie-slices |
| `fewpies` | incomplete-pie, too-many-pie-slices | | `fewpies` | incomplete-pie, too-many-pie-slices |
| `junkcharts` | inverted-y-axis, too-many-pie-slices | | `junkcharts` | inverted-y-axis, too-many-pie-slices |
| `swd` | category-colors, dual-axis-correlation |
| `truthfulart` | aspect-ratio, dual-axis-correlation | | `truthfulart` | aspect-ratio, dual-axis-correlation |
| `vizwtf` | incomplete-pie, inverted-y-axis |
| `wilke` | too-many-pie-slices, truncated-y-axis | | `wilke` | too-many-pie-slices, truncated-y-axis |
| `callingbullshit` | inverted-y-axis, rebinned-colour-scale, truncated-y-axis |
| `clevelandmcgill` | aspect-ratio, 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 | | `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 | | `economistmistakes` | dual-axis-correlation, inverted-y-axis, truncated-y-axis |
| `howchartslie` | aspect-ratio, dual-axis-correlation, inverted-y-axis, truncated-y-axis | | `munzner` | category-colors, incomplete-pie, rebinned-colour-scale |
| `vizwtf` | category-colors, incomplete-pie, inverted-y-axis, rebinned-colour-scale |
| `datawrapper` | category-colors, dual-axis-correlation, incomplete-pie, rebinned-colour-scale, too-many-pie-slices |
| `howchartslie` | aspect-ratio, dual-axis-correlation, inverted-y-axis, rebinned-colour-scale, truncated-y-axis |
The citation lists in each entry below are **starting points, not budgets** The citation lists in each entry below are **starting points, not budgets**
add to them while drafting. add to them while drafting.
@@ -237,6 +237,12 @@ rediscovering them:
## Two new levers, as of the first wild specimen ## Two new levers, as of the first wild specimen
Three specimens are live so far (`incomplete-pie`, `category-colors`,
`rebinned-colour-scale`), all collected from WTF Visualizations. Each one
arrived before its sin existed, which is turning out to be the healthier order:
a real chart in hand makes the failure mode obvious and stops us inventing sins
nobody commits.
- **`wild`** — real examples, credited and linked, rendered above the citations. - **`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 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 pages, with a different note on each; that's a reason to split sins, not merge
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

+32
View File
@@ -0,0 +1,32 @@
{
"title": "Cars Sold by Colour",
"width": 340,
"height": 220,
"data": {
"values": [
{ "colour": "White", "sold": 34 },
{ "colour": "Black", "sold": 27 },
{ "colour": "Grey", "sold": 18 },
{ "colour": "Blue", "sold": 9 },
{ "colour": "Red", "sold": 7 },
{ "colour": "Green", "sold": 3 },
{ "colour": "Yellow", "sold": 2 }
]
},
"mark": "bar",
"encoding": {
"x": {
"field": "colour",
"type": "nominal",
"sort": null,
"axis": { "labelAngle": 0, "title": null }
},
"y": { "field": "sold", "type": "quantitative", "scale": { "zero": true }, "title": "Cars sold" },
"color": {
"field": "colour",
"type": "nominal",
"sort": null,
"legend": null
}
}
}
+36
View File
@@ -0,0 +1,36 @@
{
"title": "Cars Sold by Colour",
"width": 340,
"height": 220,
"data": {
"values": [
{ "colour": "White", "sold": 34 },
{ "colour": "Black", "sold": 27 },
{ "colour": "Grey", "sold": 18 },
{ "colour": "Blue", "sold": 9 },
{ "colour": "Red", "sold": 7 },
{ "colour": "Green", "sold": 3 },
{ "colour": "Yellow", "sold": 2 }
]
},
"mark": { "type": "bar", "stroke": "#8d8d8d", "strokeWidth": 0.6 },
"encoding": {
"x": {
"field": "colour",
"type": "nominal",
"sort": null,
"axis": { "labelAngle": 0, "title": null }
},
"y": { "field": "sold", "type": "quantitative", "scale": { "zero": true }, "title": "Cars sold" },
"color": {
"field": "colour",
"type": "nominal",
"sort": null,
"scale": {
"domain": ["White", "Black", "Grey", "Blue", "Red", "Green", "Yellow"],
"range": ["#ffffff", "#161616", "#8d8d8d", "#0f62fe", "#da1e28", "#198038", "#f1c21b"]
},
"legend": null
}
}
}
+237
View File
@@ -0,0 +1,237 @@
{
"title": "7-Day Incidence by District",
"data": {
"values": [
{
"district": "A",
"row": "7-day incidence",
"when": "17 March",
"value": 22
},
{
"district": "B",
"row": "7-day incidence",
"when": "17 March",
"value": 38
},
{
"district": "C",
"row": "7-day incidence",
"when": "17 March",
"value": 45
},
{
"district": "D",
"row": "7-day incidence",
"when": "17 March",
"value": 60
},
{
"district": "E",
"row": "7-day incidence",
"when": "17 March",
"value": 85
},
{
"district": "F",
"row": "7-day incidence",
"when": "17 March",
"value": 110
},
{
"district": "G",
"row": "7-day incidence",
"when": "17 March",
"value": 140
},
{
"district": "H",
"row": "7-day incidence",
"when": "17 March",
"value": 190
},
{
"district": "A",
"row": "7-day incidence",
"when": "9 April",
"value": 45
},
{
"district": "B",
"row": "7-day incidence",
"when": "9 April",
"value": 70
},
{
"district": "C",
"row": "7-day incidence",
"when": "9 April",
"value": 88
},
{
"district": "D",
"row": "7-day incidence",
"when": "9 April",
"value": 120
},
{
"district": "E",
"row": "7-day incidence",
"when": "9 April",
"value": 150
},
{
"district": "F",
"row": "7-day incidence",
"when": "9 April",
"value": 175
},
{
"district": "G",
"row": "7-day incidence",
"when": "9 April",
"value": 210
},
{
"district": "H",
"row": "7-day incidence",
"when": "9 April",
"value": 260
}
]
},
"hconcat": [
{
"title": "17 March",
"width": 190,
"height": 46,
"transform": [
{
"filter": "datum.when === '17 March'"
}
],
"mark": {
"type": "rect",
"stroke": "#e0e0e0",
"strokeWidth": 0.5
},
"encoding": {
"x": {
"field": "district",
"type": "nominal",
"sort": null,
"axis": {
"labelAngle": 0,
"title": null
}
},
"y": {
"field": "row",
"type": "nominal",
"axis": {
"title": null,
"labels": false,
"ticks": false,
"domain": false
}
},
"color": {
"field": "value",
"type": "quantitative",
"scale": {
"type": "threshold",
"domain": [
25,
50,
100,
250
],
"range": [
"#fff1f1",
"#ffb3b8",
"#fa4d56",
"#da1e28",
"#750e13"
]
},
"legend": {
"title": "cases / 100k",
"orient": "bottom",
"direction": "horizontal"
}
}
}
},
{
"title": "9 April",
"width": 190,
"height": 46,
"transform": [
{
"filter": "datum.when === '9 April'"
}
],
"mark": {
"type": "rect",
"stroke": "#e0e0e0",
"strokeWidth": 0.5
},
"encoding": {
"x": {
"field": "district",
"type": "nominal",
"sort": null,
"axis": {
"labelAngle": 0,
"title": null
}
},
"y": {
"field": "row",
"type": "nominal",
"axis": {
"title": null,
"labels": false,
"ticks": false,
"domain": false
}
},
"color": {
"field": "value",
"type": "quantitative",
"scale": {
"type": "threshold",
"domain": [
20,
35,
50,
100,
200
],
"range": [
"#fff1f1",
"#ffd7d9",
"#ffb3b8",
"#fa4d56",
"#da1e28",
"#750e13"
]
},
"legend": {
"title": "cases / 100k",
"orient": "bottom",
"direction": "horizontal"
}
}
}
}
],
"resolve": {
"scale": {
"color": "independent"
},
"legend": {
"color": "independent"
}
}
}
+228
View File
@@ -0,0 +1,228 @@
{
"title": "7-Day Incidence by District",
"data": {
"values": [
{
"district": "A",
"row": "7-day incidence",
"when": "17 March",
"value": 22
},
{
"district": "B",
"row": "7-day incidence",
"when": "17 March",
"value": 38
},
{
"district": "C",
"row": "7-day incidence",
"when": "17 March",
"value": 45
},
{
"district": "D",
"row": "7-day incidence",
"when": "17 March",
"value": 60
},
{
"district": "E",
"row": "7-day incidence",
"when": "17 March",
"value": 85
},
{
"district": "F",
"row": "7-day incidence",
"when": "17 March",
"value": 110
},
{
"district": "G",
"row": "7-day incidence",
"when": "17 March",
"value": 140
},
{
"district": "H",
"row": "7-day incidence",
"when": "17 March",
"value": 190
},
{
"district": "A",
"row": "7-day incidence",
"when": "9 April",
"value": 45
},
{
"district": "B",
"row": "7-day incidence",
"when": "9 April",
"value": 70
},
{
"district": "C",
"row": "7-day incidence",
"when": "9 April",
"value": 88
},
{
"district": "D",
"row": "7-day incidence",
"when": "9 April",
"value": 120
},
{
"district": "E",
"row": "7-day incidence",
"when": "9 April",
"value": 150
},
{
"district": "F",
"row": "7-day incidence",
"when": "9 April",
"value": 175
},
{
"district": "G",
"row": "7-day incidence",
"when": "9 April",
"value": 210
},
{
"district": "H",
"row": "7-day incidence",
"when": "9 April",
"value": 260
}
]
},
"hconcat": [
{
"title": "17 March",
"width": 190,
"height": 46,
"transform": [
{
"filter": "datum.when === '17 March'"
}
],
"mark": {
"type": "rect",
"stroke": "#e0e0e0",
"strokeWidth": 0.5
},
"encoding": {
"x": {
"field": "district",
"type": "nominal",
"sort": null,
"axis": {
"labelAngle": 0,
"title": null
}
},
"y": {
"field": "row",
"type": "nominal",
"axis": {
"title": null,
"labels": false,
"ticks": false,
"domain": false
}
},
"color": {
"field": "value",
"type": "quantitative",
"scale": {
"type": "threshold",
"domain": [
25,
50,
100,
250
],
"range": [
"#fff1f1",
"#ffb3b8",
"#fa4d56",
"#da1e28",
"#750e13"
]
},
"legend": {
"title": "cases / 100k",
"orient": "bottom",
"direction": "horizontal"
}
}
}
},
{
"title": "9 April",
"width": 190,
"height": 46,
"transform": [
{
"filter": "datum.when === '9 April'"
}
],
"mark": {
"type": "rect",
"stroke": "#e0e0e0",
"strokeWidth": 0.5
},
"encoding": {
"x": {
"field": "district",
"type": "nominal",
"sort": null,
"axis": {
"labelAngle": 0,
"title": null
}
},
"y": {
"field": "row",
"type": "nominal",
"axis": {
"title": null,
"labels": false,
"ticks": false,
"domain": false
}
},
"color": {
"field": "value",
"type": "quantitative",
"scale": {
"type": "threshold",
"domain": [
25,
50,
100,
250
],
"range": [
"#fff1f1",
"#ffb3b8",
"#fa4d56",
"#da1e28",
"#750e13"
]
},
"legend": null
}
}
}
],
"resolve": {
"scale": {
"color": "shared"
}
}
}
+70
View File
@@ -0,0 +1,70 @@
---
title: "Bar Colours That Contradict Their Labels"
summary: "The categories already have colours. The chart assigned different ones anyway."
poke: "Your categories are colours, and you let the software pick different ones. The reader now has to override what they can see in order to read the chart."
category: "Sloppy Craft"
severity: 2
tags: ["bar chart", "colour", "encoding"]
badChart: "category-color-bad"
fixedChart: "category-color-fixed"
related:
- too-many-pie-slices
date: 2026-08-05
wild:
- caption: "“Shoe color Frequency” — a survey of shoe colours, plotted in a spreadsheet's default palette."
note: "Every category is a colour, and not one bar matches. Black is a green bar. White is a blue bar. Red is orange, grey is teal, brown is navy. The numbers are all correct — you simply cannot use the fastest channel on the chart, because it has been filled with an answer to a different question."
image: "shoe-colour-frequency.jpg"
alt: "Bar chart titled Shoe color Frequency with categories White, Blue, Yellow, Black, Red, Grey and Brown, each drawn in an unrelated default palette colour."
credit: "Spotted in the wild; original author unknown"
sourceLabel: "collected by WTF Visualizations"
sourceUrl: "https://viz.wtf/post/639589038118125568"
citations:
- key: munzner
note: "Colour is a channel that carries meaning whether or not you meant it to. Spending it on nothing is the cheap version of this mistake; spending it on the wrong thing is this one."
- key: swd
note: "Knaflic's rule that every colour on a chart should be a decision, made for a reason the reader can recover."
- key: datawrapper
note: "Their colour writing keeps returning to the same point: a palette is a mapping to data, not decoration."
- key: vizwtf
note: "Where the specimen above was collected."
- key: schwabish
note: "Chart-by-chart on matching the encoding to what the reader is actually being asked to do."
---
## The sin
Some categories arrive with a colour already attached. Colours themselves,
obviously — but also political parties, sports teams, brands, traffic-light
statuses, the red and black of a balance sheet. The reader knows the mapping
before they reach your chart.
Ignore it and you don't get a neutral chart. You get one that argues with
itself: a bar labelled *Black* drawn in green, one labelled *White* drawn in
blue. Every value is correct and the chart is still exhausting, because the
fastest channel on the page is now pointing somewhere the labels aren't.
## Why it deceives
"Deceives" is generous — nothing here is false, which is why this is a two and
not a four. But colour is pre-attentive: it lands before you've read a single
label. So the chart hands the reader an answer, the labels hand them a different
one, and the reader has to consciously suppress the first to trust the second.
That cost is paid on every glance, by every reader, forever, to save the author
one minute. And it doesn't stay merely annoying: anyone skimming, quoting from
memory, or looking at a thumbnail will take the colour at face value.
## The repentance
If a category has a natural colour, use it. The fix above is not clever — it
assigns each bar the colour it is named after, and adds a hairline stroke so
white and yellow keep an edge against the canvas.
If your categories *don't* have natural colours, the other repentance applies:
don't colour them at all. Seven bars in seven hues where a single hue would do
is the same mistake wearing a nicer coat — colour that encodes nothing but is
read as though it encodes something. One colour, or one highlighted bar and the
rest in grey.
The test is simple. Ask what a reader would conclude from the colours alone. If
the answer is "something wrong," or "nothing," change them.
+78
View File
@@ -0,0 +1,78 @@
---
title: "A Colour Scale That Changed Between Two Charts"
summary: "Two snapshots of the same measure, placed side by side, with the legend quietly rebinned in between."
poke: "You changed the bins between the before and the after. Whatever the reader sees in that comparison, some of it is your legend, not your data."
category: "Misleading Scales"
severity: 4
tags: ["colour scale", "comparison", "heatmap", "map"]
badChart: "rebinned-scale-bad"
fixedChart: "rebinned-scale-fixed"
compareNote: "The same readings in both, at both dates. Only the legend changed."
related:
- truncated-y-axis
- aspect-ratio
date: 2026-08-05
wild:
- caption: "Tagesschau, 7-day COVID incidence by German district: 17 March 2021 beside 9 April 2021."
note: "The two maps are the same measure three weeks apart, and they invite exactly one reading: look how much worse it got. But the legend was rebinned in between. On 17 March the bands ran to 25 / 50 / 100 / 250 / 500; on 9 April, to 20 / 35 / 50 / 100 / 200 / above. A district sitting at 150 is a mid-orange on the left map and near-black on the right, without having moved at all. Incidence did rise over those three weeks — that part is real. What the pair can't tell you is how much of the colour change is the virus and how much is the key."
image: "tagesschau-incidence-bins.jpg"
alt: "Two choropleth maps of Germany showing 7-day COVID incidence by district, dated 17 March and 9 April 2021, each with a different set of legend thresholds."
credit: "Maps by Tagesschau (ARD), data from the Robert Koch-Institut"
sourceLabel: "collected by WTF Visualizations"
sourceUrl: "https://viz.wtf/post/648282602475798528/german-main-news-website-changed-the-categrories"
citations:
- key: howchartslie
note: "Cairo on how the encoding, not the data, does the arguing when a reader compares two pictures."
- key: callingbullshit
note: "The axis-crime module, generalised: a colour key is an axis, and rescaling it mid-comparison is the same move."
- key: datawrapper
note: "Their colour-scale series is explicit that where you put the bin edges changes what readers see — the choice is never neutral."
- key: munzner
note: "Binning a continuous quantity is an encoding decision, and comparability depends on holding it fixed."
- key: vizwtf
note: "Where the specimen above was collected."
---
## The sin
Two panels, same measure, different dates, side by side. The reader is being
invited to compare them — that's the only reason to publish them together.
Then the bins move. The right-hand panel above uses a key that turns darker,
sooner, than the left one. Its districts are genuinely worse, but the picture
overstates by however much the key was tightened, and there is no way to
separate the two by looking.
## Why it deceives
A colour key is an axis. It maps a number to something the eye reads directly,
and every intuition about not moving the y-axis between two charts applies to it
unchanged — we just police it less, because a legend looks like a formatting
choice rather than a scale.
It's also uniquely hard to catch. A shifted y-axis leaves visible evidence: the
numbers are right there, and a suspicious reader can check them. Here the reader
would have to hold five thresholds from one legend in their head, read the five
in the other, and notice they differ — while looking at two attractive maps that
are clearly telling a story.
And rebinning is often *innocent* in origin, which is what keeps it alive. As a
number climbs, the old bands stop discriminating — everything saturates at the
top colour and the map goes flat. Rescaling genuinely does make each map, on its
own, more informative. The sin is doing it and still presenting the pair as a
before-and-after.
## The repentance
One key for every panel you want compared. That's the whole rule, and the fixed
version above does nothing else: same data, same dates, one legend serving both.
The rise is still perfectly visible — it just isn't amplified.
If the range really has outgrown the old bands, you have two honest options.
Rebin *both* panels, so the comparison stays like-for-like. Or keep the panels
separate, drop the side-by-side framing, and let each map be a snapshot rather
than half of an implied trend.
And if the two panels must carry different keys, say so loudly, in the chart —
not in a caption underneath, where it will be cropped off the first time anyone
shares it.