Scaffold Chart Sins site with Astro + build-time Vega-Lite

Set up the static-site tech stack: Astro 5 with Markdown content
collections, Vega-Lite specs rendered to static SVG at build time
(zero client JS), plain scoped CSS, and a GitHub Pages deploy workflow.

Includes three sample sins (truncated y-axis, dual-axis deception,
pie chart overload), each with a bad and fixed chart spec, plus a
gallery index and per-sin detail pages.

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 11:57:52 +00:00
commit 2e17f8fa59
23 changed files with 978 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
---
import { getCollection } from 'astro:content';
import BaseLayout from '../layouts/BaseLayout.astro';
import { severityFlames } from '../lib/charts';
import { href } from '../lib/url';
const sins = (await getCollection('sins', ({ data }) => !data.draft)).sort((a, b) => {
// Most severe first; ties broken by newest date.
if (b.data.severity !== a.data.severity) return b.data.severity - a.data.severity;
return (b.data.date?.getTime() ?? 0) - (a.data.date?.getTime() ?? 0);
});
---
<BaseLayout title="Chart Sins">
<section class="wrap hero">
<h1>A catalogue of chart sins.</h1>
<p>
Every entry is a data-visualization mistake in the wild: the chart that
misleads, why it fools the eye, and the honest version that fixes it.
</p>
</section>
<section class="wrap">
<div class="gallery">
{
sins.map((sin) => (
<a class="card" href={href(`sins/${sin.id}/`)}>
<span class="pill">{sin.data.category}</span>
<h3>{sin.data.title}</h3>
<p>{sin.data.summary}</p>
<div class="meta">
<span class="severity" title={`Severity ${sin.data.severity} of 5`}>
{severityFlames(sin.data.severity)}
</span>
<span class="pill">Read →</span>
</div>
</a>
))
}
</div>
</section>
</BaseLayout>
+53
View File
@@ -0,0 +1,53 @@
---
import { getCollection, render } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import VegaChart from '../../components/VegaChart.astro';
import { getChartSpec, severityFlames } from '../../lib/charts';
import { href } from '../../lib/url';
export async function getStaticPaths() {
const sins = await getCollection('sins', ({ data }) => !data.draft);
return sins.map((sin) => ({
params: { slug: sin.id },
props: { sin },
}));
}
const { sin } = Astro.props;
const { Content } = await render(sin);
const badSpec = getChartSpec(sin.data.badChart);
const fixedSpec = getChartSpec(sin.data.fixedChart);
---
<BaseLayout title={`${sin.data.title} — Chart Sins`} description={sin.data.summary}>
<article class="wrap sin">
<header>
<span class="pill">{sin.data.category}</span>
<h1>{sin.data.title}</h1>
<span class="severity" title={`Severity ${sin.data.severity} of 5`}>
{severityFlames(sin.data.severity)}
</span>
<div class="tags">
{sin.data.tags.map((t) => <span class="pill">{t}</span>)}
</div>
</header>
<div class="compare">
<div class="panel bad">
<h2>The Sin</h2>
<VegaChart spec={badSpec} title={`Misleading chart: ${sin.data.title}`} />
</div>
<div class="panel good">
<h2>The Fix</h2>
<VegaChart spec={fixedSpec} title={`Honest chart: ${sin.data.title}`} />
</div>
</div>
<div class="prose">
<Content />
</div>
<a class="back-link" href={href()}>← All sins</a>
</article>
</BaseLayout>