Charts

Area, bar, ring and state charts for sensor data that arrives at irregular intervals.

Live React portVue source: WildLineChart.vueStorybook story: Charts/Area ChartReact component: WAreaChart, WBarChartDc, WDonut, WStateTimeline

Try it

Change the range and the state, and hover the plot to read a reading.

Range
State
Answer
Band
<WAreaChart
  series={[{ data: soil30, name: '30 cm', color: SERIES_RAMP[0] }]}
  times={times}
  domain={[thu24, now]}
  yMin={10} yMax={40} unit="%"
  band={[22, 34]} bandLabel="Refill to full"
  thresholds={[{ value: 22, label: 'Refill 22%', short: '22', tone: 'warn' }]}
  answer={{
    value: latest, unit: '%', label: 'Now',
    delta: { value: change, period: 'vs 24 h ago', unit: 'pts' },
    verdict: { tone: 'ok', label: 'Above refill' },
  }}
  ariaLabel="Soil moisture at 30 cm, Block 4"
/>

When to use

  • Use an area or line chart for a level over time, such as soil moisture or tank level.
  • Use a bar chart for an amount per period, such as daily rainfall.
  • Use a state timeline for when something ran, such as irrigation zones.
  • Use stacked panels for measures in different units that share a time window.
Don’t use it
  • Never two y-axes: use stacked panels.
  • For one current reading. Use a Gauge.
  • For exact values to compare or export. Use a Table.

Series colours

Assign series in this order, and never skip a stop. Green, amber and red mean status, so keep them for verdicts and thresholds.

1 · #1877F2blue-1000
2 · #C33394magenta-700
3 · #6E23C9violet-800
4 · #0692BCteal-700
5 · #7B238Dpurple-900
Name two or more series in a Legend. A lone series needs none, because the card title names it.

Answer first

Lead with the latest reading, its change over a named period, and a verdict. The band and threshold are labelled in the right gutter, clear of the data.

<WAreaChart
  series={[{ data: soil30, name: '30 cm', color: SERIES_RAMP[0] }]}
  times={times}
  domain={[thu24, now]}
  yMin={10} yMax={40} unit="%"
  band={[22, 34]} bandLabel="Refill to full"
  thresholds={[{ value: 22, label: 'Refill 22%', short: '22', tone: 'warn' }]}
  answer={{
    value: latest, unit: '%', label: 'Now',
    delta: { value: change, period: 'vs 24 h ago', unit: 'pts' },
    verdict: { tone: 'ok', label: 'Above refill' },
  }}
  ariaLabel="Soil moisture at 30 cm, Block 4"
/>
The change arrow stays neutral, because down isn’t bad for a soil probe. Only the verdict chip carries status, with an icon and a word.

Several series

Draw soil moisture by depth as one chart, a line per probe, shallowest first.

10 cm30 cm60 cmLast 7 days
<Legend
  items={[{ label: '10 cm' }, { label: '30 cm' }, { label: '60 cm' }]}
  period="Last 7 days"
/>
<WLineChart
  series={[
    { name: '10 cm', data: soil10, times: t10 },
    { name: '30 cm', data: soil30, times: t30 },
    { name: '60 cm', data: soil60, times: t60 },
  ]}
  domain={[thu24, now]}
  minValue={10} maxValue={40} unit="%"
  height={260}
  endLabels
/>
No fill, so no line hides under another. Each line ends in its latest value, and the legend names the depths.

Gaps

Pass times, so readings sit where they happened. A missing reading breaks the line and reads n/a, never 0.

<WAreaChart
  series={[{ data: soil10, name: '10 cm', color: SERIES_RAMP[0] }]}
  times={times}       // null = a missed reading
  domain={[thu24, now]}
  yMin={0} yMax={40} unit="%"
/>
The grey band fills the plot from top to bottom and names the outage. Hover a missed reading to read n/a.

States

Set state so the chart says what it knows. A stale chart keeps its data, greyed, and says when it last heard.

loading
stale
empty
<WAreaChart … state="loading" />
<WAreaChart … state="stale" />
<WAreaChart … state="empty" />
Refetching holds the old data at 45% with a chip, so the plot never flashes a skeleton.

Bar

The scale starts at zero, so set only yMax. Lead with the total, because that’s what anyone asks of rain.

<WBarChartDc
  data={rain}          // null = the gauge missed that day
  times={days}
  yMax={12} unit="mm"
  state="partial"
  answer={{
    value: total, unit: 'mm',
    delta: { value: total - previous, period: 'vs previous 14 days' },
    verdict: { tone: 'idle', label: '1 day missing' },
  }}
/>
A missed day is an n/a column, not a 0 bar. Today’s bar is lighter and labelled so far.

Stacked panels

Give each measure its own panel on one time axis. One crosshair runs through every panel, and one readout lists them all.

Bore 2 flow fell from 2.1 to 1.4 L/s on Mon 28, so the pump ran 4.2 h on Tue, up from about 2.7 h a day.

Tank 3 · level %
Bore 2 · flow L/s
Pump runtime · h/day
2.7
2.6
2.6
2.7
3.0
4.2
<WChartStack
  domain={[thu24, wed30]}
  summary="Bore 2 flow fell from 2.1 to 1.4 L/s on Mon 28…"
  panels={[
    { key: 'tank', title: 'Tank 3 · level %', height: 130, chart: <WLineChart times={t} series={[{ name: 'Tank 3 level', data: level }]} unit="%" minValue={0} maxValue={100} thresholds={[{ value: 30, label: 'Pump on 30%', short: 'On', tone: 'neutral' }]} /> },
    { key: 'flow', title: 'Bore 2 · flow L/s', height: 80, chart: <WAreaChart times={t} series={[{ data: flow, name: 'Bore 2 flow', color: SERIES_RAMP[0] }]} yMax={2.5} unit="L/s" fill="none" minmax={false} /> },
    { key: 'runtime', title: 'Pump runtime · h/day', label: 'Pump runtime', height: 110, chart: <WBarChartDc times={days} data={hours} yMax={6} unit="h" /> },
  ]}
