diff --git a/README.md b/README.md
index 4860d11..e957192 100644
--- a/README.md
+++ b/README.md
@@ -3,21 +3,34 @@
A catalogue of data-visualization sins — for each one: the chart that misleads,
why it fools the eye, and the honest version that fixes it.
-Built with [Astro](https://astro.build). Charts are authored as
+Built with [Astro](https://astro.build) and styled on the
+[Carbon Design System](https://carbondesignsystem.com). Charts are authored as
[Vega-Lite](https://vega.github.io/vega-lite/) specs and rendered to **static
SVG at build time**, so pages ship with zero client-side JavaScript.
## Tech stack
-| Concern | Choice |
+| Concern | Choice |
| ---------- | --------------------------------------------------- |
| Framework | Astro 5 (static output) |
| Content | Markdown/MDX via Astro Content Collections |
| Charts | Vega-Lite specs → SVG at build (headless Vega) |
-| Styling | Plain scoped CSS + one global stylesheet |
+| Design | Carbon Design System — tokens, type scale, spacing, motion, themes |
+| Typeface | IBM Plex Sans / Mono (self-hosted via `@fontsource`) |
+| Styling | Token-driven CSS (`src/styles/global.css`) |
| Hosting | GitHub Pages (via GitHub Actions) |
| Language | TypeScript |
+### On the Carbon adoption
+
+Carbon ships React/web-component libraries built for application UIs and they
+ship JavaScript; this is a static content site, so instead of the component
+runtime we adopt Carbon at the **design-token level** — its color tokens and
+White/Gray-100 themes, the 8px spacing scale, the productive/expressive type
+distinction (IBM Plex), motion tokens, square geometry, the focus ring, and
+Carbon's **data-visualization palette** for the charts (validated for
+colorblind-safety and contrast on the chart surface).
+
## Project layout
```
diff --git a/package.json b/package.json
index ecd0725..313bed9 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,8 @@
},
"dependencies": {
"@astrojs/mdx": "^4.2.0",
+ "@fontsource/ibm-plex-mono": "^5.3.0",
+ "@fontsource/ibm-plex-sans": "^5.3.0",
"astro": "^5.6.0",
"vega": "^5.30.0",
"vega-lite": "^5.21.0"
diff --git a/src/charts/dual-axis-bad.json b/src/charts/dual-axis-bad.json
index dcce7db..4694d27 100644
--- a/src/charts/dual-axis-bad.json
+++ b/src/charts/dual-axis-bad.json
@@ -19,15 +19,15 @@
},
"layer": [
{
- "mark": { "type": "line", "color": "#c0392b", "point": true },
+ "mark": { "type": "line", "color": "#fa4d56", "point": true },
"encoding": {
- "y": { "field": "icecream", "type": "quantitative", "title": "Ice cream sales", "axis": { "titleColor": "#c0392b" } }
+ "y": { "field": "icecream", "type": "quantitative", "title": "Ice cream sales", "axis": { "titleColor": "#fa4d56" } }
}
},
{
- "mark": { "type": "line", "color": "#2980b9", "point": true },
+ "mark": { "type": "line", "color": "#1192e8", "point": true },
"encoding": {
- "y": { "field": "sharks", "type": "quantitative", "title": "Shark attacks", "axis": { "titleColor": "#2980b9" } }
+ "y": { "field": "sharks", "type": "quantitative", "title": "Shark attacks", "axis": { "titleColor": "#1192e8" } }
}
}
],
diff --git a/src/charts/dual-axis-fixed.json b/src/charts/dual-axis-fixed.json
index 65d10ad..9cbd493 100644
--- a/src/charts/dual-axis-fixed.json
+++ b/src/charts/dual-axis-fixed.json
@@ -16,7 +16,7 @@
{
"width": 360,
"height": 110,
- "mark": { "type": "line", "color": "#c0392b", "point": true },
+ "mark": { "type": "line", "color": "#fa4d56", "point": true },
"encoding": {
"x": { "field": "month", "type": "ordinal", "sort": null, "axis": { "title": null, "labels": false } },
"y": { "field": "icecream", "type": "quantitative", "scale": { "zero": true }, "title": "Ice cream" }
@@ -25,7 +25,7 @@
{
"width": 360,
"height": 110,
- "mark": { "type": "line", "color": "#2980b9", "point": true },
+ "mark": { "type": "line", "color": "#1192e8", "point": true },
"encoding": {
"x": { "field": "month", "type": "ordinal", "sort": null, "axis": { "title": null, "labelAngle": 0 } },
"y": { "field": "sharks", "type": "quantitative", "scale": { "zero": true }, "title": "Shark attacks" }
diff --git a/src/charts/pie-overload-fixed.json b/src/charts/pie-overload-fixed.json
index 00d43f8..d6ae119 100644
--- a/src/charts/pie-overload-fixed.json
+++ b/src/charts/pie-overload-fixed.json
@@ -14,7 +14,7 @@
{ "browser": "Other", "share": 2 }
]
},
- "mark": { "type": "bar", "color": "#1e8b5b" },
+ "mark": { "type": "bar", "color": "#198038" },
"encoding": {
"y": { "field": "browser", "type": "nominal", "sort": "-x", "axis": { "title": null } },
"x": { "field": "share", "type": "quantitative", "title": "Share (%)" }
diff --git a/src/charts/truncated-axis-bad.json b/src/charts/truncated-axis-bad.json
index 4c1649b..4d740b6 100644
--- a/src/charts/truncated-axis-bad.json
+++ b/src/charts/truncated-axis-bad.json
@@ -12,7 +12,7 @@
{ "month": "Jun", "revenue": 98 }
]
},
- "mark": { "type": "bar", "color": "#c0392b" },
+ "mark": { "type": "bar", "color": "#da1e28" },
"encoding": {
"x": {
"field": "month",
diff --git a/src/charts/truncated-axis-fixed.json b/src/charts/truncated-axis-fixed.json
index 9929bdc..020ae1e 100644
--- a/src/charts/truncated-axis-fixed.json
+++ b/src/charts/truncated-axis-fixed.json
@@ -12,7 +12,7 @@
{ "month": "Jun", "revenue": 98 }
]
},
- "mark": { "type": "bar", "color": "#1e8b5b" },
+ "mark": { "type": "bar", "color": "#198038" },
"encoding": {
"x": {
"field": "month",
diff --git a/src/components/VegaChart.astro b/src/components/VegaChart.astro
index 4fc9fbe..b7e8c2f 100644
--- a/src/components/VegaChart.astro
+++ b/src/components/VegaChart.astro
@@ -1,39 +1,76 @@
---
-// Build-time Vega-Lite renderer.
+// Build-time Vega-Lite renderer, themed on Carbon.
//
-// This component runs only during `astro build` (and dev SSR), never in the
-// browser. It compiles a Vega-Lite spec to Vega, renders it to a static SVG
-// string with Vega's headless view, and inlines that SVG. The result ships as
-// plain markup — zero client-side JavaScript, no Vega runtime downloaded.
+// Runs only during `astro build` (and dev SSR), never in the browser: it
+// compiles a Vega-Lite spec to Vega with a Carbon theme (IBM Plex type +
+// Carbon's data-visualization palette), renders it headlessly to a static SVG
+// string, and inlines it. Zero client-side JavaScript, no Vega runtime shipped.
//
-// To make a specific chart interactive later, you would swap this component
-// out for a client-side vega-embed island on that page only.
+// The chart sits on a fixed light Carbon canvas (--chart-canvas) in both
+// themes, so this single baked render keeps AA contrast in light and dark.
import * as vega from 'vega';
-import { compile, type TopLevelSpec } from 'vega-lite';
+import { compile, type TopLevelSpec, type Config } from 'vega-lite';
interface Props {
- /** A Vega-Lite spec (object). Do not include $schema/width/height/config. */
spec: TopLevelSpec;
- /** Accessible description of what the chart shows. */
title: string;
- /** Optional caption rendered under the chart. */
caption?: string;
}
const { spec, title, caption } = Astro.props;
+// Carbon Vega theme — the design-system parameters the renderer consumes.
+const carbonTheme: Config = {
+ font: 'IBM Plex Sans',
+ background: 'transparent',
+ view: { stroke: null },
+ title: {
+ color: '#161616',
+ font: 'IBM Plex Sans',
+ fontSize: 13,
+ fontWeight: 600,
+ anchor: 'start',
+ dy: -8,
+ },
+ axis: {
+ labelFont: 'IBM Plex Sans',
+ labelFontSize: 11,
+ labelColor: '#525252',
+ titleFont: 'IBM Plex Sans',
+ titleFontSize: 12,
+ titleFontWeight: 600,
+ titleColor: '#393939',
+ gridColor: '#e0e0e0',
+ domainColor: '#8d8d8d',
+ tickColor: '#8d8d8d',
+ labelPadding: 4,
+ },
+ legend: {
+ labelFont: 'IBM Plex Sans',
+ labelFontSize: 11,
+ labelColor: '#525252',
+ titleFont: 'IBM Plex Sans',
+ titleFontSize: 12,
+ titleColor: '#393939',
+ symbolType: 'square',
+ },
+ // Carbon data-visualization categorical palette (validated: passes the
+ // dataviz colorblind/contrast checks on the --chart-canvas surface).
+ range: {
+ category: ['#8a3ffc', '#009d9a', '#fa4d56', '#0f62fe', '#24a148', '#d02670', '#b28600', '#1192e8'],
+ },
+};
+
let svg = '';
let error: string | null = null;
try {
- // Compile Vega-Lite -> Vega, then render headlessly to SVG.
- const vgSpec = compile(spec as TopLevelSpec).spec;
+ const vgSpec = compile(spec, { config: carbonTheme }).spec;
const view = new vega.View(vega.parse(vgSpec), { renderer: 'none' }).initialize();
svg = await view.toSVG();
view.finalize();
} catch (e) {
error = e instanceof Error ? e.message : String(e);
- // Fail loudly in the build log, but don't crash the whole build.
console.error(`[VegaChart] failed to render "${title}": ${error}`);
}
---
@@ -49,32 +86,3 @@ try {
)}
{caption &&