Design System

Data visualization

Charts as instruments, not decoration.

When to use a chart

A chart earns its space when:

  1. The shape of the data matters more than the exact values. Trends, distributions, correlations.
  2. Comparison across 3+ values or 2+ series is the point. Two values is a delta, not a comparison.
  3. Time trend is load-bearing - “is this going up or down” is the operator question.

A chart is the wrong answer when:

  1. The operator needs one exact number. Use a large-type readout: H2 numeral + label + unit.
  2. The operator needs to compare two values. Use a delta readout (+12% or -3 in semantic color).
  3. The operator needs to see the structure of a list. Use a table.
  4. The data has fewer than 3 points. A chart of 2 bars is a waste.

The default is not “chart.” The default is the simplest artifact that answers the question. A chart is a commitment.

Permitted chart types

Type Use Primitive
Line Time series, continuous values wa-chart
Area Time series with magnitude emphasis, stacked categories wa-chart
Bar (horizontal, vertical) Categorical comparison, counts wa-chart
Radar Multi-dimensional comparison (4–8 axes), doctrinal fit profiles wa-radar-chart
Scatter Two-variable correlation, outlier detection wa-chart
Heatmap Dense 2D data - sector risk, time-of-day engagement rates Custom composition
Sparkline Inline trend, paired with a readout - no axes, no labels Custom composition

Not permitted

  • Pie charts - low information density, bad for comparison. Use a horizontal bar or stacked area.
  • Donut charts - same problem as pie, with worse center waste.
  • 3D charts of any kind - distortion from perspective, zero analytical advantage, unmistakably consumer-dashboard aesthetic.
  • Treemaps - unless the hierarchy genuinely needs rendering. Rare in tactical contexts.
  • Gauge / speedometer charts - imply a “redline” without being useful at reading it. Use a progress bar or a horizontal threshold line.

Anatomy

Every chart carries up to six elements, each with a defined type treatment:

  1. Title - Inter 600 14px, fog-100, left-aligned above the plot. One line.
  2. Subtitle / unit - Inter 400 12px, fog-200. Time range, unit of measure, data source. Optional but encouraged.
  3. Y axis - label rotated 90°, JetBrains Mono 10px, steel-300. Tick marks only at major gridlines. Unit goes in the subtitle, not on every tick.
  4. X axis - JetBrains Mono 10px, steel-300. Time labels format HH:MMZ (24h Zulu for timelines spanning <24h) or YYYY-MM-DD for longer spans.
  5. Gridlines - horizontal only, steel-500 at 30% opacity, 0.5px. No vertical gridlines. Gridlines whisper - they are orientation, not decoration.
  6. Data series - see color rules below.

Color rules

  • Single series: electric-500.
  • Categorical series with semantic meaning: use the semantic palette directly. Friendly track count → electric-500; hostile → hostile; neutral → neutral; caution state → caution. The chart’s legend doubles as a classification key - the operator already knows what red means in a tactical context.
  • Multi-series without semantic meaning: assign in order electric-500, electric-300, fog-200, steel-400. Beyond four, add distinguishing shape - dashed line, dotted line, different marker - rather than inventing more colors. Four distinct colors is the operator-reliable ceiling.
  • Emphasis line (threshold, benchmark, target): caution amber, 1px dashed, labeled in caution at the right edge of the line with Inter 500 UC tracked 10px.
  • Selected point or highlighted range: electric-500 fill with --fdt-glow-sm. The selection is the one thing that lights up; everything else dims to 60% opacity.

Color is reinforcement, not semantics. Every chart passes color-blind review. Series are distinguished by shape, position, or pattern first; color reinforces the reading. A chart that relies on red-vs-green as its only signal fails.

Typography in charts

