Charts/Core charts
Histogram
<Histogram /> · 13 examples
Counts of values per bucket, so you can see the shape of a whole distribution at once.
Overview
An average is a summary, and summaries are polite. They tell you what the data would like you to believe. A histogram is the data in its own clothes: take every value, drop it into a bucket by size, count the buckets, draw the counts as bars. What comes out is a shape, and the shape tells you things the mean never would. One hump or two. A long tail or a cliff. A suspicious spike at exactly zero.
Karl Pearson gave the histogram its name in the 1890s, and the idea of counting things into bins is of course far older. The chart has outlived most of the fashions that tried to replace it, for the dull reason that it is hard to fool. You cannot hide a bimodal distribution in a histogram, though people have tried.
The one decision that is yours is the bucket size. Too wide and everything merges into one hump. Too narrow and the hump becomes a comb. In zopra-charts the default aims for about thirty buckets and rounds the width to a friendly number, so you get a sensible first picture and can decide whether the buckets mean something afterwards.
Props
API
<Histogram> counts how many values fall into each bucket and draws the counts as bars: how request times, sizes or scores are spread. It is a port of Grafana's Histogram panel; see Coming from Grafana.
use zopra_charts::*;
Not sure which option fits? See the guide.
<Histogram>
<Histogram data={requests}>
<HistogramSeries name="GET" value={|r: &Request| r.get_ms} />
<HistogramSeries name="POST" value={|r: &Request| r.post_ms} />
</Histogram>| Prop | Type | Default |
|---|---|---|
data | Vec<T>, Signal<Vec<T>>, Frame<N>, Signal<Frame<N>>, or your own Dataset | required |
| children | <HistogramSeries> tags | for a Frame: every field except xMin / xMax |
bucket_count | usize: roughly how many buckets to aim for | 30 |
bucket_size | f64: the width of every bucket | picked from bucket_count, see Buckets |
bucket_offset | f64: shifts where buckets start | 0. |
combine | bool: add every series into one Count series | false |
x_min / x_max | Fn(&T) -> f64: each row's bucket edges, for pre-counted buckets | none |
fill_opacity | f32, 0.0–1.0 | 0.8 |
gradient | GradientMode: None, Opacity, Hue, Scheme | None |
line_width | f32 px | 1. |
stacking | Stacking: None, Stacking::NORMAL, Stacking::PERCENT | None |
hide_from | HideFrom | nothing hidden |
thresholds, scheme | colours for gradient={GradientMode::Scheme} | your theme's |
x_axis | AxisConfig | decimals from the bucket size |
y_axis | AxisConfig | starts at 0 |
legend | Legend | Legend::list() at the bottom |
tooltip | Tooltip | Tooltip::single() |
class, key |
<HistogramSeries>
<HistogramSeries name="GET" value={|r: &Request| r.get_ms} color={rgb(0x1e66f5)} />| Prop | Type | Default |
|---|---|---|
value | Fn(&T) -> f64: the value to count, or the count itself for pre-counted buckets | required |
name | text | "Series 1", "Series 2", … |
color | any colour | your theme's palette |
fill_opacity, line_width, gradient, hide_from | as on the chart | the chart's |
Missing values (NaN or infinite) are not counted.
Buckets
- Size. With
bucket_size, every bucket is that wide. Without it, the chart divides the range of all values bybucket_countand picks the next size up from a list of round sizes (1, 2, 2.5, 4 and 5 times a power of ten). It never picks a size smaller than the smallest gap between two values. - Edges. A value
vfalls in the bucket starting atfloor((v - offset) / size) * size + offset. Buckets include their start and exclude their end. - Series. Every series is counted into the same buckets so their bars line up. Buckets with no values are left out.
- Hover. The tooltip shows the bucket, such as
120 - 125, and the count of each series.
Pre-counted buckets
If your data is already bucketed, give each row's edges with x_min and x_max; each series' value is then the count:
<Histogram data={buckets} x_min={|b: &Bucket| b.low} x_max={|b: &Bucket| b.high}>
<HistogramSeries name="requests" value={|b: &Bucket| b.count} />
</Histogram>Buckets may have different widths. If only x_min or only x_max is given, the other edge is guessed from the gap between the first two rows. A Frame with fields named xMin and xMax is read the same way, as in Grafana.
Coming from Grafana
| Grafana option | Here |
|---|---|
| Bucket count, Bucket size, Bucket offset | bucket_count, bucket_size, bucket_offset |
| Combine series | combine |
| Fill opacity, Gradient mode, Line width | fill_opacity (0.0–1.0), gradient, line_width |
| Stack series | stacking |
| Legend, Tooltip | legend, tooltip |
| Hide in area | hide_from |
xMin / xMax frames | x_min / x_max, or a Frame with those field names |
Not available here: Prometheus le histograms (use <Heatmap> with BucketLayout::Le), units and log-scale bucket axes.
Run them
13 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
- 01Find the tallest bar, the mode, and ask whether it is the only one. A second hump means two populations.
- 02Look at the tails. A long right tail means a few large values pull the mean away from the typical one.
- 03Check the bucket width before you read anything into the bar heights. Different widths make heights incomparable.
- 04Hover a bar for its range and count. With several series, overlap and stacking behave differently, so check which one you are looking at.
When to use it, and when not to
Reach for it when
- You want the shape of one numeric variable: spread, skew, modes, outliers.
- You are comparing two groups and want to see whether their shapes differ, not only their means.
- You are deciding how to summarise. A long tail argues for percentiles. Two humps argue for splitting the data first.
- You have raw samples (request latency, file sizes, test scores) or counts already in buckets from a metrics system.
Skip it when
- You want how the distribution changes over time. A Heatmap with
calculatestacks one histogram per time bucket. - You have very few values. Twelve values in thirty buckets is a drawing of twelve bars.
- You need to compare each value against another variable. XY Chart.
- You want precise quantiles. A histogram shows them roughly. Compute them.
The math behind it
Binning
Given values , a bucket width and an offset (zero by default), the bucket of a value is identified by its lower edge, and the bucket covers the half-open interval from that edge up to the edge plus . The count of a bucket is the number of values whose lower edge matches it.
Only buckets that contain at least one value are produced, so gaps in the data show as gaps. Lower edges are rounded to nine decimals to keep floating-point crumbs out of the bucket identity, which stops 0.30000000000000004 from becoming a bucket of its own.
Choosing the width
When you give neither bucket_size nor bucket_count, the chart aims for buckets. It computes the target width as the range of all values divided by , and then picks the smallest width from a ladder of round numbers (1, 2, 2.5, 4, 5 times a power of ten, from to ) that is larger than the target and also no smaller than the smallest gap between distinct values.
The second condition is what keeps integer data tidy: if your values are whole numbers, the bucket will never be narrower than 1, so no bucket is mysteriously empty. bucket_offset set to half the width centres buckets on round numbers.
Several series and pre-counted data
With several series, values from all of them are bucketed with the same width and offset, so bars line up. Each bucket row then has one count per series. stacking stacks them to show the total and the parts, combine={true} merges everything into one count, and an overlay with lower fill_opacity compares shapes.
If your data is already counted, you pass the lower and upper edges and a count. A missing edge is guessed from the spacing of the other one, and non-finite counts are treated as zero.
Real life usage
The exam that had two peaks
Oksana taught introductory statistics at a technical college in Lviv, and every term she graded the final exam, computed the mean, and posted it. Sixty-one out of a hundred, again, which felt right and told her nothing at all.
This term the head of department asked her why a third of the class had still failed, given that the average looked healthy. It was a reasonable question, and she had no answer, because she had never actually looked at the scores. She had looked at their mean, which is a different thing.
She exported the 214 results into a spreadsheet and plotted them with zopra-charts, using <Histogram> with a bucket size of five points and a single series. She expected a hill with a hump at sixty. She wanted to be right, in the same way that everyone wants to be right.
The chart had two humps. One at about 40 and one at about 80, with a valley between them at 60 that was very nearly empty. Sixty-one was the average of two groups, and almost nobody had actually scored sixty-one.
She split the data by which tutorial group the student had attended. The two humps separated cleanly: a section that had been taught by a substitute for the middle four weeks had scored low, and the rest high. The lecturer on leave had covered conditional probability, which is the one topic on the exam that is entirely made of ideas you only get once.
Oksana ran two revision sessions for that section on conditional probability. At the retake, the low hump had moved from 40 to 66 and the valley had filled in.
She now plots the histogram before she computes anything else. The mean, she tells her students, is the number you get when you ask a crowd to stand in the middle of a room, and nobody in the room is standing there.
Pitfalls
- Bucket width changes the story. Try a coarser and a finer width before you conclude there is a gap or a second hump.
- Unequal widths. Pre-counted buckets of different sizes make bar heights misleading. Count per unit width or use equal buckets.
- Overlaying more than two series. Overlapping bars turn to mud. Stack them, or compare two at a time.
- Integers and half-open buckets. Edges belong to the bucket above, so a value of exactly 10 falls in 10 to 15, not 5 to 10.
- Treating the histogram as a density. Bars show counts, not probabilities, so the totals depend on how much data you have.
From around the web