Zopra/Charts
Guide
Getting startedAll chartsWhich chart to use?Domain specific
Base charts7
XY ChartTrendCandlestickHeatmapState TimelineFlame GraphBar Gauge
Derived charts73
Time SeriesHistogramStatus HistoryStatGaugeHoneyCombTracesNode GraphHeikin-AshiRenkoKagiPoint & FigureParetoIshikawaControl ChartBland-AltmanLollipopDumbbellSlopeBumpBack-to-BackWaterfallBulletFunnelMekkoForestBarcodeBeeswarmRidgelineHorizonSpectrogramAttention MapWaffleVennUpSetWarped WaffleWord CloudGanttSankeySankey MatrixTreemap ForceChord DiagramCircosArc DiagramAlluvialSankey FunnelDendrogramIcicleSunburstSpiderNightingale RoseParallel CoordinatesOscilloscopeEfficient FrontierPhase PortraitPoincaré MapBifurcationH-R DiagramRamachandranHarkerRoot LocusRadiation PatternQuiverHodographSmith ChartEye DiagramSequence LogoTernaryTernary ContourPiperStüveTephigramChoropleth

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>

PropTypeDefault
dataVec<T>, Signal<Vec<T>>, Frame<N>, Signal<Frame<N>>, or your own Datasetrequired
xFn(&T) -> f64: the time of each rowfor a Frame: the x_field, or the first field
x_fieldfield name, for a Framethe first field
children<StateRow> tagsfor a Frame: one lane per field other than x
merge_valuesbooltrue
show_valueVisibilityModeAuto
align_valueValueAlignLeft
row_heightf32, share of each lane, 0.0–1.00.9
per_pageusize20
mappings&'static ValueMappingsnone
colorGaugeColorgreen–yellow–red scheme
thresholdsThresholdsyour theme's good colour, then its bad colour from 80
min / maxf64each lane's smallest / largest number
fill_opacityf32, 0.0–1.00.7
line_widthf32 px0.
span_nullsNullSpanNever
insert_nullsf64, in x unitsnone
hide_fromHideFromnothing hidden
x_axisAxisConfignumeric; use .time() for timestamps
y_axisAxisConfig: placement (Hidden hides lane names, Right moves them) and widthnames on the left
legendLegend: list() / hidden(), bottom() / right()Legend::list() at the bottom
tooltipTooltipTooltip::single()
on_selectFn(SelectedRange, &mut Window, &mut App)none
class, key

<StateRow>

<StateRow name="api" value={|c: &Check| c.api} />
PropTypeDefault
valueFn(&T) -> V, where V is a number, text, bool, Option of those, or a StateValuerequired
nametext"Series 1", "Series 2", …
mappings, color, thresholds, fill_opacity, line_width, span_nulls, insert_nulls, hide_fromas on the chartthe 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}>
MethodMatches
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:

GaugeColorNumbersText and bool
Scheme(scheme) (default: green–yellow–red)their position between min and max on the schemeone palette colour per distinct value
Thresholdsthe threshold step they reachone palette colour per distinct value
Palettethe lane's palette colourone palette colour per distinct value
Fixed(color)that colourthat 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.max if you set it, otherwise for one typical step past the last row.
  • Missing values end the current state (a gap) unless span_nulls bridges them. insert_nulls ends a state when the next row is further away than its value.
  • Rows. Each lane gets an equal share of the height; row_height is how much of it the blocks fill.
  • Fill and outline. Blocks are filled at fill_opacity. line_width adds an outline in the full colour. Colours that already have their own transparency keep it.
  • Value text. With show_value:
    • Auto writes the label when at least two characters fit, shortening it with "…".
    • Always writes it even when only one character fits.
    • Never writes none.
    • align_value places 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.
    • sort orders 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

TypeValues
StateValueNull, Number(f64), Text(SharedString), Bool(bool); made from numbers, text, bool and Option
SpecialNull, NaN, NullOrNaN, True, False, Empty
ValueAlignLeft (default), Center, Right
VisibilityModeAuto (default), Always, Never
SelectedRange{ x: (f64, f64), y: Option<(f64, f64)> }

Coming from Grafana

Grafana optionHere
Merge equal consecutive valuesmerge_values
Show values, Align values, Row heightshow_value, align_value, row_height
Page sizeper_page
Line width, Fill opacityline_width, fill_opacity (0.0–1.0)
Connect null values, Disconnect valuesspan_nulls, insert_nulls
Value mappingsmappings
Colour scheme, Thresholds, Min, Maxcolor, thresholds, min, max
Axis placement and widthy_axis
Legend, Tooltiplegend, tooltip
Hide in areahide_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

  1. 01Scan each lane for its dominant colour. That is the normal state. Everything else is an exception.
  2. 02Look for vertical alignment across lanes. Blocks that change at the same x are usually one event seen from several places.
  3. 03Note the short blocks. A thin sliver of red that repeats is a flapping service, which is often worse than a long outage.
  4. 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 (ti,si)(t_i, s_i)(ti​,si​) with times tit_iti​ and states sis_isi​. 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 K(s)K(s)K(s) for the resolved key:

segmentm=[ ta, tb ),K(sa)=K(sa+1)=⋯=K(sb−1)≠K(sb)\text{segment}_m = \bigl[\,t_a,\ t_b\,\bigr), \qquad K(s_a) = K(s_{a+1}) = \cdots = K(s_{b-1}) \neq K(s_b)segmentm​=[ta​, tb​),K(sa​)=K(sa+1​)=⋯=K(sb−1​)=K(sb​)

Gaps and missing values

Two things end a segment early. First, a missing value (sis_isi​ 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.

end=ti−1+τif ti−ti−1>τ\text{end} = t_{i-1} + \tau \quad \text{if } t_i - t_{i-1} > \tauend=ti−1​+τif ti​−ti−1​>τ

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 static mapping.
  • Null means unknown. A missing sample leaves a gap by default. If it means no change, set span_nulls. If the source stops sending, set insert_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_page and 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

Watch and read

YouTubeBeginners Guide - How to Configure a State Timeline Panel | GrafanaGrafanaOfficial Grafana beginner guide to configuring a state timeline panel, the chart this one is modelled on.

Keep going

Related charts

Status HistoryWhen each sample is a result and neighbours should stay as separate cells.GanttWhen blocks are planned tasks with dependencies rather than observed states.HeatmapWhen you have counts or distributions instead of discrete states.TrendWhen the number's shape matters more than the band it is in.
PreviousHeatmapNextFlame Graph

On this page

  • Live
  • Overview
  • API
  • Examples
  • How to read it
  • When to use it
  • The math
  • Story
  • Pitfalls
  • Watch and read
  • Related