Element Font Size Color
Title Inter 600 14px fog-100
Subtitle Inter 400 12px fog-200
Axis tick labels JetBrains Mono 400 10px steel-300
Axis titles Inter 500 UC tracked 0.08em 10px steel-300
Legend labels Inter 500 11px fog-200
Data value callouts JetBrains Mono 500 11px fog-100
Annotations Inter 400 italic 11px steel-300

Numeric values are always JetBrains Mono with tabular figures - the axis needs the 9 and the 4 to land on the same baseline regardless of which digit sits above them.

Animation

  • Entrance animation on first render: permitted. 250ms ease-out, draw from origin outward. No stagger across series, no bounce, no spring.
  • Update animation on data change: permitted. 150ms linear interpolation between old and new values. No morph dramatics.
  • Hover tooltips: instant, no delay. The operator is asking a question; don’t make them wait.
  • Never animate decoratively. No “reveal when scrolled into view,” no counter-ups on numeric readouts, no background particles, no gradient flows. A chart is not a loading screen.

Under prefers-reduced-motion: reduce, entrance animations are disabled; charts render in their final state immediately.

Empty and error states

Charts follow the same state conventions as System states:

  • No data - centered steel-300 inbox icon, NO DATA uppercase label in Barlow Condensed, one-line description, primary action if applicable (adjust filters, change scope). The plot area renders with gridlines faded to 15% opacity so the empty frame is still recognizable as a chart, not broken UI.
  • Data error / fetch failure - octagon-exclamation in hostile-400, error code in JetBrains Mono, title + description, retry action. Plot area empty.
  • Loading - see Loading & progress states. The plot renders a shimmer skeleton matching the chart’s grid.

Sparklines

Sparklines accompany readouts - they live inline, not in isolation. A sparkline is the shape of the number next to it.

  • Height: 24px, matching single-line readout height
  • Width: 60–120px depending on container
  • No axes, no gridlines, no labels - if any of those are needed, it’s a chart, not a sparkline
  • Color: electric-500 by default; hostile-400 if the most recent value crosses a downward threshold; neutral if crossing an upward target
  • Paired readout provides the exact number; the sparkline provides the shape. They’re one composed element, not two adjacent ones.

Example composition:

ENGAGEMENT RATE    84%  ↗   [sparkline]   past 24h

The uppercase label is Barlow Condensed 700 10px tracked. The number is Inter 700 32px. The arrow is the delta indicator. The sparkline is 80px wide. The suffix is Inter 400 11px steel-300.

Dashboards

Dashboards compose multiple charts + readouts on one canvas. Rules:

  • Reading order top-left to bottom-right, aligned to a 12-column grid.
  • Readouts above charts - the summary number precedes its trend visualization. The operator reads the answer, then the shape that supports it.
  • Maximum 8 charts per dashboard view. Beyond that, operators stop reading and start skimming. Split into tabs or drill-downs, not more rows.
  • Classification strip at top and bottom of the dashboard surface, matching product-level rules.
  • Single LAST UPDATED HH:MMZ timestamp in the top-right of the dashboard frame - not per chart. One clock for the whole surface.
  • Refresh rhythm indicated below the timestamp if data auto-refreshes: UPDATES · 30S in Barlow Condensed uppercase tracked.

Tactical vs analytical register

FDT renders two classes of chart with different visual weight:

  • Tactical charts - in-product, live data, mission-critical. Dense, quiet, minimal padding, JetBrains Mono labels dominant, electric-500 primary, minimal annotation. The operator glances. Example: a GHOST GRID resource-burn readout.
  • Analytical charts - briefings, reports, decks, program reviews. More padding, slightly larger type, annotations welcome, semantic color usage permitted on multi-series, gridlines slightly more present. The reader dwells. Example: quarterly engagement outcomes in a program review slide.

Both render the same underlying data. The difference is the pace at which the reader consumes the surface. Tactical = glance; analytical = dwell. Don’t mix - a tactical dashboard with analytical chart annotations is overdecorated and slow; an analytical briefing with tactical charts feels sparse and under-informative.