Charts/Core charts
Status History
<StatusHistory /> · 12 examples
A grid with one cell per sample and one row per thing, coloured by the result of each check.
Overview
Some things happen on a schedule, and the schedule is the point. A nightly backup. A health check every minute. Daily sales. Each run has a result: passed, failed, slow, or a number worth colouring. The useful picture is not the length of a state but the row of results, like a row of stitches, where a single wrong one is visible from across the room.
That is the Status History chart. Rows are things, columns are samples, and each cell is coloured by what that sample said. It is cousin to the State Timeline, with one stubborn difference: it never merges. Ten green checks in a row are ten green cells, and that is the information. You can count them. You can notice the fourth one was amber.
This is the chart of the calendar wall, the habit tracker, the CI matrix. Humans are very good at scanning a grid for the odd one out, and very bad at reading ten thousand log lines for the same thing. The chart takes advantage of that, and has the manners not to talk much.
Props
API
<StatusHistory> shows a grid of samples: one row per thing and one cell per sample, each cell coloured by its value. Use it when every sample matters: periodic health checks, hourly SLO results, a value per day. It is a port of Grafana's Status history panel; see Coming from Grafana.
use zopra_charts::*;
Status History shares its colours, value mappings, tooltip, legend and pages with State Timeline. Not sure which of the two you need? See the guide.
<StatusHistory>
<StatusHistory data={probes} x={|p: &Probe| p.time} mappings={&*STATES} x_axis={AxisConfig::new().time()}>
<StatusRow name="api" value={|p: &Probe| p.api} />
<StatusRow name="db" value={|p: &Probe| p.db} />
</StatusHistory>| 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 sample | for a Frame: the x_field, or the first field |
x_field | field name, for a Frame | the first field |
| children | <StatusRow> tags | for a Frame: one row per field other than x |
show_value | VisibilityMode | Auto |
row_height | f32, share of each row, 0.0–1.0 | 0.9 |
col_width | f32, share of each column, 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 row's smallest / largest number |
fill_opacity | f32, 0.0–1.0 | 0.7 |
line_width | f32 px | 1. |
hide_from | HideFrom | nothing hidden |
x_axis | AxisConfig | numeric; use .time() for timestamps |
y_axis | AxisConfig: placement (Hidden hides row 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 |
Zoom: hold Ctrl (Cmd on macOS) and scroll, or pinch, to zoom the time axis about the pointer. Drag to select a range, and double-click to reset.
<StatusRow>
<StatusRow name="api" value={|p: &Probe| p.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, hide_from | as on the chart | the chart's |
None, f64::NAN and StateValue::Null are missing values: their cells stay empty unless a mapping such as Special::Null names them.
How cells are drawn
- One cell per sample. Each sample gets a column as wide as the typical gap between samples. The cell is centred on the sample's time, and
col_widthis how much of the column it fills. Equal neighbours never merge; use State Timeline for that. - Rows. Each row gets an equal share of the height;
row_heightis how much of it the cells fill. - Fill and outline. Cells are filled at
fill_opacity, with aline_widthoutline in the full colour. - Value text is centred in the cell.
Autowrites it when at least two characters fit,Alwayswhen one fits,Neverhides it. - Tooltip. Hovering shows the sample's time and value.
Tooltip::multi()lists every row at that sample. - Zooming. Drag across the cells to zoom the time axis; double-click to zoom out.
on_selectreceives the range.
Coming from Grafana
| Grafana option | Here |
|---|---|
| Show values, Row height, Column width | show_value, row_height, col_width |
| Page size | per_page |
| Line width, Fill opacity | line_width, fill_opacity (0.0–1.0) |
| 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.
Run them
12 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 across a row for its normal colour. The exceptions are what you came for.
- 02Scan down a column. A vertical stripe of the same bad colour means many things failed at the same sample, which is one cause, not twenty.
- 03Look for patterns in the exceptions: every Sunday, every fifth run, always the same row.
- 04Hover a cell for its value and time.
Tooltip::multi()compares every row at that sample.
When to use it, and when not to
Reach for it when
- Each sample is a result: a check passed or failed, a job succeeded, a day's total was in or out of range.
- You want to scan many rows for the odd cell, such as one failing job among fifty nights.
- The count of ok and not ok matters, not just the lengths of runs.
- The x axis is not really time: build numbers, test runs, weeks.
Skip it when
- States that last until they change, such as up or down for hours. A State Timeline merges equal neighbours into readable blocks.
- The shape of a number matters. Colour bins throw it away. Use Trend.
- You have tens of thousands of samples per row. Cells shrink below a pixel. Aggregate first, or use a Heatmap.
- You need precise values from the picture. Colour is a verdict, not a ruler.
The math behind it
One cell per sample
For row you supply samples . The chart draws one cell for each sample, centred on , in the colour that the lane's rule assigns to . That rule is a value mapping (text or code to named colour), a threshold step, or a colour scheme over a numeric range. Nothing is merged: the cell is the sample.
The cell width is the column width in pixels, col_width, which by default leaves a small gap between neighbours. Setting it to 1 removes the gap and makes the grid read as a continuous strip. A smaller value gives separated dots. row_height works the same way vertically.
Numbers as colour
For numeric samples with the default scheme, the colour position is the value's place in the range given by min and max, clamped. With thresholds, the colour is the step the value falls in, so 31 and 44 on the same band show the same colour. With a value mapping, each distinct value has its own named colour and appears once in the legend.
Rules can differ by row: color, thresholds or mappings set on an individual <StatusRow> override the chart's defaults.
x is a number, not just a time
The columns are positioned by x, which does not have to be a timestamp. It can be a day number, a build number, a test run. When x is time, the axis uses clock ticks. Otherwise give x_axis a format to turn numbers into labels. A weekly calendar grid is just rows of weekdays with x as the week.
Real life usage
The Sunday that kept failing
Ayodele was the only person on the platform team at a small online pharmacy in Lagos who liked checking things, which in practice made him the person who was asked about all of them. There were thirty-four scheduled jobs: stock sync, invoice generation, delivery manifests, and a nightly reconciliation job that everybody agreed was important and nobody could explain.
His monitoring was an email. One, per failure, to a shared inbox that had accumulated 3,100 unread messages. Occasionally somebody would search it for a job name and report, with the energy of a person finding a coin in a coat, that it had indeed failed on a Thursday in May.
The question that mattered was not whether a job failed but whether it failed often and whether it failed together with other jobs. Email is excellent at the first kind of fact and spectacularly poor at the second.
He loaded ninety days of results into <StatusHistory> from zopra-charts: one row per job, one cell per run, a shared mapping from success to green, retry to amber and failed to red, and show_value turned off since the colours were enough. Thirty-four rows by ninety columns fit in a quarter of a screen.
The grid was mostly green with a pattern that no email could have shown him: a faint red column every seventh cell, always on a Sunday, running down through eleven jobs at once. It was not eleven problems. It was one: a database snapshot that locked the tables for about six minutes starting at 02:00.
Moving the snapshot to 04:00 took one line in a cron file and made the Sunday column vanish. The failure count over the next ninety days dropped from 142 to 19, and the 19 were genuinely different from one another, which is how he knew they were real.
The shared inbox was archived. Nobody noticed, which is the best review a monitoring system can get.
Pitfalls
- Merging is not on offer. Every sample is a cell. If you have long runs of one state, use a State Timeline instead.
- One mapping per row. Share a single
staticmapping across rows so the legend lists each state once. - Colour alone. A colour-blind reader cannot separate amber from green. Show text for small grids, or choose distinguishable colours.
- Gaps look like data. Irregular sampling leaves uneven spacing. Decide whether x is a time or just a count of runs.
- Too many rows. Use
per_pageand a number that fits your panel.
From around the web