Charts/Base
State Timeline
<StateTimeline /> · 14 examples
One lane per thing, with coloured blocks showing which state it was in and for how long.
Overview
Some data is not a number. A service is healthy or degraded or down. A machine is idle, running or faulted. A feature flag is on or it is off, and the interesting question is when it changed and what else changed at the same time. Plotting those on a line chart means inventing numbers (healthy is 1, degraded is 2) and then trying to read an axis you made up.
A state timeline drops the axis and keeps the idea. Time runs left to right, each lane is one thing, and a block of colour says in this state from here to there. It looks like a Gantt chart and behaves like a very patient historian. It came into operations tooling because it is the cheapest way to see correlation: if three lanes turn red at the same x, you have a shared cause, and a shared cause is a shorter meeting.
In zopra-charts the blocks are made from samples. Each row is a value at a time. Neighbouring samples with the same state are merged into one block. That means a service that reports OK every ten seconds for a day is one long green bar, and not 8,640 green dots that the reader has to take on trust.
Props
API
<StateTimeline> shows which state each thing was in over time, one lane per thing: services up or down, a machine's mode, a threshold level. It's a port of Grafana's State timeline panel; see Coming from Grafana.
use zopra_charts::*;<StateTimeline><StateRow>- Value mappings
- Colour
- How states are drawn
- Tooltip, legend and pages
- Enums and types
- Coming from Grafana
Not sure which option fits? See the guide.
<StateTimeline>
| Prop | Type | Default |
|---|---|---|
data | Vec<T>, Signal<Vec<T>>, Frame<N>, Signal<Frame<N>>, or your own Dataset | required |
x | Fn(&T) -> f64: the time of each row | for a Frame: the x_field, or the first field |
x_field | field name, for a Frame | the first field |
| children | <StateRow> tags | for a Frame: one lane per field other than x |
merge_values | bool | true |
show_value | VisibilityMode | Auto |
align_value | ValueAlign | Left |
row_height | f32, share of each lane, 0.0–1.0 | 0.9 |
per_page | usize | 20 |
mappings | &'static ValueMappings | none |
color | GaugeColor | green–yellow–red scheme |
thresholds | Thresholds | your theme's good colour, then its bad colour from 80 |
min / max | f64 | each lane's smallest / largest number |
fill_opacity | f32, 0.0–1.0 | 0.7 |
line_width | f32 px | 0. |
span_nulls | NullSpan | Never |
insert_nulls | f64, in x units | none |
hide_from | HideFrom | nothing hidden |
x_axis | AxisConfig | numeric; use .time() for timestamps |
y_axis | AxisConfig: placement (Hidden hides lane names, Right moves them) and width | names on the left |
legend | Legend: list() / hidden(), bottom() / right() | Legend::list() at the bottom |
tooltip | Tooltip | Tooltip::single() |
on_select | Fn(SelectedRange, &mut Window, &mut App) | none |
class, key |
<StateRow>
<StateRow name="api" value={|c: &Check| c.api} />| Prop | Type | Default |
|---|---|---|
value | Fn(&T) -> V, where V is a number, text, bool, Option of those, or a StateValue | required |
name | text | "Series 1", "Series 2", … |
mappings, color, thresholds, fill_opacity, line_width, span_nulls, insert_nulls, hide_from | as on the chart | the chart's |
None, f64::NAN and StateValue::Null are missing values.
Value mappings
Mappings turn raw values into state names and colours. Build them once, in a static:
static STATES: LazyLock<ValueMappings> = LazyLock::new(|| {
ValueMappings::new()
.value("200", "OK", rgb(0x40a02b))
.range(500., 599., "Server error", rgb(0xd20f39))
.regex("^timeout", "Timeout", rgb(0xdf8e1d))
.special(Special::Null, "No data", rgb(0x9ca0b0))
});
<StateTimeline data={checks} x={|c: &Check| c.time} mappings={&*STATES}>| Method | Matches |
|---|---|
value(text, name, color) | the value's text exactly: "OK", "1", "true" |
range(from, to, name, color) | numbers from from to to, both included |
regex(pattern, name, color) | text that matches the regular expression |
special(Special::…, name, color) | Null, NaN, NullOrNaN, True, False or Empty (empty text) |
The first matching mapping wins. An empty name keeps the original value as the label. Mapped missing values are drawn as states; unmapped missing values are gaps.
Colour
Values that no mapping matches are coloured by color:
GaugeColor | Numbers | Text and bool |
|---|---|---|
Scheme(scheme) (default: green–yellow–red) | their position between min and max on the scheme | one palette colour per distinct value |
Thresholds | the threshold step they reach | one palette colour per distinct value |
Palette | the lane's palette colour | one palette colour per distinct value |
Fixed(color) | that colour | that colour |
With Thresholds and merge_values, neighbouring numbers in the same step merge into one block labelled < 50, 50+, 70+, as in Grafana.
How states are drawn
- Merging. Each state lasts until the next row with a different state. With
merge_values={false}, every row starts a new block. - The last state lasts until
x_axis.maxif you set it, otherwise for one typical step past the last row. - Missing values end the current state (a gap) unless
span_nullsbridges them.insert_nullsends a state when the next row is further away than its value. - Rows. Each lane gets an equal share of the height;
row_heightis how much of it the blocks fill. - Fill and outline. Blocks are filled at
fill_opacity.line_widthadds an outline in the full colour. Colours that already have their own transparency keep it. - Value text. With
show_value:Autowrites the label when at least two characters fit, shortening it with "…".Alwayswrites it even when only one character fits.Neverwrites none.align_valueplaces it at the start, centre or end of the visible part of the block.
Zooming. Drag across the lanes to zoom the time axis; double-click to zoom out. on_select receives the range as a SelectedRange.
Tooltip, legend and pages
- Tooltip.
Tooltip::single()shows the hovered lane's state, with its start time and how long it lasted.Tooltip::multi()shows every lane's state at the hovered time, and draws a line there.sortorders the rows by state name.
- Legend. It lists the mapped states; otherwise the threshold steps, the distinct text values, or the colour scheme as a gradient with its range.
- Pages. With more lanes than
per_page, page buttons appear below the chart.
Enums and types
| Type | Values |
|---|---|
StateValue | Null, Number(f64), Text(SharedString), Bool(bool); made from numbers, text, bool and Option |
Special | Null, NaN, NullOrNaN, True, False, Empty |
ValueAlign | Left (default), Center, Right |
VisibilityMode | Auto (default), Always, Never |
SelectedRange | { x: (f64, f64), y: Option<(f64, f64)> } |
Coming from Grafana
| Grafana option | Here |
|---|---|
| Merge equal consecutive values | merge_values |
| Show values, Align values, Row height | show_value, align_value, row_height |
| Page size | per_page |
| Line width, Fill opacity | line_width, fill_opacity (0.0–1.0) |
| Connect null values, Disconnect values | span_nulls, insert_nulls |
| Value mappings | mappings |
| Colour scheme, Thresholds, Min, Max | color, thresholds, min, max |
| Axis placement and width | y_axis |
| Legend, Tooltip | legend, tooltip |
| Hide in area | hide_from |
Not available here: units from Grafana's unit registry, data links and annotations. Text values without a mapping get one palette colour per value, where Grafana gives them all the field's colour.
Run them
14 examples
Press Run to load an example into the live render at the top of the page. Open one to read its code.
How to read it
- 01Scan each lane for its dominant colour. That is the normal state. Everything else is an exception.
- 02Look for vertical alignment across lanes. Blocks that change at the same x are usually one event seen from several places.
- 03Note the short blocks. A thin sliver of red that repeats is a flapping service, which is often worse than a long outage.
- 04Hover for the state, its start and end, and the value.
Tooltip::multi()shows every lane at the same moment. Drag to zoom in time.
When to use it, and when not to
Reach for it when
- The data is categorical over time: up/down, running/idle, 200/404/500, on/off.
- You have a number that is really a level, such as CPU in normal, high and critical bands, and the thresholds are what matters.
- You want to compare many lanes at once and look for things that happen together.
- How long each state lasted matters, not just how often it happened.
Skip it when
- Each sample is a separate result and every one matters, such as a nightly job. Status History draws one cell per sample.
- You care about the shape of a numeric signal. Colour bands throw the curve away. Use Trend.
- There are more than about eight states. Colours stop being distinguishable and the legend becomes homework.
- You need exact durations. Block widths are honest but approximate. Put the numbers in a table.
The math behind it
Samples to segments
A lane is a sequence of samples with times and states . A state is the value after it has been passed through the lane's colouring rule: a value mapping, a threshold step, or a colour scheme. Two samples are the same state when their resolved keys are equal, not when their raw numbers are: with thresholds, a CPU at 31% and one at 44% are both normal and merge.
A segment opens at the first sample of a state and closes at the next sample with a different state, or at the end of the data. Writing for the resolved key:
Gaps and missing values
Two things end a segment early. First, a missing value ( is null): by default the block ends at that sample and a gap follows, because unknown is not the same as same as before. span_nulls can bridge nulls always, or only below a given gap length. Second, insert_nulls is a timeout: if the next sample arrives more than that long after the previous one, the block is closed at the previous sample plus the timeout, so a source that went silent does not appear to stay healthy forever.
With merge_values={false} nothing is merged and every sample becomes its own block, which is the right choice when each sample is an event rather than a reading.
Blocks to pixels
The x scale maps the data's time range linearly onto the width of the plot, so a block's width is exactly proportional to its duration. Lanes stack vertically, each of height row_height, and are paginated by per_page. A block's label is written only if it fits, and align_value puts it left, centre or right.
Real life usage
Seven red slivers
Hanna was the on-call engineer for a ticketing company in Tallinn on the night the big concert went on sale. Pages kept firing for the checkout service, then stopping, then firing again, in a rhythm that a colleague described as someone knocking politely on a very large door.
Each page, taken alone, looked like a blip. The status line on the dashboard showed degraded for ninety seconds and then OK. Nobody could say whether this was one problem repeated or seven different ones, which matters, because one problem has one fix and seven problems have a long night.
What Hanna needed was to see the state of the checkout service, its database, its payment gateway and the cache layer on the same time axis, for the last six hours, with the state names spelled out. She did not want numbers. She wanted to know who was red when.
She built it with <StateTimeline> from zopra-charts: one lane per component, a shared value mapping from OK to green, DEGRADED to amber and DOWN to red, and a lower row height so all eleven lanes fit without a scroll bar.
The picture was unambiguous. Seven red slivers on the cache lane, each about forty seconds wide, and immediately after each one, an amber block on checkout. The database lane was green throughout. It was never the database. It was never the database, in the way that it is never the database right up until it is.
The cache nodes were being recycled by an autoscaler that treated a brief memory spike as a reason to restart. Hanna raised the threshold and the slivers disappeared. The concert sold out with checkout at 100 per cent for the last four hours.
She printed the chart and stuck it on the team's fridge with a magnet that said it is the cache. Nobody has taken it down, and nobody has said it was not.
Pitfalls
- One mapping per lane. If every lane builds its own value mapping, the legend lists OK once per lane. Share one
staticmapping. - Null means unknown. A missing sample leaves a gap by default. If it means no change, set
span_nulls. If the source stops sending, setinsert_nulls. - Merging hides samples. With the default merge, ten identical readings are one block. Turn merging off when every sample is an event.
- Too many lanes. Past a screenful, use
per_pageand keep the number at what your panel can show. - Colour as the only signal. Value text inside the block helps readers who cannot tell amber from green.
From around the web