Charts/Base
Trend
<Trend /> · 18 examples
Lines, areas, bars and steps over an ordered numeric x, with stacking, thresholds and two axes.
Overview
A trend chart is what most people picture when somebody says graph: a number on the left, an x axis along the bottom, a line wandering across. It is also the chart most likely to be used for the wrong thing, because a line is so persuasive. The moment two dots are joined, the reader believes something happened between them.
The form is old and has a known father. William Playfair drew the first line charts of economic data in the 1780s, showing England's trade balance as a pair of lines with the gap between them shaded. His point was that a picture of a number moving is worth a table of the number standing still. Two centuries later the point stands, and we still shade the gap.
Trend in zopra-charts is the general version. Its x is any numeric value that only goes up: speed, distance, depth, load. If x is a clock, Time Series is the same chart with the right axis. Each series can be a line, an area, bars or points, stack with its neighbours, sit on its own axis and change colour when it crosses a limit. It is a lot of chart. Use less of it than you can.
Props
API
<Trend> draws one or more series against a numeric x that only goes up: engine speed, distance, load, depth. It's Grafana's Time series graph with a numeric x instead of time, and takes all of its styling options. See Coming from Grafana.
use zopra_charts::*;<Trend><TrendSeries>- Style props
- Axes
- Stacking
- Colour, gradients and thresholds
- Missing values
- Annotations, exemplars and zoom
- Tooltip, legend and keyboard
- Enums
- Coming from Grafana
Not sure which option fits? See the guide.
<Trend>
| Prop | Type | Default |
|---|---|---|
data | Vec<T>, Signal<Vec<T>>, Frame<N>, Signal<Frame<N>>, or your own Dataset | required |
x | Fn(&T) -> f64 | for a Frame: the x_field, or the first field |
x_field | field name, for a Frame | the first field |
| children | <TrendSeries> tags | for a Frame: one series per field other than x |
x_axis | AxisConfig | AxisConfig::new() |
y_axis | AxisConfig | AxisConfig::new() |
legend | Legend | Legend::list() at the bottom |
tooltip | Tooltip | Tooltip::single() |
thresholds | Thresholds | your theme's good colour, then its bad colour from 80 |
scheme | Scheme | none |
annotations | Annotations: a Vec<Annotation>, Signal<Vec<Annotation>> or &'static [Annotation] | none |
exemplars | Vec<(f64, f64)>: (x, y) points drawn as diamonds | none |
exemplar_color | colour | magenta |
orientation | VizOrientation: Auto, Horizontal, Vertical | Auto (horizontal) |
on_select | Fn(SelectedRange, &mut Window, &mut App), after a drag-zoom | none |
keyboard | bool: arrow-key stepping | true |
class | Tailwind classes | fills its parent |
key | anything Into<ElementId> | – |
| style props | defaults for every series |
x must be in ascending order. Rows with a missing x (f64::NAN) are skipped. If x goes down anywhere, the chart shows "Values must be in ascending order" instead of a graph, as Grafana does.
With no data, or a Vec<T> with no <TrendSeries>, the chart shows "No data".
<TrendSeries>
| Prop | Type | Default |
|---|---|---|
y | Fn(&T) -> f64 | required |
name | text | "Series 1", "Series 2", … |
color | Hsla / Rgba | the next colour from the theme palette |
y_axis | AxisConfig | the chart's y_axis |
thresholds | Thresholds | the chart's thresholds |
scheme | Scheme | the chart's scheme |
| style props | inherited from <Trend> |
Return f64::NAN from y for a missing value.
<Trend data={pulls} x={|p: &Pull| p.rpm}>
<TrendSeries name="Torque" y={|p: &Pull| p.torque} />
<TrendSeries name="Power" y={|p: &Pull| p.power} y_axis={AxisConfig::new().label("hp")} />
</Trend>Style props
<Trend> and <TrendSeries> both take these. A value on the series wins over the chart's value, and the chart's value wins over the default.
| Prop | Type | Default | Notes |
|---|---|---|---|
draw_style | DrawStyle | Line | lines, bars, or points only |
interpolation | Interpolation | Linear | lines only |
line_width | f32 px | 1. | 0. draws no line |
line_style | LineStyle | Solid | lines only |
fill_opacity | f32, 0.0–1.0 | 0. | the area under a line, or the inside of a bar |
gradient | GradientMode | None | |
fill_below_to | another series' name | none | fills the band between this series and that one |
show_points | PointsMode | Auto | |
point_size | f32 px | 5. | never smaller than line_width |
show_values | bool | false | the value above each shown point or bar |
span_nulls | NullSpan | Never | |
insert_nulls | f64, in x units | none | break the line where x jumps further than this |
bar_alignment | BarAlignment | Center | |
bar_width_factor | f32, 0.1–1.0 | 0.6 | share of the space between two x values |
bar_max_width | f32 px | 200. | |
stacking | Stacking | None | |
transform | ValueTransform | None | |
thresholds_style | ThresholdsStyle | Off | |
hide_from | HideFrom | nothing hidden | hide a series from the legend, the tooltip, or the chart |
Points with Auto are shown when there's room, about one point every two point-widths, and hidden on dense data. Points that stand alone between missing values are always shown, so they don't vanish. Bars show no points with Auto.
Axes
The x axis spans exactly the smallest to the largest x. The y axes round out to tidy tick values, like the XY Chart's. Every AxisConfig method works here: label, placement, soft and hard limits, log and symlog scales, grid, border, centred zero, colour mode, decimals and format.
More than one y axis. Give a series its own y_axis to put it on a second scale.
- Series whose
y_axishas the samelabelshare one axis. - With
AxisPlacement::Auto, the first axis goes on the left and the next ones on the right. AxisColorMode::Seriescolours an axis's ticks like its first series.- The grid follows the first axis.
Stacking
Stacking | Effect |
|---|---|
Stacking::None | each series on its own |
Stacking::Normal(group), or Stacking::NORMAL for group "A" | series in the same group are added on top of each other |
Stacking::Percent(group), or Stacking::PERCENT | each series is shown as its share of the group's total; the axis reads 0% to 100% |
Positive and negative values stack separately, so a negative series grows down from zero. A stacked series with a fill_opacity fills down to the series below it. The tooltip and legend show the real values, not the stacked totals. Series hidden from the legend drop out of the stack.
Colour, gradients and thresholds
GradientMode | Line | Fill |
|---|---|---|
None | the series colour | the series colour at fill_opacity |
Opacity | the series colour | fades from fill_opacity at the top of the chart to transparent at the bottom |
Hue | shifts in hue from top to bottom | the same, at fill_opacity |
Scheme | changes colour with the value | the same, at fill_opacity |
GradientMode::Scheme follows the series' scheme if it has one, blending between its colours from the lowest to the highest value. Otherwise it follows its thresholds, switching colour at each step.
ThresholdsStyle | What's drawn |
|---|---|
Off | nothing |
Line / Dashed | a line at each threshold step |
Area | each band between steps shaded in its colour |
LineAndArea / DashedAndArea | both |
Series | the series itself coloured by threshold, like GradientMode::Scheme with thresholds |
Threshold lines and areas sit behind the series, once per y axis. Percentage thresholds are a share of the axis's min–max, or of the data when those aren't set.
Missing values
A missing value is f64::NAN in y.
NullSpan | The line at a missing value |
|---|---|
Never | breaks |
Always | joins the values either side |
Within(gap) | joins them when they're no more than gap apart in x, otherwise breaks |
insert_nulls breaks the line wherever two values are further apart in x than its value, even if neither is missing. Use it for data that has gaps without explicit missing rows.
Annotations, exemplars and zoom
let notes = vec![
Annotation::at(deployed, "Deploy v1.4.2").text("Rolled out to all pods"),
Annotation::region(start, end, "Incident #218").color(rgb(0xd20f39)),
];
<Trend data={rows} x={|r: &Row| r.x} annotations={notes} exemplars={slow} on_select={zoomed}>- Annotations mark a point (a dashed line) or a region (a shaded band) with a marker along the bottom edge. Hover the marker for its title, time and text.
coloroverrides the default cyan. - Exemplars are individual samples, such as one slow request among the percentiles, drawn as diamonds on the first y axis. Hover one for its x and y.
- Zoom. Drag across the chart to zoom the x axis; double-click to zoom out.
on_selectreceives the selected range. - Orientation.
VizOrientation::Verticalturns the chart so x runs down the left and values run left to right. Everything else (fills, gradients, thresholds, tooltips, zoom) works the same.
Tooltip, legend and keyboard
Tooltip::multi().sort(SortOrder::Descending).hide_zeros(true).max_width(px(240.))| Method | Effect |
|---|---|
Tooltip::single() | the series nearest the pointer at the hovered x |
Tooltip::multi() | every series at the hovered x |
Tooltip::hidden() | no tooltip; the hover line still shows |
sort | SortOrder of the rows in a multi tooltip |
hide_zeros | leave out series whose value is 0 |
max_width / max_height | Pixels |
The tooltip title is the x value. A vertical line marks the hovered x and highlights each series' point there.
The legend works like the XY Chart's: click a series to show only it, Ctrl-click to toggle it. Calcs use the real values.
Keyboard. Click the chart, then press ← / → (or ↑ / ↓) to move the hover by one x value, Shift + arrow to move by ten, and Esc to clear it. keyboard={false} turns this off.
Enums
| Enum | Variants |
|---|---|
DrawStyle | Line (default), Bars, Points |
Interpolation | Linear (default), Smooth, StepBefore, StepAfter |
GradientMode | None (default), Opacity, Hue, Scheme |
PointsMode | Auto (default), Always, Never |
NullSpan | Never (default), Always, Within(f64) |
BarAlignment | Before, Center (default), After |
Stacking | None (default), Normal(&str), Percent(&str), plus NORMAL and PERCENT for group "A" |
ValueTransform | None (default), Constant (a flat line at the first value), NegativeY (drawn below zero) |
ThresholdsStyle | Off (default), Line, Dashed, Area, LineAndArea, DashedAndArea, Series |
TooltipMode | Single (default), Multi, Hidden |
SortOrder | None (default), Ascending, Descending |
VizOrientation | Auto (default, horizontal), Horizontal, Vertical |
Smooth curves pass through every point without overshooting it. StepAfter holds each value until the next x; StepBefore jumps to the next value straight away.
Coming from Grafana
| Grafana option | Here |
|---|---|
| X field | x, or x_field for a Frame |
| Style, Line interpolation, Line width, Line style | draw_style, interpolation, line_width, line_style |
| Fill opacity (0–100) | fill_opacity (0.0–1.0) |
| Gradient mode | gradient |
| Fill below to | fill_below_to |
| Show points, Point size, Show values | show_points, point_size, show_values |
| Connect null values, Disconnect values | span_nulls, insert_nulls |
| Bar alignment, Bar width factor, Bar max width | bar_alignment, bar_width_factor, bar_max_width |
| Stack series | stacking |
| Transform | transform |
| Axis placement, label, width, soft min/max, scale, centred zero, colour, border, grid | y_axis on the chart or a series |
| Min, Max, Decimals, Unit | AxisConfig::min, max, decimals, format |
| Thresholds, Show thresholds | thresholds, thresholds_style |
| Color scheme | color, or gradient with scheme / thresholds |
| Hide in area | hide_from |
| Tooltip mode, sort, hide zeros, max width/height | tooltip |
| Legend | legend |
| Annotations, Exemplars | annotations, exemplars |
| Orientation | orientation |
| Disable keyboard events | keyboard={false} |
Not available here: units from Grafana's unit registry (use AxisConfig::format), value mappings, data links and overrides by field regex (set the prop on each <TrendSeries> instead).
Run them
18 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
- 01Read the axis first, especially the left one. A line that goes up sharply on a 94 to 96 axis is a calm line on a 0 to 100 axis.
- 02Find the direction and the breaks. Gaps are real: the default behaviour is that a missing value breaks the line, and that is the chart telling you data was missing.
- 03For stacks, read the top edge as the total and the thickness of a band as that series' own value. Only the bottom band sits on the baseline, so compare the others by thickness, not by height.
- 04Hover to see every series at the same x. Click a legend entry to show only that series. Ctrl-click or Cmd-click toggles one.
When to use it, and when not to
Reach for it when
- x is ordered and numeric (distance, speed, depth, time-like) and you want to see how one or more values change along it.
- Several series share one x: you want them in one tooltip, aligned.
- Parts of a total over x: stacked areas, or percent stacking when shares are the point.
- A value has a limit: draw it as a threshold line or a shaded band, or let the line change colour when it crosses.
- Two measures with different units, such as torque and power over rpm, each on its own axis.
Skip it when
- x is a clock. Time Series gives you time ticks, timezones and compare.
- x is a category. Joining Monday to Tuesday with a line implies Monday slowly turns into Tuesday.
- x is not ordered, or you have several y values at one x. XY Chart keeps rows as given.
- You have six series that all wander around the same range. The result is a plate of spaghetti. Pick the two that matter and mute the rest.
The math behind it
Stacking
Series that share a stacking group are drawn on top of each other. At each x index , series in the group has a raw value . Positive and negative values accumulate in separate piles, so a loss never eats into a gain. The base of a series is the current pile, and its plotted top is the pile plus its own value.
Missing values (not finite) are skipped: they add nothing to the pile and leave a break in that series' line. Hidden series and series with a fill_below_to floor never join the stack.
Percent stacking
With Stacking::PERCENT each value is first divided by the column total of absolute finite values, , whenever that total is positive. The stack then always ends at 1, the axis is formatted as a percentage, and the totals disappear from the picture. Use it when you care about shares, and only then.
Smooth lines
Interpolation::Smooth is not a spline through anything clever. It is a monotone cubic Bézier: for neighbouring points and with spacing and slope , the tangent at an interior point is a weighted harmonic mean of the two neighbouring slopes, and it is set to zero when they disagree in sign.
The two Bézier control points sit one third of the way along the segment, each offset vertically by tangent times . The zero-tangent rule is what stops the curve from overshooting: a smooth line never goes higher than the highest of the two points it joins, which a plain cubic spline cheerfully does.
Real life usage
Two axes, one gearbox
Tomasz built electric motor controllers for a company that made cargo bikes in a converted tram depot in Gdansk. His job that month was to explain to the sales team why the new motor felt weaker than the old one, even though the datasheet said it was stronger.
The sales team did not want to hear about motor constants. They wanted a picture. Specifically they wanted a picture that could be sent to a customer who had written, in capital letters, that the bike felt like pushing a fridge up a hill.
The measurement was simple: torque in newton-metres and power in watts, both against rotation speed. Two units, one x axis, and a very obvious temptation to draw two charts and let the reader align them with their finger.
He used <Trend> from zopra-charts instead, with torque as one <TrendSeries> and power as a second one on its own right-hand axis, coloured to match the line. Old motor was dashed, new motor solid, thresholds drawn as a dashed line at the controller's current limit.
The chart made the answer obvious in about four seconds. The new motor had far more peak power, but it came at 480 rpm higher than the old one. At the low rpm of climbing a hill the torque curve was actually 12 per cent below the old motor. The datasheet quoted the peak, the customer lived at the bottom.
Tomasz sent the chart to firmware with one line, asking for a field-weakening profile that moved the torque up in that region. Three weeks later a new curve appeared on the same chart, and the low rpm torque was 9 per cent above the old motor. The customer was sent the new chart and a bike, and replied with a single exclamation mark.
The fridge, reportedly, has since been returned to the kitchen.
Pitfalls
- Joining points that should not be joined. Use
Pointsfor raw samples,StepAfterfor a value that holds until it changes, andLinefor continuous measurements. - Stacking different units. Stacks add numbers. Adding Nm to hp gives you a number in no unit at all. Stack like with like.
- Truncated axes. Auto ranges are chosen to fit the data, not to be honest. Fix the min to zero for bars and areas.
- Unsorted x. Trend expects x to go up. If it does not, lines join in the wrong order and you should use the XY chart.
- Smoothing as decoration. A smooth line has no data between the points. It is a presentation choice, not a measurement.
From around the web