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

XY Chart

<XYChart /> · 17 examples

Points, lines or bubbles placed by two numbers, with size and colour for a third and fourth.

Overview

Most questions that start with does this go with that? end up here. Taller people weigh more. Hotter servers fail more. Larger pull requests take longer to review, and the exceptions are the interesting part. An XY chart puts one number on the horizontal axis, another on the vertical axis, and lets your eye do what eyes are annoyingly good at: spotting the cloud, the line through it, and the one dot that is definitely not supposed to be there.

The idea is Descartes' (the coordinate plane carries his name, Cartesian, for good reason), and the habit of plotting measurements against each other became routine in the nineteenth century, when scientists discovered that a table of 400 numbers produces glazed eyes and a scatter plot produces an argument. That is the real job of the chart. It does not summarise. It shows you every row and trusts you to find the pattern.

In zopra-charts the XY chart is the plain, unsorted cousin of Trend. Neither axis has to be ordered, rows are drawn in the order you give them, and you can spend two more dimensions on size and colour. Four variables on one flat plot sounds like a crime. Used with restraint it is a very small, very legal one.

Props

API

<XYChart> plots numeric x/y pairs as points, lines, or both. A third value can drive point size, and another can drive point colour. If you have used Grafana's XY Chart panel, every option there has a counterpart here; see Coming from Grafana.

use zopra_charts::*;
  • <XYChart>
  • <Series>
  • Style props
  • AxisConfig
  • Legend
  • Tooltip
  • Color and Scheme
  • Data
  • Enums
  • Coming from Grafana

Not sure which option fits? See the guide.


<XYChart>

PropTypeDefault
dataVec<T>, Signal<Vec<T>>, Frame<N>, Signal<Frame<N>>, or your own Datasetrequired
children<Series> tagsnone: series are created from data
x_axisAxisConfigAxisConfig::new()
y_axisAxisConfigAxisConfig::new()
legendLegendLegend::list() at the bottom
tooltipTooltipTooltip::single()
classTailwind classesfills its parent
keyanything Into<ElementId>–
style propsdefaults for every series

Where the series come from

  • With <Series> children, each child is one series.
  • Without children, the series come from a Frame: the first field is x and every other field is a y series. A Vec<T> has no field names, so a chart with no children and a Vec<T> shows "No data".

The chart remembers which series you hid in the legend and which point is hovered. Give each chart a key when you render several of them in a loop, so that state stays with the right one.

<Series>

PropTypeDefault
xFn(&T) -> f64required
yFn(&T) -> f64required
nametext"Series 1", "Series 2", …
sizeFn(&T) -> f64every point uses point_size
colorColor<T>, or any Hsla / Rgbathe next colour from the theme palette
keyanything Into<ElementId>–
style propsinherited from <XYChart>

Return f64::NAN from x or y to leave a row out.

Write the row type on the first closure of each tag. The other closures on that tag pick it up:

<Series x={|r: &Reading| r.temperature} y={|r| r.load} />

Style props

<XYChart> and <Series> both take these props. A value on the series wins over the chart's value, and the chart's value wins over the default.

PropTypeDefaultNotes
showShowPointspoints, lines, or both
point_sizef32 px5.used when the series has no size
size_rangeRangeInclusive<f32> px5.0..=100.0smallest and largest point, when the series has a size
point_shapePointShapeCircle
point_stroke_widthf32 px1.outline around each point
fill_opacityf32, 0.0–1.00.5colours with their own alpha keep it
line_widthf32 px2.
line_styleLineStyleSolid
hide_fromHideFromnothing hiddenhide a series from the legend, the tooltip, or the chart

Point size grows by area: a value twice as large gets a point with twice the area, not twice the diameter. The smallest and largest size values across all series map to the two ends of size_range.

AxisConfig

AxisConfig::new()
    .label("Load")
    .placement(AxisPlacement::Right)
    .soft_min(0.)
    .scale(ScaleKind::Log { base: 10. })
