Plank help · updated 2026-10-29

Charts: the method

How to make a chart that is right by construction: pick the form from the job, give each colour one job, run the colour-blind validator baked at /opt/plank/design, mark sizes, hover, accessibility, and render it before you deliver.

Agents: fetch the raw markdown of this page at /ru/help/charts.md

Charts: the method

A chart in a Plank deliverable is read by a person who did not make it, often on a phone, often in the dark theme, sometimes printed in black and white, and sometimes by someone who cannot tell red from green. This page is the procedure that makes a chart work for all of them by construction — every step has a check, so nothing here is a matter of taste.

Charts for dashboards is the companion: copy-ready components (KPI tile, bars, donut, line, gauge, funnel…) that already follow this method. Use them when one fits; use this page when you draw something they do not cover, change their colours, or need to judge whether a chart is right.

Colour comes last. Most bad charts start by choosing colours.

1. Pick the form from the job

Ask what the reader must take away, then pick the form that shows exactly that.

The reader needsFormNot
One number and whether it is gooda stat tile: the number, its unit, the comparison ("+4.2% vs September"), maybe a sparklinea chart with one bar
Exact values to look up or copya tablea chart with a number on every bar
Compare amounts across categorieshorizontal bars, sorted, from a zero baselinea pie, a 3-D anything
Change over timea line (an area only for one series)bars for 30+ periods
Parts of one total, ≤ 5 partsa proportion bar; a donut when the total sits in the middlea pie with 9 slices
Parts of a total across categoriesstacked bars — at most 4 segmentsstacked area of 8 series
How a start value became an end valuea waterfalltwo bars and a paragraph
Above / below a reference (budget, plan, last year)diverging bars from the reference linetwo overlaid colours
Two measures of different scaletwo charts, side by side, same x-axis — or index both to 100a second y-axis, ever
Order, handoff, what contains whata diagram — see Diagramsa chart

If two forms fit, take the one with fewer marks. If the answer is a sentence ("revenue grew every month"), write the sentence and put the chart under it as evidence, not instead of it.

More than six series is never a seventh colour. Fold the tail into "Other", split into small multiples (one small chart per series, shared scale), or highlight the one that matters in --c1 and draw the rest in --c-muted.

2. Give each colour one job

There are four jobs. Decide which one each colour on the chart is doing before picking any value.

  • Identity (which series is this?) — the categorical slots --c1 … --c6, assigned in order and by entity: "Almaty" is --c2 on every chart in the file, and stays --c2 when a filter hides "Astana". Never pick a colour per chart, never cycle back to --c1 for a seventh series.
  • Magnitude (how much?) — one hue, light to dark: --seq-1 … --seq-5. Heatmaps, choropleths, a value-shaded table. Never a rainbow.
  • Polarity (above or below a reference?) — two hues around a neutral grey: --div-cool-3 … --div-mid … --div-warm-3. The midpoint is grey, never a colour.
  • State (is it OK?) — --st-good, --st-warn, --st-bad, --st-neutral. Reserved: never "series 4". Always with an icon and a word (Icons has circle-check, triangle-alert, circle-x).

Text never wears a series colour. Values, labels and legend text stay in the page's ink tokens; the coloured swatch or line beside them carries the identity. Coloured text fails contrast for half the palette and turns a legend into a rainbow.

Plank's palette

