Charts
Area, bar, ring and state charts for sensor data that arrives at irregular intervals.
Try it
Change the range and the state, and hover the plot to read a reading.
<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.
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.
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"
/>Several series
Draw soil moisture by depth as one chart, a line per probe, shallowest first.
<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
/>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="%"
/>States
Set state so the chart says what it knows. A stale chart keeps its data, greyed, and says when it last heard.
<WAreaChart … state="loading" />
<WAreaChart … state="stale" />
<WAreaChart … state="empty" />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' },
}}
/>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.
<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" /> },
]}
/>State timeline
Give each zone its own lane. A state has no y value, so don’t plot it as a line.
<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}
/>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.
| Key | What it does |
|---|---|
| Tab | Moves focus to the plot. |
| ← / → | Steps to the previous or next reading. In a stack, Shift moves ten. |
| Home / End | Jumps to the first or last reading. |
| Escape | Hides the readout. |
Props
Every engine takes times, domain, state, answer and thresholds. A reading is a number, or null for none.
WAreaChart
| Prop | Type | Default | Description |
|---|---|---|---|
| series | AreaSeries[] | — | Each: data, color, name, times?, raw?, dashed?, fillScale?. |
| times / domain | number[] / [from, to] | — | Reading times and the window, in ms. |
| yMin / yMax | number | 0 / — | The scale. Fix it per kind of reading. |
| unit / fmt | string / (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. |
| thresholds | Threshold[] | — | Dashed rules: value, label, short?, tone?. |
| endLabels | boolean | false | Latest value per series in the gutter. |
| answer | AnswerRowProps | — | Latest value, delta and verdict above the plot. |
| state | ChartState | 'ready' | ready, loading, refetching, empty, stale or partial. |
| stateMessage / stateDetail | ReactNode | — | The loading or empty message. |
| staleSince / partialFrom | number | — | Where the stale or partial tail starts. |
| gapMs | number | 2.5 × spacing | Readings further apart break the line. |
| xTicks / timeZone | XTick[] / string | auto / 'UTC' | Explicit ticks, and the zone for labels. |
| ariaLabel | string | — | One-sentence summary that names the chart. |
AnswerRowProps
| Prop | Type | Default | Description |
|---|---|---|---|
| value / unit | number | null / string | — | The latest reading. null prints n/a. |
| label | ReactNode | — | 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. |
| muted | boolean | — | Greys the value when it is stale. |
WBarChartDc
| Prop | Type | Default | Description |
|---|---|---|---|
| data / yMax | Reading[] / number | — | A value per bucket; null is an n/a column. |
| times / bucketMs | number[] / number | — / a day | Bucket starts, and each bucket’s length. |
| unit / fmt | string / (v) => string | — | After each value, and how values round. |
| partialLast / partialLabel | boolean / string | — / 'so far' | The last bucket is unfinished. |
| color / colorLast | string | — | Bars, and the latest bar. Ramp tokens only. |
WChartStack
| Prop | Type | Default | Description |
|---|---|---|---|
| panels | StackPanel[] | — | Each: key, title, height, chart; label? and fmt?. |
| domain | [from, to] | — | The shared window, in ms. |
| summary | ReactNode | — | What changed, above the panels. Also its name. |
| gutters / gap | { left, right } / number | 40, 96 / 14 | Shared gutter widths, and space between panels. |
| readout | boolean | true | One readout listing every panel. |
WStateTimeline
| Prop | Type | Default | Description |
|---|---|---|---|
| lanes | TimelineLane[] | — | 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 / now | string[] / number | — / 0.47 | Axis labels, and the now marker. |
Tokens
| Token | Value | Where it lands |
|---|---|---|
| SERIES_RAMP | #1877F2 | Series, in order; blue-1000 first. |
| SERIES_LONE | #00AEEF | Brand cyan, blue-900: a lone default series. |
| SERIES_REFERENCE | #7F7D83 | Reference line, solid 1.5px, grey-900. |
| BAND_FILL.neutral | #F1F1F1 | Range band, grey-300. The default. |
| BAND_FILL.blue | #EBFAFF | A water range band, blue-100. |
| BAND_EDGE.neutral | #ADACB0 | Band bracket in the gutter, grey-800. |
| THRESH_LINE.warn | #EC980C | Warning threshold rule, squash-800. |
| THRESH_LINE.bad | #E12121 | Critical threshold rule, red-800. |
| THRESH_LINE.neutral | #4F4D55 | Set-point rule, such as pump on, grey-1000. |
| GAP_FILL | #F5F5F5 | Gaps and stale tails, grey-200. |
| PARTIAL_FILL | #EBFAFF | The unfinished period, blue-100. |
| SKELETON_FILL | #F1F1F1 | Loading pills, grey-300. |
| TONE.*.surface | #E1FAEA | Verdict chip ground, by tone. |
| T.grid | #F1F1F1 | Gridlines, grey-300. |
| T.baseline | #E6E6E6 | The baseline under each plot, grey-500. |
| T.ink3 | #4F4D55 | Ticks, labels and the change arrow. |
| TIMELINE_TONE | #00AEEF | Running blue-900, queued squash-800, fault red-800. |
| RAMP.grey[1200] | #1D1C20 | Readout ground. |