MethodArgumentDefault
labeltextworked out from field names (see below)
placementAxisPlacementx at the bottom, y on the left
widthPixelsfits the widest tick label
soft_min / soft_maxf64none
min / maxf64none
scaleScaleKindLinear
gridbooltrue
borderboolfalse
centered_zeroboolfalse
color_modeAxisColorModeText
decimalsusizechosen from the tick spacing
format|value| -> SharedString closurenone
time– : values are milliseconds since the Unix epoch, shown as dates and times in UTCoff
utc_offseti32 minutes0
reversebool: turns the axis aroundfalse

The axis range starts at the data's extent, rounded out to tidy tick values (whole powers for a log scale).

  • soft_min / soft_max widen the range when the data doesn't reach them, but never narrow it.
  • centered_zero makes the range symmetric around 0.
  • min / max pin the range exactly, and data outside it is cut off.

A log scale ignores zero and negative values. With no data the range is 0 to 1.

The label, when you don't set one, is the field name if every series shares it, or the words common to the start and end of the field names. Only Frame data has field names.

Tick and tooltip text uses format if you set it. Otherwise it uses decimals. Otherwise the decimals are chosen from the tick spacing. format takes a closure that doesn't capture anything; end it with .into():

AxisConfig::new().format(|v| format!("{v:.0} °C").into())

Legend

Legend::table().right().width(px(240.)).calcs([Calc::Mean, Calc::Max]).sort_by(LegendSort::Calc(Calc::Mean)).sort_desc(true)
MethodArgumentDefault
Legend::list() / table() / hidden()–list
bottom() / right() / placementLegendPlacementBottom
widthPixelsfits its content (right placement only)
calcsa list of Calcnone
sort_byLegendSort::Name or LegendSort::Calc(..)series order
sort_descboolfalse
limitusizeno limit
wrapboolfalse: long names are cut off with an ellipsis
modeLegendModeList

Clicking a legend item shows only that series; click it again to show them all. Ctrl-click (Cmd-click on macOS) shows or hides a single series. Hidden series stay in the legend, dimmed. The axes rescale to the series still shown, while colours and point sizes stay the same.

Calcs

Calcs summarise each series' y values, skipping NaN. They always appear in this order, whatever order you list them in:

CalcShown asMeaning
First / LastFirst / Lastfirst / last value
Min / MaxMin / Maxsmallest / largest
MeanMeanaverage
MedianMedianmiddle value
SumTotalsum
CountCountnumber of values
RangeRangeMax - Min
DiffDifferenceLast - First
Variance / StdDevVariance / StdDevspread of the values

Tooltip

MethodArgumentDefault
Tooltip::single() / hidden()–single
max_widthPixelsnone
max_heightPixelsnone

The same Tooltip type is shared with <Trend>, which also uses Tooltip::multi(), sort and hide_zeros. The XY Chart shows multi() like single(), as Grafana's XY Chart does.

Hovering a point highlights it and shows a tooltip next to it. The tooltip has:

  • the series name and the point's colour;
  • the x and y values, each labelled with its field name or axis label;
  • the size and colour values, when the series has them.

Series with hide_from.tooltip are still highlighted on hover, but show no tooltip. Points from series drawn only as lines can't be hovered.

Color and Scheme

color={rgb(0x89b4fa)}
color={Color::by(|r: &Reading| r.memory, Scheme::GreenYellowRed)}
color={Color::by(|r: &Reading| r.load, Scheme::thresholds(vec![(0., green), (80., red)]))}
Color<T>Meaning
Color::fixed(color), or any Hsla / Rgbaone colour for the whole series
Color::by(value, scheme)each point is coloured by its value, through scheme
SchemeMeaning
GreenYellowRed, RedYellowGreen, BlueYellowRed, YellowRed, BluePurple, YellowBlue, Blues, Reds, Greens, Purples, Orangesgradients from the theme
Named("…")a gradient you added to your theme file
Scheme::gradient(colors)your own gradient
Scheme::thresholds([(value, color), …])fixed steps: a value takes the colour of the highest threshold at or below it, and values below the first threshold take the first colour