Validated in both modes against the chart surface (#ffffff light, #16161d dark) and the page grounds. Slot 1 is Plank blue. The dark values are their own steps, chosen and checked against the dark surface — not an automatic inversion of the light ones.

TokenHueLightDark
--c1blue#4f6df5#4b8eff
--c2ochre#c2701f#c08a1f
--c3teal#0e9f9f#028f96
--c4green#4c7a2e#30ae40
--c5violet#9b5de5#9b5cd7
--c6rose#c2456b#de5077

Paste these three rules into the page's <style> (they are /opt/plank/design/palette.css; copy the text — a deliverable cannot link a file):

:root{--chart-surface:#ffffff;--c1:#4f6df5;--c2:#c2701f;--c3:#0e9f9f;--c4:#4c7a2e;--c5:#9b5de5;--c6:#c2456b;--c-muted:#b4b6be;--seq-1:#a2b0d8;--seq-2:#7c91cf;--seq-3:#5971c4;--seq-4:#3950b7;--seq-5:#2028aa;--div-cool-1:#92adff;--div-cool-2:#6a84d8;--div-cool-3:#465cad;--div-mid:#d6d7da;--div-warm-1:#ed9b56;--div-warm-2:#c2732a;--div-warm-3:#934f01;--st-good:#1f7a45;--st-warn:#a35a00;--st-bad:#c0392b;--st-neutral:#6b7080}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--chart-surface:#16161d;--c1:#4b8eff;--c2:#c08a1f;--c3:#028f96;--c4:#30ae40;--c5:#9b5cd7;--c6:#de5077;--c-muted:#4a4b55;--seq-1:#3f4b71;--seq-2:#55669c;--seq-3:#6b82ca;--seq-4:#839ffa;--seq-5:#adc2fe;--div-cool-1:#405395;--div-cool-2:#6379bf;--div-cool-3:#89a1ea;--div-mid:#414246;--div-warm-1:#834602;--div-warm-2:#ad6c33;--div-warm-3:#d8935b;--st-good:#4cc38a;--st-warn:#f0a93a;--st-bad:#ff6b6b;--st-neutral:#9a9aa6}}
:root[data-theme="dark"]{--chart-surface:#16161d;--c1:#4b8eff;--c2:#c08a1f;--c3:#028f96;--c4:#30ae40;--c5:#9b5cd7;--c6:#de5077;--c-muted:#4a4b55;--seq-1:#3f4b71;--seq-2:#55669c;--seq-3:#6b82ca;--seq-4:#839ffa;--seq-5:#adc2fe;--div-cool-1:#405395;--div-cool-2:#6379bf;--div-cool-3:#89a1ea;--div-mid:#414246;--div-warm-1:#834602;--div-warm-2:#ad6c33;--div-warm-3:#d8935b;--st-good:#4cc38a;--st-warn:#f0a93a;--st-bad:#ff6b6b;--st-neutral:#9a9aa6}

The first rule is light; the second follows the OS dark setting unless the page was switched to light; the third follows the theme the Plank app pushes to the file viewer as data-theme. Define the rest of the page's colours the same three-state way — HTML pages shows the pattern.

3. Run the validator — do not judge colours by eye

Whether two colours stay apart for a colour-blind reader is arithmetic, not taste. The image has the tool:

python3 -I /opt/plank/design/validate_palette.py --plank        # Plank's palette, both modes
python3 -I /opt/plank/design/validate_palette.py --mode light --surface "#ffffff" "#4f6df5,#c2701f,#0e9f9f"
python3 -I /opt/plank/design/validate_palette.py --mode dark --surface "#16161d" "#4b8eff,#c08a1f,#028f96"
python3 -I /opt/plank/design/validate_palette.py --pairs all --mode light "<hex,…>"   # dots, lines, map regions
python3 -I /opt/plank/design/validate_palette.py --ordinal --mode light "<ramp, light→dark>"

Run it whenever you use any colours other than the tokens above unchanged — a client's brand colours, a seventh slot, a ramp of your own — and run it once per mode with that mode's surface. What it checks, for each list:

CheckPassWhy
Lightness bandeach colour in a mid band (light ≈ 0.43–0.77, dark ≈ 0.48–0.67 OKLab L)too pale vanishes on the surface, too dark reads as text
Chroma flooreach colour visibly coloureda grey slot is indistinguishable from the axes
Colour-blind separationneighbours ≥ 8 apart (OKLab ×100) under protan and deutan simulation; 6–8 is a WARNthe most common colour blindness merges red/green pairs
Normal-vision floorneighbours ≥ 15 aparttwo "different" colours anybody would confuse
Contrasteach colour ≥ 3:1 against the surface; below is a WARNa mark the eye cannot find
Tritanreported, not gatedrare; noted so you can add a second channel if it is low

--pairs all checks every pair instead of neighbours: use it when marks of any two series can touch (scatter points, crossing lines, map regions). --ordinal checks a magnitude ramp instead: one hue, lightness strictly ordered, even steps, the light end still visible.

Exit code: 0 no FAIL, 1 a FAIL, 2 bad input. A FAIL is fixed before you go on — move the colour's lightness or swap it for another hue, and re-run. A WARN is legal only with its relief: visible direct labels, a texture on one series, or a table view of the same numbers. A normal-vision FAIL has no relief — the pair is too close for anyone.

4. Draw the marks

Thin, quiet marks let the data be the loudest thing on the page.

The short way, inside Plank: python3 -I /opt/plank/design/chart.py bar|hbar|line|donut|kpi --data '{"labels":[…],"values":[…]}' --title "The finding" --unit "млн ₸" (or --csv FILE; --size slide|page|phone) prints an inline SVG that already follows this section and the next: palette tokens only, so it switches theme by itself; bars from zero with the data end rounded; values where they fit; a legend from two series; 1 240 000 and 4,2 number format; a <title> and a <desc> that reads the figures out. It passes plank_deck_qa.py's diagram checks. Hover and the phone-width redraw below are still yours when the page is interactive.

No figures given? Never invent them. chart.py <kind> --example --title "The claim" (--data becomes optional) draws the same chart on plausible example data with a visible badge «пример — замените своими данными» and data-plank-example="1" on the root <svg>. plank_check_canvas lists those boards (WARN example-data); name them in the reply («на слайдах 4 и 6 — пример, замените своими цифрами») and never remove the badge while the numbers are still examples. Every chart's root carries data-plank-visual="chart", which is how the deck check counts a slide as carrying a visual (text-only-deck, Design canvas).

  • Bars: thickness ≈ 60% of the slot, never more than 24px. Round only the end that carries the value (radius 4px); the end on the baseline stays square. Bars start at zero, always.
  • Gaps: 2px of surface colour between adjacent filled shapes — stacked segments, neighbouring bars, donut slices. Draw it as a stroke in var(--chart-surface), not as transparent space. Overlapping dots get a 2px ring in the same colour.
  • Lines: 2px, round joins. Markers, when used, ≥ 8px across. An area under a single line is a ~10% wash of the line's colour.
  • Grid and axes: recessive. Solid 1px hairlines in a grid token, horizontal only for bars and lines; no chart border, no tick on every gridline, no axis line where the grid already shows zero. Axis labels in the muted ink token, 11–12px, numbers in tabular figures (font-variant-numeric: tabular-nums).
  • Phone width: an SVG scales its text with the drawing, so a 640-wide viewBox on a 340px phone turns 11px axis labels into 6px ones. Draw the chart at the width it actually has — read the container's clientWidth and build the SVG for it, again on resize (the hover script already runs, so this costs nothing) — and drop to fewer ticks or a horizontal bar layout when narrow. A table beside a chart scrolls inside its own overflow-x:auto box, never the page.
  • Labels: selective. Label the last point of each line, the largest bar, the one value the heading talks about — not every mark. A number on every bar is a table drawn badly.
  • Units and formats: Kazakh/Russian readers expect a space as the thousands separator and a comma for decimals (12 450,5), and the currency after the number (1,2 млн ₸). Use Intl.NumberFormat('ru-RU') and put the unit in the axis title or the heading once, not on every tick.
  • Heading = the finding. "Payroll is 54% of September spend", not "Spend by category".

5. Make every mark answer on hover

An HTML chart is interactive by default. Every plotted mark carries its exact value, label and unit in a tooltip — one shared tooltip element, one listener, data-tip on each mark (the dashboard skeleton has it). Lines and areas get a vertical crosshair that snaps to the nearest x and lists every series at that x. Make the hover target larger than the mark: an invisible full-height rect per bar slot, an invisible 12px-wide stroke over a 2px line. On a phone a tap shows the tooltip; nothing important may live only in a hover.

Filters (period, branch, category) sit in one row above the charts they affect, and changing one never repaints the surviving series in new colours — identity follows the entity (step 2).

Only a bare stat tile with no plot skips this step.

6. Accessibility pass

  • Never colour alone. ≥ 2 series → a legend, always; ≤ 4 series → also a direct label at the end of each line or on each segment. One series → no legend; the heading names it.
  • A table view. Every chart offers the same numbers as a table — a <details> under it is enough. It is also what a screen reader reads: give the <svg> role="img" and an aria-label stating the finding, and put the table in the page.
  • Dark mode is its own design. Check the chart in both themes, not just the light one; use the dark tokens, never filter: invert().
  • Texture for print. If the deliverable may be printed, or a pair only WARNs, give one series a diagonal hatch (an SVG <pattern> at 45°, 1.5px lines, in the series colour) so it survives greyscale.
  • Reduced motion. Animate nothing that carries data; respect prefers-reduced-motion for any transition you add.

7. Render it and look

The validator checks colour, not layout. Render the page and look at it before you deliver — both themes and a phone width:

chrome-headless-shell --no-sandbox --hide-scrollbars --window-size=1200,900 \
  --screenshot=/tmp/chart-light.png "file://$PWD/report.html"
chrome-headless-shell --no-sandbox --hide-scrollbars --window-size=1200,900 \
  --blink-settings=preferredColorScheme=0 --screenshot=/tmp/chart-dark.png "file://$PWD/report.html"
chrome-headless-shell --no-sandbox --hide-scrollbars --window-size=390,844 \
  --screenshot=/tmp/chart-phone.png "file://$PWD/report.html"

Then read the PNGs. Look for: labels that collide or are clipped, a legend that wraps under the chart, a tooltip that would cover the mark it explains, a bar that does not start at zero, text that disappears in dark mode, a page that scrolls sideways on the phone. A board on a canvas is checked by plank_check_canvas instead (Canvas).

What goes wrong — check every chart against this

  • Two y-axes. Any two lines can be made to "correlate" by choosing the scales. Two charts, or index both to 100.
  • A rainbow ramp for a quantity. Lightness must carry the order; hue cannot.
  • Colours by rank — the biggest series is always blue, so "Almaty" changes colour when the month changes.
  • A seventh generated colour. Fold, split, or highlight instead.
  • Red/green as the only difference between good and bad. Add the icon and the word.
  • Status colours as series colours. Red as "series 3" reads as an alarm.
  • Coloured text for values and legends.
  • A pie with more than five slices, a 3-D chart, a donut used for a single number.
  • Bars from a non-zero baseline. It turns a 3% difference into a 3× one.
  • A number on every mark, and a gridline under each.
  • Thick bars, heavy borders, drop shadows, gradients on data marks.
  • Hover-only information with no table view.
  • The dark theme as an inversion — colours that were never checked against the dark surface.
  • Never rendered. The chart that "should look fine" is the one with the clipped legend.

Non-negotiables

  1. Form from the job; a stat tile or a table when that is the answer.
  2. One y-axis.
  3. Categorical colours in fixed order, by entity, ≤ 6; never a generated seventh.
  4. Magnitude = one hue light→dark; polarity = two hues around grey.
  5. Status colours are reserved and always come with an icon and a word.
  6. Any palette other than the tokens above unchanged goes through validate_palette.py, in both modes, before it ships. A FAIL is fixed; a WARN gets its relief.
  7. Text wears ink tokens, never a series colour.
  8. A legend for ≥ 2 series, a tooltip on every mark, a table view.
  9. Both themes and a phone width, rendered and looked at.