Generalize pokes; drop citations from OG card

- Rewrite pokes to name the sin itself, not our sample's exact numbers
  (a visitor didn't "start at 90" — they mis-set the baseline)
- Remove the "cited:" line from the OG card; citations stay on the page

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPEvmMbac2fCvKXpQcovj8
This commit is contained in:
Claude
2026-08-05 14:10:55 +00:00
parent dc0ce07dce
commit 4c63e57ac6
5 changed files with 4 additions and 18 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
title: "The Dual-Axis Deception" title: "The Dual-Axis Deception"
summary: "Two unrelated series on two independent axes, engineered to look correlated." summary: "Two unrelated series on two independent axes, engineered to look correlated."
poke: "Two y-axes, tuned until the lines kiss. Congratulations — you can now make anything 'cause' anything." poke: "Two y-axes, scaled until the lines overlap — and suddenly anything 'causes' anything."
category: "False Relationships" category: "False Relationships"
severity: 5 severity: 5
tags: ["dual axis", "correlation", "line chart"] tags: ["dual axis", "correlation", "line chart"]
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
title: "Pie Chart Overload" title: "Pie Chart Overload"
summary: "Eight near-identical slices no human eye can rank. A bar chart was right there." summary: "Eight near-identical slices no human eye can rank. A bar chart was right there."
poke: "Eight slices, three of them the same size. Nobody can read this. A bar chart could." poke: "More slices than anyone can rank by eye. A bar chart was right there."
category: "Chart-Type Abuse" category: "Chart-Type Abuse"
severity: 3 severity: 3
tags: ["pie chart", "part-to-whole", "comparison"] tags: ["pie chart", "part-to-whole", "comparison"]
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
title: "The Truncated Y-Axis" title: "The Truncated Y-Axis"
summary: "Chopping the baseline off a bar chart to make small differences look enormous." summary: "Chopping the baseline off a bar chart to make small differences look enormous."
poke: "You started the y-axis at 90. That's not a bar chart — it's a magic trick." poke: "Your bars don't start at zero. That's not a bar chart — it's a magic trick."
category: "Misleading Scales" category: "Misleading Scales"
severity: 4 severity: 4
tags: ["bar chart", "axis", "exaggeration"] tags: ["bar chart", "axis", "exaggeration"]
+1 -14
View File
@@ -5,7 +5,6 @@ import { html } from 'satori-html';
import { Resvg } from '@resvg/resvg-js'; import { Resvg } from '@resvg/resvg-js';
import type { TopLevelSpec } from 'vega-lite'; import type { TopLevelSpec } from 'vega-lite';
import { renderChartSvg } from './renderChart'; import { renderChartSvg } from './renderChart';
import { getReference } from './references';
// OpenGraph card generator. Runs only at build time. Composes a 1200×630 card // OpenGraph card generator. Runs only at build time. Composes a 1200×630 card
// with satori (text → vector paths, so the raster step needs no fonts) and // with satori (text → vector paths, so the raster step needs no fonts) and
@@ -34,13 +33,6 @@ const fonts = [
const esc = (s: string) => const esc = (s: string) =>
s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
/** Surname of the first author, for a compact "cited: Tufte, Cairo" line. */
function shortAuthor(key: string): string {
const first = getReference(key).authors.split(/ & |,|;/)[0].trim();
const parts = first.split(' ');
return parts[parts.length - 1];
}
/** Render a chart spec to a PNG data URI at a fixed display height (2× for crispness). /** Render a chart spec to a PNG data URI at a fixed display height (2× for crispness).
* Fixing height (not width) keeps tall vconcat charts from overflowing the card. */ * Fixing height (not width) keeps tall vconcat charts from overflowing the card. */
async function chartImage(spec: TopLevelSpec, displayHeight: number) { async function chartImage(spec: TopLevelSpec, displayHeight: number) {
@@ -79,13 +71,11 @@ export interface SinCard {
severity: number; severity: number;
badSpec: TopLevelSpec; badSpec: TopLevelSpec;
fixedSpec: TopLevelSpec; fixedSpec: TopLevelSpec;
citationKeys: string[];
} }
export async function renderSinCard(d: SinCard): Promise<Buffer> { export async function renderSinCard(d: SinCard): Promise<Buffer> {
const bad = await chartImage(d.badSpec, 250); const bad = await chartImage(d.badSpec, 250);
const fixed = await chartImage(d.fixedSpec, 250); const fixed = await chartImage(d.fixedSpec, 250);
const cited = d.citationKeys.slice(0, 3).map(shortAuthor).join(', ');
const panel = (label: string, color: string, img: { uri: string; width: number; height: number }) => ` const panel = (label: string, color: string, img: { uri: string; width: number; height: number }) => `
<div style="display:flex;flex-direction:column;background:#f4f4f4;padding:12px;border-top:5px solid ${color}"> <div style="display:flex;flex-direction:column;background:#f4f4f4;padding:12px;border-top:5px solid ${color}">
@@ -104,10 +94,7 @@ export async function renderSinCard(d: SinCard): Promise<Buffer> {
${panel('THE SIN', '#fa4d56', bad)} ${panel('THE SIN', '#fa4d56', bad)}
${panel('THE FIX', '#42be65', fixed)} ${panel('THE FIX', '#42be65', fixed)}
</div> </div>
<div style="display:flex;justify-content:space-between;margin-top:22px;font-size:17px;color:#a8a8a8"> <div style="display:flex;margin-top:22px;font-size:17px;color:#a8a8a8">${esc(d.category)}</div>
<div style="display:flex">${esc(d.category)}</div>
<div style="display:flex">${cited ? 'cited: ' + esc(cited) : ''}</div>
</div>
</div> </div>
`); `);
} }
-1
View File
@@ -16,7 +16,6 @@ export const GET: APIRoute = async ({ props }) => {
severity: sin.data.severity, severity: sin.data.severity,
badSpec: getChartSpec(sin.data.badChart), badSpec: getChartSpec(sin.data.badChart),
fixedSpec: getChartSpec(sin.data.fixedChart), fixedSpec: getChartSpec(sin.data.fixedChart),
citationKeys: sin.data.citations.map((c) => c.key),
}); });
return new Response(new Uint8Array(png), { return new Response(new Uint8Array(png), {
headers: { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=31536000, immutable' }, headers: { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=31536000, immutable' },