gradient and thresholds take a Vec or a &'static slice. Prefer a static slice: it costs nothing to rebuild on every render. See Static colour lists.

A gradient spans the smallest to the largest colour value across all series that colour by value, so equal values look the same in every series. Points with a NaN colour value use the theme's fallback colour.

Data

Which data type
dataUse it whenRecomputed
Vec<T>the data is fixed, or comes from propson every render
Signal<Vec<T>>the data lives in your component's state or changes over timeonly when the signal is written
Frame<N>the data is a table of named numeric fieldson every render
Signal<Frame<N>>a table that lives in state or streams inonly when the signal is written

See Pass data as a signal.

Frame<N>

A table with N named numeric fields, one row per [f64; N].

MethodMeaning
Frame::new(names)an empty frame with N field names
Frame::from_columns([(name, values), …])a frame built from columns; short columns are padded with NaN
.with_rows(rows)sets the rows, from a Vec<[f64; N]> or an array of them
.push(row)appends a row
.size(field)without <Series>, sizes every series' points by this field
.color(field, scheme)without <Series>, colours every series' points by this field
.index(field)the position of a field
.get(field)a value accessor for <Series>, found by field name

Without <Series>, the size and colour fields aren't drawn as series of their own. Words shared by every series name are trimmed: cpu load host-a and cpu load host-b become host-a and host-b, and the y axis is labelled cpu load.

With <Series>, read fields by position (x={|r: &[f64; 3]| r[1]}) or by name (x={frame.get("Requests")}).

Your own data source

Implement Dataset<T> to chart data stored some other way:

pub trait Dataset<T>: 'static {
    fn rows<'a>(&'a self, cx: &'a App) -> &'a [T];
    fn revision(&self, cx: &App) -> Option<Revision> { None }
    fn schema<'a>(&'a self, cx: &'a App) -> Option<Schema<'a, T>> { None }
}

pub struct Schema<'a, T> {
    pub names: &'a [SharedString],
    pub read: fn(&T, usize) -> f64,
    pub size: Option<usize>,
    pub color: Option<(usize, &'a Scheme)>,
}
MethodImplement it to
rowsreturn the rows
revisionreturn a value that changes whenever the rows change. While it stays the same, the chart reuses its work. Revision::of(signal, cx) builds one from a zopra signal.
schemadescribe named numeric fields, so charts can build series (XY Chart) or bars (Bar Gauge) without accessors. read(row, field) returns one value; size and color pick fields for the XY Chart's auto mapping.

Enums

