High-level guidance, architecture, and conventions for data visualisation components in the Design System.
SPC users: See the SPC SQL v2.6 ↔ TypeScript parity plan here: spc-sql-parity.mdx
- Consistent visual language (tokens, typography, spacing)
- Accessibility-first (keyboard, screen reader, colour contrast)
- Composability (low-level primitives and ergonomic chart wrappers)
- Predictable scaling & layout (deterministic axes, margins, responsive behaviour)
- Progressive enhancement (core information available without interaction; richer context via tooltips / legends / focus states)
- Extensibility (easy to introduce new series / chart types without rewriting core infrastructure)
| Layer | Purpose | Examples |
|---|---|---|
| Context Foundations | Provide shared measurement and scaling and state | ChartRoot, ScaleContext |
| Data / Visibility State | Track series presence & hidden state | VisibilityContext |
| Interaction State | Pointer focus, keyboard navigation, nearest-point logic | TooltipContext |
| Primitives | Low-level render units | Axis, GridLines, LineSeriesPrimitive, Legend, TooltipOverlay |
| Composition | Assemble primitives declaratively | (Custom composition stories) |
| High-level Charts | Opinionated convenience wrappers | LineChart |
Calculates and exposes chart dimensions, inner plot area, margins, and a coordination surface for layered elements (e.g. overlay layers).
Provides xScale, yScale, tick calculation, and scale-driven formatting utilities. Supports overrides (explicit tick values, formatting functions) while guarding against label collisions.
Maintains a set of hidden series IDs; primitives & legends subscribe to reflect toggled states. Behaviour configurable via visibilityMode (see Visibility & Domain Behaviour).
Central state for the currently "focused" datum (via pointer hover, keyboard nav, or programmatic focus). Supplies navigation helpers:
focusNextPoint/focusPrevPointfocusNextSeries/focusPrevSeriesfocusNearest(xPx, yPx)for pointer-based snapping
| Category | Component | Notes |
|---|---|---|
| High-level | LineChart |
Aggregates axes, grid, series, legend, tooltip overlay; exposes convenience props (e.g. tick alignment) |
| Primitive | LineSeriesPrimitive |
Renders path and focusable points; registers with tooltip & visibility contexts |
| Primitive | Axis |
Supports collision strategies (wrap, rotate, truncate), explicit tick values, formatting hooks |
| Primitive | GridLines |
Visual reference lines (tied to scale ticks) |
| Interaction | Legend |
Toggles visibility (auto-wires to VisibilityContext) |
| Interaction | TooltipOverlay |
Renders focused datum indicator / label (a11y enhancements in progress) |
| Infrastructure | PointerEventsLayer (story usage) |
Captures pointer events over full plot area; delegates to focusNearest |
Three modes influence tick -> point relationship (precedence highest first):
- Explicit
xTickValues - Data-aligned
alignXTicksToData - Automatic "nice" ticks (default)
See detailed guide: LineChart X‑Axis Tick Alignment
Opt-in keyboard controls (keyboardNav prop on LineChart, or custom handlers using TooltipContext):
- Arrow Left / Right: Previous / next point in the current series.
- Arrow Up / Down: Previous / next series (retains index, clamps if shorter). Hidden series are skipped.
- Home: Jump to first point in the current series (or first visible series if none focused yet).
- End: Jump to last point in the current series (or first visible series if none focused yet).
- Escape: (Future) Clear focused point.
To enable built-in navigation, pass keyboardNav. This renders a focusable wrapper with an onKeyDown handler. If you need custom semantics, omit the prop and implement your own onKeyDown using the context methods.
Optional wrap-around behaviour (disabled by default) can be enabled via wrapAroundNav on LineChart (or wrapAround prop on TooltipProvider). When enabled, attempting to move past the first/last point or series wraps to the opposite end within that navigation axis.
Pointer movement snaps to the nearest point (Euclidean distance) within a pixel threshold (default 40px) ensuring predictable tooltip activation without requiring pixel-perfect aiming.
| Aspect | Current | Planned Enhancements |
|---|---|---|
| Focus Indicators | Visible highlight circle and tooltip overlay | High-contrast outline & motion reduction variant |
| Screen Reader Output | (WIP) Basic aria-label on points | Live region announcements on focus and summary description |
| Colour Contrast | Palette derived from tokens with contrast validation | Automated audit in CI |
| Keyboard Navigation | Horizontal and vertical movement across points/series | Home/End, Page Up/Down semantics; skip hidden series |
| Hidden Series | Removed from DOM | Optional faded style with aria-hidden |
Guidance: Always accompany a chart with a textual summary (key trend / anomaly) for users who cannot or do not consume visual detail.
- O(N * S) point registration where N = points per series, S = series count (acceptable for typical dashboard scales < ~10k total points). Future optimisation: spatial index (Kd-tree) for nearest lookup.
- Tick collision resolution avoids layout thrash by precomputing candidate labels before commit.
- Avoid rendering thousands of focusable point nodes; a density threshold may pivot to canvas or clustered points in future roadmap.
- Introduce new series primitives (e.g.
BarSeriesPrimitive) by:- Registering their data with
TooltipContext(if focusable). - Respecting
VisibilityContexthidden IDs. - Using shared scale context for coordinates.
- Registering their data with
- Use composition first: build custom chart variants in stories via primitives before codifying a new high-level component.
- Prop Naming: prefer
xTickValues,yDomain,series,alignXTicksToDatastyle — explicit & domain-oriented. - Keep visual-only elements (e.g. backgrounds) out of accessibility tree (
aria-hidden="true").
LineChart props:
| Prop | Type | Default | Description |
|---|---|---|---|
visibilityMode |
'remove' | 'fade' |
remove |
Hidden series are either removed entirely or rendered faded (opacity 0.25, aria-hidden, points unfocusable). |
recomputeYDomainOnHidden |
boolean |
false |
Recomputes y-scale using only currently visible series values. |
Faded mode preserves trend context while removing interactive / accessibility surface.
- Accessible live region for aggregated multi-series tooltip content
- Heuristic tick alignment auto mode (switch to data-aligned below threshold)
- Bar / Area / Scatter primitives
- Canvas or hybrid renderer for large datasets
- Theming hooks for data viz colour scales beyond current palette (diverging/sequential)
- Improved responsive label strategies (multi-line wrapping heuristics, ellipsis tooltips)
<LineChart
series={series}
yLabel="Value"
alignXTicksToData
// or xTickValues={[...explicitDates]}
/> - X‑Axis Tick Alignment
- (Coming soon) Accessibility implementation notes
- Colour scale guidelines (sequential & diverging) below
In addition to the categorical and region palettes, utilities are now provided for continuous (metric) encodings:
Use when values progress monotonically (e.g. rate, count, single‑direction magnitude). Create a scale:
import { createSequentialColorScale } from '../../components/DataVisualisation/utils/colors';
const scale = createSequentialColorScale({
domain: [minValue, maxValue],
// Optional custom anchor colours (light -> dark). If omitted a lightness ramp is generated from NHS blue.
colors: ['#e5f1fa', '#005eb8']
});
const fill = scale(value); // returns hexGuidance:
- Provide 2–5 anchor colours to emphasise critical thresholds or perceptually uniform transitions.
- Ensure sufficient contrast for overlays (consider adding outline on very light swatches).
- Avoid using more than one sequential palette in the same small visual unless clearly separated.
Use when values deviate around a meaningful midpoint (e.g. zero, average, target). Create a scale:
import { createDivergingColorScale } from '../../components/DataVisualisation/utils/colors';
const scale = createDivergingColorScale({
domain: [-maxMagnitude, 0, maxMagnitude],
colors: ['#d5281b', '#f2f2f2', '#007f3b'] // negative, neutral, positive
});
const fill = scale(delta);If colours are omitted, a ramp is auto‑generated between NHS red → neutral → green. Provide explicit anchors where colour meaning is semantic (e.g. deficit vs surplus).
Both factories clamp by default – values outside the domain map to the nearest end colour. Disable clamping via clamp: false if you want extrapolation behaviour (not usually recommended for legend alignment).
- Don’t rely on colour hue alone for thresholds – pair with labels, markers, or patterned overlays for critical boundaries.
- For diverging scales choose a sufficiently light neutral to keep both sides distinguishable for users with colour vision deficiencies.
- Test palettes in simulated protanopia/deuteranopia; avoid red/green pairs with similar lightness – consider adding a secondary encoding (shape, underline) when unavoidable.
- Tokenisation of commonly used sequential ramps (e.g.
color.data-viz.sequential.blue.*). - Perceptual uniformity audit (ΔE between equal domain steps) & optional logarithmic mapping helper.
- Discrete stepped scale generator for choropleth legends.
Feedback or proposals: open a discussion or issue with the label data-viz.