| name | tufte-chart-design |
| description | Apply Edward Tufte's visualization principles to produce data-honest, high data-ink ratio charts with minimal chartjunk |
| triggers | ["make a chart","create a visualization","design a dashboard","improve this chart","what chart should I use","apply Tufte principles","visualize this data","build a graph"] |
Tufte Chart Design Skill
Skill by ara.so — Claude Code Skills collection.
This skill teaches you to create charts following Edward Tufte's principles from his three foundational books: The Visual Display of Quantitative Information, Envisioning Information, and Visual Explanations. It turns "make me a chart" into a Tufte-compliant visualization with maximum data-ink ratio and zero chartjunk.
What This Skill Does
- Activates automatically when you're asked to create, improve, or critique any chart or visualization
- Applies ten core principles distilled from Tufte's work
- Chooses the right chart type based on data shape and analytical goal
- Removes chartjunk — 3D effects, unnecessary gridlines, redundant legends, pie charts, dual axes
- Produces clean output as inline SVG, React (Recharts), or D3-in-React
- Enforces graphical integrity — lie factor between 0.95 and 1.05, bars always start at zero
The Ten Principles
- Show the data — Above all else. Every decision asks "does this help see the data?"
- Maximize data-ink ratio — Ink that changes when data changes is data-ink. Everything else is overhead.
- Erase non-data-ink — Remove frame boxes, gridlines, tick marks at minor units.
- Erase redundant data-ink — One encoding per quantity. Don't show "37" as a bar + number + gridline + tick.
- Graphical integrity — Bars start at zero. Never use area for 1D quantities. Lie factor = (size of effect shown) / (size of effect in data).
- Small multiples — N small charts on shared scales beats one chart with N overlapping lines.
- Layering and separation — Data on top, labels next, scaffolding faintest. Avoid 1+1=3 visual noise.
- Micro/macro readings — One shape from across the room, every point legible up close.
- Smallest effective difference — Gray for context, one accent for focus. Subtle but clear.
- Word-data integration — Sparklines in sentences. Numbers next to visuals. Tables are charts.
When This Skill Activates
Auto-triggers on:
- "Make a chart of...", "visualize...", "graph...", "plot..."
- "Build a dashboard", "design a KPI tile"
- "Improve this chart", "critique this visual"
- "What chart should I use for...?"
/tufte command or mentioning Tufte by name
- Any code editing session involving chart/visualization code
Does NOT trigger for:
- Logos, illustrations, decorative graphics
- Flowcharts, architecture diagrams, wireframes
- Pure typography or layout without quantitative data
Chart Selection Guide
| Data Shape | Goal | Use | NOT |
|---|
| One time series | Show trend | Sparkline or line chart | Area chart, bar chart |
| Multiple time series | Compare trends | Small multiples (separate charts) | Multi-line spaghetti |
| Categories (≤10) | Compare values | Sorted dot plot or bar chart | Pie, donut, 3D bars |
| Categories (>10) | Show distribution | Strip plot or histogram | Rainbow color scales |
| Two time points | Show change | Slopegraph | Clustered bars |
| Part-to-whole | Show composition | Sorted table with percentages | Pie, donut, stacked bars |
| Process stages | Show flow | Horizontal bars + percentages | 3D funnel |
| Correlation | Show relationship | Scatterplot | Dual-axis line chart |
| Cohorts over time | Show retention | Sequential heatmap + sparklines | Rainbow heatmap |
| Geographic | Show regional pattern | Choropleth (single-hue sequential) | Rainbow choropleth |
Kill List — Never Use
❌ 3D effects — distort area perception, add no information
❌ Pie charts — humans can't compare angles; use sorted table instead
❌ Donut charts — same problem as pie, worse
❌ Dual-axis charts — viewer can't tell if correlation is real or axis-manipulation
❌ Rainbow color scales — perceptually non-uniform; use sequential single-hue
❌ Gradient fills — decorative ink, not data-ink
❌ Drop shadows, glows — chartjunk
❌ Chart frames/boxes — erase, data should float
❌ Heavy gridlines — if you need them, print values instead
❌ Redundant legends — direct-label instead
HTML/SVG Output Pattern
When asked for inline SVG or given no preference, use this pattern:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 11px;
color: #1a1a1a;
background: white;
padding: 40px;
}
.label { font-size: 10px; fill: #666; }
.value { font-size: 13px; fill: #1a1a1a; font-weight: 500; }
.data-point { fill: #1a1a1a; }
.data-line { stroke: #1a1a1a; stroke-width: 1.5; fill: none; }
.reference-line { stroke: #e0e0e0; stroke-width: 1; }
.accent { fill: #d73027; }
</style>
</head>
<body>
<svg width="600" height="200" viewBox="0 0 600 200">
</svg>
</body>
</html>
Sparkline Example
<div style="font-family: sans-serif; padding: 20px;">
<div style="font-size: 11px; color: #666; margin-bottom: 4px;">Monthly Active Users</div>
<div style="display: flex; align-items: baseline; gap: 12px;">
<span style="font-size: 24px; font-weight: 500; color: #1a1a1a;">247K</span>
<svg width="120" height="24" style="display: block;">
<polyline
points="0,18 10,15 20,16 30,14 40,12 50,13 60,10 70,9 80,11 90,8 100,6 110,4 120,3"
stroke="#1a1a1a"
stroke-width="1.5"
fill="none"
/>
<text x="0" y="24" font-size="9" fill="#999">Jan</text>
<text x="110" y="24" font-size="9" fill="#999" text-anchor="end">Dec</text>
</svg>
</div>
</div>
Dot Plot Example
<svg width="400" height="180" viewBox="0 0 400 180">
<circle cx="320" cy="20" r="4" fill="#1a1a1a"/>
<text x="330" y="24" font-size="11" fill="#1a1a1a">Product A</text>
<text x="310" y="24" font-size="11" fill="#666" text-anchor="end">$247K</text>
<circle cx="280" cy="45" r="4" fill="#1a1a1a"/>
<text x="290" y="49" font-size="11" fill="#1a1a1a">Product B</text>
<text x="270" y="49" font-size="11" fill="#666" text-anchor="end">$213K</text>
<circle cx="190" cy="70" r="4" fill="#1a1a1a"/>
<text x="200" y="74" font-size="11" fill="#1a1a1a">Product C</text>
<text x="180" y="74" font-size="11" fill="#666" text-anchor="end">$145K</text>
<line x1="0" y1="160" x2="320" y2="160" stroke="#e0e0e0" stroke-width="1"/>
<text x="0" y="175" font-size="9" fill="#999">$0</text>
<text x="320" y="175" font-size="9" fill="#999" text-anchor="end">$250K</text>
</svg>
React/Recharts Output Pattern
When asked for React or Recharts:
import { LineChart, Line, ResponsiveContainer, XAxis, YAxis } from 'recharts';
const data = [
{ month: 'Jan', value: 180 },
{ month: 'Feb', value: 195 },
{ month: 'Mar', value: 205 },
{ month: 'Apr', value: 198 },
{ month: 'May', value: 215 },
{ month: 'Jun', value: 247 },
];
export default function RevenueChart() {
return (
<div style={{ fontFamily: 'system-ui, sans-serif' }}>
<div style={{ fontSize: 11, color: '#666', marginBottom: 4 }}>
Monthly Revenue (thousands)
</div>
<ResponsiveContainer width="100%" height={120}>
<LineChart data={data} margin={{ top: 5, right: 5, bottom: 5, left: 5 }}>
<XAxis
dataKey="month"
axisLine={false}
tickLine={false}
tick={{ fontSize: 10, fill: '#999' }}
interval="preserveStartEnd"
/>
<YAxis
hide={true}
/>
<Line
type="monotone"
dataKey="value"
stroke="#1a1a1a"
strokeWidth={1.5}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
<div style={{ fontSize: 20, fontWeight: 500, color: '#1a1a1a' }}>
$247K
</div>
</div>
);
}
Recharts Theme Config
Apply these props to strip chartjunk:
const tufteTheme = {
xAxis: {
axisLine: false,
tickLine: false,
tick: { fontSize: 10, fill: '#999' },
},
yAxis: {
axisLine: false,
tickLine: false,
tick: { fontSize: 10, fill: '#999' },
},
grid: {
strokeDasharray: '3 3',
stroke: '#f0f0f0',
},
};
D3-in-React Pattern (for Custom Charts)
For slopegraphs, custom dot plots, small multiples:
import { useEffect, useRef } from 'react';
import * as d3 from 'd3';
export default function Slopegraph({ data }) {
const svgRef = useRef();
useEffect(() => {
const svg = d3.select(svgRef.current);
const width = 400;
const height = 300;
const margin = { top: 20, right: 80, bottom: 20, left: 80 };
svg.selectAll('*').remove();
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => Math.max(d.q2, d.q3))])
.range([height - margin.bottom, margin.top]);
svg.selectAll('line.slope')
.data(data)
.join('line')
.attr('class', 'slope')
.attr('x1', margin.left)
.attr('x2', width - margin.right)
.attr('y1', d => y(d.q2))
.attr('y2', d => y(d.q3))
.attr('stroke', d => d.q3 < d.q2 ? '#d73027' : '#ccc')
.attr('stroke-width', 1.5);
svg.selectAll('text.left-label')
.data(data)
.join('text')
.attr('class', 'left-label')
.attr('x', margin.left - 8)
.attr('y', d => y(d.q2))
.attr('text-anchor', 'end')
.attr('alignment-baseline', 'middle')
.attr('font-size', 11)
.attr('fill', '#1a1a1a')
.text(d => `${d.team} ${d.q2}`);
svg.selectAll('text.right-label')
.data(data)
.join('text')
.attr('class', 'right-label')
.attr('x', width - margin.right + 8)
.attr('y', d => y(d.q3))
.attr('text-anchor', 'start')
.attr('alignment-baseline', 'middle')
.attr('font-size', 11)
.attr('fill', '#1a1a1a')
.text(d => d.q3);
svg.append('text')
.attr('x', margin.left)
.attr('y', 10)
.attr('text-anchor', 'middle')
.attr('font-size', 10)
.attr('fill', '#666')
.text('Q2');
svg.append('text')
.attr('x', width - margin.right)
.attr('y', 10)
.attr('text-anchor', 'middle')
.attr('font-size', 10)
.attr('fill', '#666')
.text('Q3');
}, [data]);
return <svg ref={svgRef} width={400} height={300} />;
}
Small Multiples Pattern
When comparing multiple series, create separate charts on a shared scale instead of overlaying:
const regions = ['North', 'South', 'East', 'West'];
const sharedYDomain = [0, 300];
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 20 }}>
{regions.map(region => (
<div key={region}>
<div style={{ fontSize: 11, color: '#666', marginBottom: 4 }}>
{region}
</div>
<ResponsiveContainer width="100%" height={100}>
<LineChart data={data.filter(d => d.region === region)}>
<YAxis domain={sharedYDomain} hide />
<XAxis dataKey="month" tick={{ fontSize: 9 }} />
<Line type="monotone" dataKey="value" stroke="#1a1a1a" strokeWidth={1.5} dot={false} />
</LineChart>
</ResponsiveContainer>
</div>
))}
</div>
);
Dashboard KPI Tile Pattern
Replace giant number tiles with table + sparkline + target delta:
const kpis = [
{ metric: 'Revenue', value: 247, trend: [180,195,205,198,215,247], target: 250, unit: 'K' },
{ metric: 'Users', value: 12.4, trend: [10.2,10.8,11.1,11.5,12.0,12.4], target: 12, unit: 'K' },
{ metric: 'Churn', value: 3.2, trend: [2.1,2.4,2.7,2.9,3.1,3.2], target: 2.5, unit: '%' },
];
return (
<table style={{ fontFamily: 'sans-serif', fontSize: 11, borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid #e0e0e0', textAlign: 'left' }}>
<th style={{ padding: '8px 12px', fontWeight: 500 }}>Metric</th>
<th style={{ padding: '8px 12px', fontWeight: 500 }}>Current</th>
<th style={{ padding: '8px 12px', fontWeight: 500 }}>Trend</th>
<th style={{ padding: '8px 12px', fontWeight: 500 }}>vs Target</th>
</tr>
</thead>
<tbody>
{kpis.map(kpi => {
const delta = ((kpi.value - kpi.target) / kpi.target * 100).toFixed(0);
const isBad = (kpi.metric === 'Churn' ? delta > 0 : delta < 0);
return (
<tr key={kpi.metric}>
<td style={{ padding: '8px 12px' }}>{kpi.metric}</td>
<td style={{ padding: '8px 12px', fontWeight: 500 }}>
{kpi.value}{kpi.unit}
</td>
<td style={{ padding: '8px 12px' }}>
<Sparkline data={kpi.trend} width={60} height={20} />
</td>
<td style={{
padding: '8px 12px',
color: isBad ? '#d73027' : '#666',
fontWeight: isBad ? 500 : 400
}}>
{delta > 0 ? '+' : ''}{delta}%
</td>
</tr>
);
})}
</tbody>
</table>
);
Pre-Publish Checklist
Before declaring a chart done, verify:
Common Overrides
If a user explicitly requests something on the kill list:
"Make a pie chart — the CFO wants a pie"
Comply, but add a comment noting the Tufte alternative:
<svg>...</svg>
User stays in control; you provide the alternative for reference.
Color Rules
- Default: Grayscale. Black for data, mid-gray (#666) for labels, light-gray (#e0e0e0) for reference lines.
- One accent: Use red (#d73027) or blue (#4575b4) to highlight one series or flag an outlier.
- Sequential scales: Single-hue (light to dark). Never rainbow.
- Diverging scales: Two hues meeting at neutral (red-white-blue). Equal steps.
- Categorical: Only if categories have no natural order. Max 5 colors. Consider direct-label + grayscale instead.
Troubleshooting
| Issue | Fix |
|---|
| "Chart feels cramped" | Increase padding, reduce font size, or use small multiples |
| "Can't read labels" | Direct-label at data points instead of axis; rotate if needed |
| "Too much white space" | Tufte prefers it — resist urge to fill; let data breathe |
| "Legend overlaps data" | Remove legend; direct-label each line/bar at its endpoint |
| "Gridlines requested" | Print values on chart instead; use faint reference line only at key thresholds |
| "Dual-axis requested" | Make two separate charts with shared X-axis; stack vertically |
Configuration
No installation needed — this skill is already active in your environment.
Override strictness per request:
- Default: Apply Tufte principles unless user explicitly asks otherwise
- User can say "ignore Tufte for this one" or "CFO wants X" and you comply (with a note of the alternative)
Stack preference (auto-detected from user request):
- "inline SVG" or no preference → self-contained HTML/SVG
- "in React" or "with Recharts" → Recharts component
- "with D3" or "custom layout" → D3-in-React
Reference
This skill distills:
- The Visual Display of Quantitative Information (1983, 2nd ed. 2001) — data honesty, data-ink ratio, lie factor, small multiples
- Envisioning Information (1990) — layering, micro/macro, escaping flatland
- Visual Explanations (1997) — smallest effective difference, narrative, visual confections
Core Tufte quote (VDQI p.105):
Above all else show the data. Maximize the data-ink ratio. Erase non-data-ink. Erase redundant data-ink. Revise and edit.
When in doubt: Show the data. Remove everything else. Let the numbers speak.