mirror of
https://github.com/olehomelchenko/chart-sins.git
synced 2026-08-08 02:22:43 +00:00
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:
@@ -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,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,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
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
|
||||||
/** 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>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
Reference in New Issue
Block a user