Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Choose, encode, color, label, and ship accurate accessible charts and dashboards — perceptual encoding ranking, chart-by-intent selection, data color scales, Tufte's data-ink discipline, and interaction patterns with concrete do/don't rules.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 80% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 293% | 0% |
| case-19 | ✗→✓ | ▲ Improved | 201% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 358% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 320% | 0% |
A chart is an answer to a question, not decoration. Before any encoding decision, state the question in one sentence ("Which region grew fastest last quarter?"). The question dictates the chart; the data dictates the scale; perception dictates the encoding. This skill gives the ranking, the chart map, the color math, and the discipline to ship charts that read correctly on any screen.
Never start from "what data do I have." Start from the verb in the user's question, then map it to an intent. The intent picks the chart (§3). If you can't name the question, you can't pick the chart — and a chart with no question becomes a rainbow blob that demos well and informs nothing.
| The user asks… | Intent | Default chart | |---|---|---| | "How do these categories compare?" | Comparison | Bar (horizontal if labels long) | | "How has this changed over time?" | Trend | Line (area only if cumulative/stacked) | | "What makes up the total?" | Part-to-whole | Stacked bar / treemap | | "How is this spread out?" | Distribution | Histogram / box / violin | | "Do these two move together?" | Correlation | Scatter | | "Where is this happening?" | Geospatial | Choropleth / symbol map | | "How does flow move between stages?" | Flow | Sankey / alluvial | | "Who's on top?" | Ranking | Sorted bar / bump / slope | | "What's the single number now?" | KPI | Big number + sparkline |
Do put the question or its answer in the chart title ("Sales grew 23% in EMEA"), not the chart type ("Bar chart of sales"). Don't ship a "dashboard of everything" — that's a furniture catalog, not an analysis.
Cleveland & McGill (1984, JASA) ran controlled experiments ranking how accurately humans decode quantitative values from each visual channel. This is the single most important table in data viz. Encode your most important quantity with the highest channel you can.
| Rank | Channel | Decode accuracy | Where it shows up | |---|---|---|---| | 1 | Position on a common scale | Best | Bar tops, scatter dots, dot plots | | 2 | Position on non-aligned scales | Very good | Small multiples | | 3 | Length | Good | Bars, stacked-bar segments | | 4 | Angle / slope | Moderate | Slopegraphs, line steepness | | 5 | Area | Poor | Bubbles, treemaps | | 6 | Volume / 3D | Very poor | (avoid) | | 7 | Color hue / saturation | Worst for quantity | Heatmaps, choropleths |
Rule: position > length > angle/slope > area > volume > color. A pie chart asks the eye to compare angles and areas (ranks 4–5); the same data as a bar chart uses position/length (ranks 1–3) — which is exactly why bars beat pies. Munzner (Visualization Analysis & Design, 2014) frames this as matching channel "effectiveness" to attribute importance: spend your best channel on what matters most.
r ∝ √value), never radius∝value.js// Bubble sizing — correct: human-perceived size ∝ value const r = maxR * Math.sqrt(value / maxValue); // ✅ area encodes value const rWrong = maxR * (value / maxValue); // ❌ radius∝value → 2× value looks 4× big
Worked example. "Which of 6 plans sells most?" Magnitude (units sold) is the question → give it position/length (sorted horizontal bar, rank 1+3). Plan tier is a secondary categorical attribute → give it color hue (rank 7, fine for nominal grouping). Don't invert this and make units the bubble area and tier the X-position — you'd spend your best channel on the thing you care least about.
Comparison → bar. Position+length, ranks 1 and 3. Horizontal bars when category labels are long (no rotated 45° text). Sort by value unless the category has a natural order (time, age bins). One color unless a category needs to pop.
Trend over time → line. Time on X (always), value on Y. Lines show rate-of-change (slope) directly. Use area only for a single cumulative series or stacked totals where the filled mass is the message; stacked areas above 3–4 bands become unreadable (you can't judge a middle band's thickness against a wavy baseline). Never stack non-additive metrics.
Part-to-whole → stacked bar or treemap; rarely pie. A pie is acceptable for 2–3 slices showing a rough split (60/40). Beyond that it fails: humans can't rank angle/area, and labels collide. A 100%-stacked bar compares the same parts across categories far better. Treemaps handle hierarchy and many parts but inherit area's low accuracy — use for "roughly how big" not "exactly how much." Donut = pie with a hole; same problems.
Distribution → histogram / box / violin. Histogram shows shape (modes, skew, gaps) — bin width is the key decision (too wide hides structure, too narrow is noise). Box plot compresses to median/quartiles/outliers — great for comparing many groups side by side, but hides bimodality entirely. Violin (or beeswarm/strip) restores the shape box plots throw away. Don't show a mean ± error bar and call it a distribution; it hides everything Anscombe's quartet and the datasaurus dozen warn about — wildly different data, identical summary stats.
js// Bin width — Freedman–Diaconis (robust to outliers; default in most libs) const iqr = q3 - q1; const binWidth = 2 * iqr / Math.cbrt(n); // then bins = ceil((max-min)/binWidth) // Quick fallback: bins ≈ ceil(√n). Always offer a manual override — one bin width never fits all data.
Correlation → scatter. Two quantitative axes, position on both (ranks 1–2). Add a trend/LOESS line for direction, faceting or color for a third categorical dimension. For >~2k points use opacity, hexbin, or density contours to fight overplotting. Don't infer causation from a scatter.
Geospatial → choropleth or symbol. Choropleth (regions shaded by value) must use a rate or normalized value, never a raw count — otherwise you're just drawing a population map. Symbol/bubble maps avoid the area-size bias of unequal regions. Beware that large rural regions visually dominate small dense urban ones (the "land doesn't vote" problem).
Flow → Sankey / alluvial. Width = magnitude (length channel) across stages; good for budgets, funnels, migration. Keep nodes few; crossing ribbons get hairball-y fast.
Ranking → sorted bar, bump, or slopegraph. Slopegraph (Tufte) compares two time points across items with one line each — reads rank changes instantly.
KPI / big-number. One metric, huge, with a delta (▲ +12% vs last week) and a sparkline for context. The number is the chart.
| Chart | Primary channel | Sort | Baseline | First thing to get right | |---|---|---|---|---| | Bar | position + length | by value | must be 0 | horizontal if labels long; one color | | Line | position / slope | n/a (time order) | data-fit OK | time on X; ≤5 series or highlight one | | Stacked bar | length | total or fixed order | 0 | put the series you compare at the baseline | | Histogram | position + length | bin order | 0 | bin width (FD/√n); show the override | | Box / violin | position | by median | n/a | violin if bimodality possible | | Scatter | position ×2 | n/a | data-fit | overplotting → opacity/hexbin past ~2k | | Choropleth | color (rank 7) | n/a | n/a | normalize to a rate, not raw count | | Treemap | area (rank 5) | by value | n/a | "roughly how big" only; label leaves |
Color is rank 7 for quantity — use it deliberately, by scale type.
| Scale | Data | Example | Rule | |---|---|---|---| | Sequential | Ordered, one-directional (0→high) | Viridis, single-hue ramps | Light = low, dark = high; perceptually even steps | | Diverging | Centered around a midpoint (− 0 +) | RdBu, BrBG | Neutral middle (white/grey); set the midpoint meaningfully (0, mean, target) | | Categorical | Unordered groups | Okabe-Ito, Tableau-10 | Distinct hues, similar lightness; max ~7 |
L steps look equally spaced; HSL "lightness" lies and yields muddy uneven ramps.css/* Sequential ramp — even perceptual lightness, fixed hue. Low→high = light→dark. */ --seq-1: oklch(0.96 0.03 250); --seq-2: oklch(0.86 0.07 250); --seq-3: oklch(0.74 0.11 250); --seq-4: oklch(0.60 0.15 250); --seq-5: oklch(0.46 0.15 250); /* Diverging — neutral grey midpoint, two hues out. Set midpoint to 0/target, not data-min. */ --div-neg: oklch(0.55 0.16 25); --div-mid: oklch(0.92 0.01 250); --div-pos: oklch(0.55 0.13 145);
Okabe-Ito categorical (CVD-safe, ship these): #000000 #E69F00 #56B4E9 #009E73 #F0E442 #0072B2 #D55E00 #CC79A7.
Edward Tufte (The Visual Display of Quantitative Information, 1983):
js// The bar-vs-line axis rule, in code const barYScale = d3.scaleLinear().domain([0, max]).range([h, 0]); // ✅ bars: domain[0]=0 const lineYScale = d3.scaleLinear().domain([min * 0.98, max * 1.02]).nice(); // ✅ lines: zoom to data, label clearly // Forcing the line to [0,max] often flattens real variation to a meaningless straight line.
js// Locale-aware, abbreviated, no false precision — use Intl, not hand-rolled const axisFmt = new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 }); axisFmt.format(1_240_000); // "1.2M" ← short axis ticks const pct = new Intl.NumberFormat('en', { style: 'percent', maximumFractionDigits: 1 }); pct.format(0.234); // "23.4%" const money = new Intl.NumberFormat('en', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }); money.format(5012); // "$5,012" ← tooltip shows exact; axis shows compact // Tabular figures keep decimals aligned in tables: CSS font-variant-numeric: tabular-nums;
A chart that only works for sighted, full-color-vision, mouse users is broken for a large minority. WCAG 2.2 and common sense both require:
<table>. This serves screen readers and power users who want exact numbers. A <figure> + <figcaption> summarizing the takeaway is the floor.role="img" with aria-label for static charts, or proper aria on interactive SVG. A hover-only tooltip is inaccessible; expose the same data on focus.prefers-reduced-motion — animated transitions, racing bar charts, and auto-rotating carousels can trigger vestibular issues. Gate transitions behind the media query; render the final state instantly when reduced motion is set.aria-label or caption should state.html<!-- Floor pattern: figure + caption + collapsible data table --> <figure role="group" aria-labelledby="cap"> <svg role="img" aria-label="Revenue 2021–2024: rose to $5M then plateaued">…</svg> <figcaption id="cap">Quarterly revenue ($M). Peaked Q3 2023, flat since.</figcaption> <details><summary>View as table</summary> <table><caption>Revenue by quarter ($M)</caption>…</table> </details> </figure>
css@media (prefers-reduced-motion: reduce) { .chart * { transition: none !important; animation: none !important; } /* render final state instantly */ }
A dashboard is a system of charts. The hard part is hierarchy, not the individual charts.
Interaction reveals detail without crowding the default view (Shneiderman's "details on demand").
Other measured skills in the registry, with their headline benchmark lift.