/>
Say what changed in the summary. Title each panel with its measure and unit.

State timeline

Give each zone its own lane. A state has no y value, so don’t plot it as a line.

RunningQueuedFaultToday
<WStateTimeline
  lanes={[
    { name: 'Zone 1', runs: [{ from: .04, to: .19, state: 'run' }] },
    { name: 'Zone 2', runs: [{ from: .30, to: .34, state: 'fault' }] },
    …
  ]}
  xLabels={['12 am', '6 am', '12 pm', '6 pm', '12 am']}
  now={0.47}
/>
A bare track is idle, and every other state has a fixed colour. Show a legend, and name any fault in text too.

Accessibility

Pass ariaLabel with a one-sentence summary; the state is added to it. Line charts and stacks are one tab stop, and a screen reader hears each reading as the arrow keys move.

KeyWhat it does
TabMoves focus to the plot.
← / →Steps to the previous or next reading. In a stack, Shift moves ten.
Home / EndJumps to the first or last reading.
EscapeHides the readout.

Props

Every engine takes times, domain, state, answer and thresholds. A reading is a number, or null for none.

WAreaChart

PropTypeDefaultDescription
seriesAreaSeries[]—Each: data, color, name, times?, raw?, dashed?, fillScale?.
times / domainnumber[] / [from, to]—Reading times and the window, in ms.
yMin / yMaxnumber0 / —The scale. Fix it per kind of reading.
unit / fmtstring / (v) => string—After each value, and how values round.
fill'flat' | 'none''flat'A flat 10% fill under the first series, or none.
band / bandLabel[lo, hi] / string—A range band, named in the gutter.
bandTone'neutral' | 'blue' | 'ok''neutral'Band tint. 'ok' only when it means healthy.
thresholdsThreshold[]—Dashed rules: value, label, short?, tone?.
endLabelsbooleanfalseLatest value per series in the gutter.
answerAnswerRowProps—Latest value, delta and verdict above the plot.
stateChartState'ready'ready, loading, refetching, empty, stale or partial.
stateMessage / stateDetailReactNode—The loading or empty message.
staleSince / partialFromnumber—Where the stale or partial tail starts.
gapMsnumber2.5 × spacingReadings further apart break the line.
xTicks / timeZoneXTick[] / stringauto / 'UTC'Explicit ticks, and the zone for labels.
ariaLabelstring—One-sentence summary that names the chart.

AnswerRowProps

PropTypeDefaultDescription
value / unitnumber | null / string—The latest reading. null prints n/a.
labelReactNode—When it was read: 'Now', '4:35 am'.
delta{ value, period, unit? }—The change against a named period, in neutral ink.
verdict{ tone, label, icon? }—The status chip. Always a word.
mutedboolean—Greys the value when it is stale.

WBarChartDc

PropTypeDefaultDescription
data / yMaxReading[] / number—A value per bucket; null is an n/a column.
times / bucketMsnumber[] / number— / a dayBucket starts, and each bucket’s length.
unit / fmtstring / (v) => string—After each value, and how values round.
partialLast / partialLabelboolean / string— / 'so far'The last bucket is unfinished.
color / colorLaststring—Bars, and the latest bar. Ramp tokens only.

WChartStack

PropTypeDefaultDescription
panelsStackPanel[]—Each: key, title, height, chart; label? and fmt?.
domain[from, to]—The shared window, in ms.
summaryReactNode—What changed, above the panels. Also its name.
gutters / gap{ left, right } / number40, 96 / 14Shared gutter widths, and space between panels.
readoutbooleantrueOne readout listing every panel.

WStateTimeline

PropTypeDefaultDescription
lanesTimelineLane[]—Each: a name, and runs as 0–1 fractions.
runs[].state'run' | 'queue' | 'idle' | 'fault'—Sets the run’s colour. Idle is the bare track.
xLabels / nowstring[] / number— / 0.47Axis labels, and the now marker.

Tokens

TokenValueWhere it lands
SERIES_RAMP#1877F2Series, in order; blue-1000 first.
SERIES_LONE#00AEEFBrand cyan, blue-900: a lone default series.
SERIES_REFERENCE#7F7D83Reference line, solid 1.5px, grey-900.
BAND_FILL.neutral#F1F1F1Range band, grey-300. The default.
BAND_FILL.blue#EBFAFFA water range band, blue-100.
BAND_EDGE.neutral#ADACB0Band bracket in the gutter, grey-800.
THRESH_LINE.warn#EC980CWarning threshold rule, squash-800.
THRESH_LINE.bad#E12121Critical threshold rule, red-800.
THRESH_LINE.neutral#4F4D55Set-point rule, such as pump on, grey-1000.
GAP_FILL#F5F5F5Gaps and stale tails, grey-200.
PARTIAL_FILL#EBFAFFThe unfinished period, blue-100.
SKELETON_FILL#F1F1F1Loading pills, grey-300.
TONE.*.surface#E1FAEAVerdict chip ground, by tone.
T.grid#F1F1F1Gridlines, grey-300.
T.baseline#E6E6E6The baseline under each plot, grey-500.
T.ink3#4F4D55Ticks, labels and the change arrow.
TIMELINE_TONE#00AEEFRunning blue-900, queued squash-800, fault red-800.
RAMP.grey[1200]#1D1C20Readout ground.