EnumVariants
ShowPoints (default), Lines, PointsAndLines
PointShapeCircle (default), Square
LineStyleSolid (default), Dash(&[on, off, …]), Dot(gap); ready-made LineStyle::DASH and LineStyle::DOT
HideFrom{ legend, tooltip, viz }, all bool
ScaleKindLinear (default), Log { base }, Symlog { base, linear_threshold }
AxisPlacementAuto (default), Top, Right, Bottom, Left, Hidden
AxisColorModeText (default), Series (tick labels use the first series' colour)
LegendModeList (default), Table, Hidden
LegendPlacementBottom (default), Right
LegendSortName, Calc(Calc)
TooltipModeSingle (default), Multi (shown like Single), Hidden

Dot draws small square dots along the line.

Coming from Grafana

Grafana optionHere
Series mapping: Autono <Series> children, with a Frame
Series mapping: Manual<Series> children
Series: X / Y / Size / Color field<Series x y size color>
Series name<Series name>
LegendLegend
TooltipTooltip
Field defaults (show, point size, line style, …)style props on <XYChart>
Field overridesstyle props on <Series>
Axis options, scale distributionAxisConfig
Min, Max, DecimalsAxisConfig::min, max, decimals
UnitAxisConfig::format
Color schemeColor and Scheme, with the palette from your theme

Not available here: several data frames in one chart (combine your rows first), separate y axes per unit, value mappings, data links, the tooltip's sort and hide-zeros options, and ordinal axes. fill_opacity goes from 0.0 to 1.0 rather than 0 to 100.

Run them

17 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. 01Look at the overall shape first: a rising cloud, a flat one, two blobs, a curve. The shape is the finding. Individual points are footnotes.
  2. 02Find the outliers and ask whether they are errors or the story. A dot far from the cloud is either a bug in the data or a customer worth calling.
  3. 03If points have different sizes, remember that area tracks the value. A dot twice as big is twice the value, not four times.
  4. 04Hover a point for its x and y, plus its size and colour value when the series has them. Click a legend entry to isolate a series, and Ctrl-click (Cmd-click on macOS) to toggle just one.

When to use it, and when not to

Reach for it when

  • Both axes are measurements: load against latency, dose against response, price against area.
  • You want to see clusters, outliers or correlation before you decide what to say about them.
  • A third value (a size) or a fourth (a colour) explains the cloud: the big dots are the big customers, the red ones are failing.
  • The order of rows is the story, as in a path, a trajectory or a hysteresis loop. Lines follow your row order exactly.
  • You need one chart for many groups: one <Series> per group, each with its own colour, sharing the same axes.

Skip it when

  • x is a category such as team or region. A bar chart gives each category a slot and a label. A scatter plot gives you ugly numbers on an axis that means nothing.
  • x is time and the question is how a value moved. Time Series has a time axis, sensible ticks and zoom that understands clocks.
  • You have tens of thousands of points that sit on top of each other. Lower fill_opacity helps up to a point. Past it, bin the data and use a Heatmap.
  • Size and colour carry different variables on every series. Readers will compare them as if they were the same thing, and then it is your fault.

The math behind it

From rows to pixels

Each row rrr gives a pair (xr,yr)(x_r, y_r)(xr​,yr​). The chart finds the smallest and largest values on each axis, rounds them outwards to a nice range, and maps that range linearly onto the plot rectangle of width WWW and height HHH. Hidden series are left out before the extents are computed, so the axes rescale to what you can still see.

The nice range uses a step sss built from the raw span. With ℓ\ellℓ and hhh the data limits, the raw step is (h−ℓ)/5(h-\ell)/5(h−ℓ)/5. Write it as m⋅10km \cdot 10^{k}m⋅10k with m∈[1,10)m \in [1,10)m∈[1,10), then pick the smallest m′m'm′ from the list 1,2,2.5,5,101, 2, 2.5, 5, 101,2,2.5,5,10 that is at least mmm. The axis runs from ⌊ℓ/s⌋s\lfloor \ell / s \rfloor s⌊ℓ/s⌋s to ⌈h/s⌉s\lceil h / s \rceil s⌈h/s⌉s with s=m′⋅10ks = m' \cdot 10^{k}s=m′⋅10k, which is why your axis ends at 250 rather than 237.

px=x−axbx−ax W,py=(1−y−ayby−ay)Hp_x = \frac{x - a_x}{b_x - a_x}\,W, \qquad p_y = \Bigl(1 - \frac{y - a_y}{b_y - a_y}\Bigr)Hpx​=bx​−ax​x−ax​​W,py​=(1−by​−ay​y−ay​​)H
s=m′⋅10k,a=⌊ℓ/s⌋ s,b=⌈h/s⌉ ss = m'\cdot 10^{k}, \quad a = \lfloor \ell/s \rfloor\, s, \quad b = \lceil h/s \rceil\, ss=m′⋅10k,a=⌊ℓ/s⌋s,b=⌈h/s⌉s

A third value as area

When a series has a size accessor, its values vvv are normalised across all series together, so two series use the same scale and can be compared. With vmin⁡v_{\min}vmin​ and vmax⁡v_{\max}vmax​ the extremes and [A0,A1][A_0, A_1][A0​,A1​] the size_range (default 555 to 100100100), a point gets

The drawn diameter is the square root of that interpolated quantity, so a value twice as large covers about twice the area, not twice the width. Missing or non-finite sizes fall back to the smallest end of the range.

t=v−vmin⁡vmax⁡−vmin⁡,d=A0+t (A1−A0)t = \frac{v - v_{\min}}{v_{\max} - v_{\min}}, \qquad d = \sqrt{A_0 + t\,(A_1 - A_0)}t=vmax​−vmin​v−vmin​​,d=A0​+t(A1​−A0​)​

Which point is under the cursor

Hovering is a nearest-point question, and answering it by scanning every row on every mouse move would be rude. The chart drops each point into a coarse grid of pixel cells once, then on each move checks only the cells around the cursor. A point is hit when the cursor lies within the radius (d+w)/2(d + w)/2(d+w)/2 of its centre, where www is the point's stroke width, and the closest such point wins. Points lying outside the plot area are not drawn and cannot be hit.

Real life usage

Forty-one goats and one very honest scatter plot

Mirela ran quality at a small cheese cooperative in the hills above Sibiu. The cooperative bought milk from forty-one farms, and for years the only thing anyone knew about those farms was the price per litre and whether the farmer would pick up the phone.

Then a batch of aged cheese came back from a distributor with a note that said, in the polite way these notes say things, that it tasted like a wet barn. Mirela had a spreadsheet with a row per farm: average somatic cell count in the milk, fat percentage, litres delivered per week. She also had a boss who wanted to know by Friday whether this was one bad farm or a pattern.

She started with the thing everyone starts with, a pivot table, and stared at it for an afternoon. Forty-one rows of numbers is an excellent way to feel busy.

On Thursday she put the data into an <XYChart> from zopra-charts. Cell count on the x axis, fat percentage on y, weekly litres as the point size, and a colour ramp from green to red for the number of failed taste panels. It took about twenty lines of Rust, most of which were opening the CSV.

The cloud had a clear tilt, which she half expected: higher cell count, lower fat. What she did not expect was one enormous red dot sitting alone at the top right, a farm that delivered more milk than any other and failed nearly every panel. Farm number 17 was also the only one that had been added in the last year, and the only one with a new milking shed that nobody had inspected.

The inspection found a cleaning schedule that existed mainly as a laminated sheet on the wall. After the fix the farm's dot slid left and down over two months, and the failed panels dropped from nine in a quarter to one. The distributor stopped writing notes about barns.

Mirela keeps the first version of the chart pinned above her desk. Under it she wrote one sentence: forty-one rows of data, one dot that mattered, and a pivot table that had been hiding it for years.

Pitfalls

  • Lines are drawn in row order. If your rows are not sorted by x and you ask for lines, you get a scribble. Sort first, or use Trend, which does it for you.
  • Overplotting. A thousand dots stacked in the same place look like one dot. Lower fill_opacity so dense areas read darker.
  • Size range too wide. With the default 5 to 100 range, one huge value can bury its neighbours. Narrow size_range when the biggest bubble covers half the plot.
  • Colour without a scheme choice. A rainbow for a one-sided value suggests categories that are not there. Use a single-hue gradient for how much, thresholds for ok, warning, critical.
  • Data as a plain Vec. Passed directly, the per-row work is redone on every render of your component. Keep large data in a signal.

From around the web

Watch and read

YouTubeNew: XY Chart is Generally Available in Grafana 11.1 | GrafanaGrafanaGrafana announcement and demo of the XY chart panel, which this chart is modelled on.

Keep going

Related charts

TrendWhen x always goes up and you want lines joined in x order, with stacking and a shared tooltip.HeatmapWhen there are so many points that counting them per cell tells you more than drawing them.BeeswarmWhen one axis is a category and you want every point visible without overlap.Time SeriesWhen x is time.
NextTrend

On this page

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