1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
/// Builder for a histogram.
///
/// Bins a 1-D dataset and renders each bin as a vertical bar. The bin
/// boundaries are computed from the data range (or an explicit range)
/// and the requested bin count.
///
/// # Example
///
/// ```rust,no_run
/// use kuva::plot::Histogram;
/// use kuva::backend::svg::SvgBackend;
/// use kuva::render::render::render_multiple;
/// use kuva::render::layout::Layout;
/// use kuva::render::plots::Plot;
///
/// let data = vec![1.1, 2.3, 2.7, 3.2, 3.8, 3.9, 4.0, 1.5, 2.1, 3.5];
///
/// let hist = Histogram::new()
/// .with_data(data)
/// .with_bins(10)
/// .with_color("steelblue");
///
/// let plots = vec![Plot::Histogram(hist)];
/// let layout = Layout::auto_from_plots(&plots)
/// .with_title("Histogram")
/// .with_x_label("Value")
/// .with_y_label("Count");
///
/// let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
/// std::fs::write("histogram.svg", svg).unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct Histogram {
pub data: Vec<f64>,
pub bins: usize,
pub range: Option<(f64, f64)>,
pub color: String,
pub normalize: bool,
pub legend_label: Option<String>,
pub precomputed: Option<(Vec<f64>, Vec<f64>)>,
pub show_tooltips: bool,
pub tooltip_labels: Option<Vec<String>>,
}
impl Default for Histogram {
fn default() -> Self { Self::new() }
}
impl Histogram {
/// Create a histogram with default settings.
///
/// Defaults: 10 bins, color `"black"`, no normalization.
pub fn new() -> Self {
Self {
data: vec![],
bins: 10,
range: None,
color: "black".to_string(),
normalize: false,
legend_label: None,
precomputed: None,
show_tooltips: false,
tooltip_labels: None,
}
}
/// Create a histogram from precomputed bin edges and counts.
///
/// `edges` must have length `counts.len() + 1`. Use `f64` counts to support
/// fractional values (density estimates, normalized inputs from R/numpy).
/// `range` and `with_data` / `with_bins` are ignored when precomputed bins are set.
///
/// ```rust,no_run
/// # use kuva::plot::Histogram;
/// let edges = vec![0.0, 1.0, 2.0, 3.0];
/// let counts = vec![5.0, 12.0, 8.0];
/// let hist = Histogram::from_bins(edges, counts).with_color("steelblue");
/// ```
pub fn from_bins(edges: Vec<f64>, counts: Vec<f64>) -> Self {
Self {
precomputed: Some((edges, counts)),
..Self::new()
}
}
/// Set precomputed bin edges and counts via the builder chain.
///
/// Equivalent to `Histogram::from_bins(edges, counts)` but usable when
/// constructing conditionally after other options are set.
pub fn with_precomputed(mut self, edges: Vec<f64>, counts: Vec<f64>) -> Self {
self.precomputed = Some((edges, counts));
self
}
/// Set the input data.
///
/// Accepts any iterator of values implementing `Into<f64>`. Values
/// outside the active range are silently ignored.
///
/// > **Note:** [`with_range`](Self::with_range) must also be called.
/// > Without an explicit range, [`Layout::auto_from_plots`](crate::render::layout::Layout::auto_from_plots)
/// > cannot determine the axis extent and the chart will be empty.
///
/// ```rust,no_run
/// # use kuva::plot::Histogram;
/// let data = vec![1.1, 2.3, 2.7, 3.2, 3.8];
/// let min = data.iter().cloned().fold(f64::INFINITY, f64::min);
/// let max = data.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
/// let hist = Histogram::new()
/// .with_data(data)
/// .with_range((min, max));
/// ```
pub fn with_data<T, I>(mut self, data: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<f64>,
{
self.data = data.into_iter().map(|x| x.into()).collect();
self
}
/// Set the number of equal-width bins (default `10`).
///
/// The bin edges span from `range.min` to `range.max`. Choose a
/// value that balances resolution against noise for your sample size.
pub fn with_bins(mut self, bins: usize) -> Self {
self.bins = bins;
self
}
/// Set the bin range — **required** for `Layout::auto_from_plots` to work.
///
/// Without an explicit range, `bounds()` returns `None` and
/// [`Layout::auto_from_plots`](crate::render::layout::Layout::auto_from_plots)
/// cannot determine the axis extent, resulting in an empty chart.
///
/// Typically pass the data min/max. For overlapping histograms, pass the
/// same combined range to both so their x-axes align.
///
/// Values outside the range are silently ignored during binning.
///
/// ```rust,no_run
/// # use kuva::plot::Histogram;
/// let data = vec![0.1, 0.5, 1.2, 2.8, 3.0];
/// let min = data.iter().cloned().fold(f64::INFINITY, f64::min);
/// let max = data.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
/// let hist = Histogram::new()
/// .with_data(data)
/// .with_range((min, max));
/// ```
pub fn with_range(mut self, range: (f64, f64)) -> Self {
self.range = Some(range);
self
}
/// Set the bar fill color (CSS color string, e.g. `"steelblue"`, `"#4682b4"`).
///
/// For overlapping histograms, use an 8-digit hex color with an alpha
/// channel (`#RRGGBBAA`) so bars from different series show through:
///
/// ```rust,no_run
/// # use kuva::plot::Histogram;
/// let hist = Histogram::new()
/// .with_data(vec![1.0, 2.0, 3.0])
/// .with_color("#4682b480"); // steelblue at 50% opacity
/// ```
pub fn with_color<S: Into<String>>(mut self, color: S) -> Self {
self.color = color.into();
self
}
/// Normalize bar heights so the tallest bar equals `1.0`.
///
/// This is a peak-normalization — not a probability density. The
/// y-axis represents relative frequency (tallest bin = 1), not
/// counts or probability per unit width.
pub fn with_normalize(mut self) -> Self {
self.normalize = true;
self
}
/// Attach a legend label to this histogram.
///
/// A legend is rendered automatically when at least one plot in the
/// `Vec<Plot>` has a label.
pub fn with_legend<S: Into<String>>(mut self, label: S) -> Self {
self.legend_label = Some(label.into());
self
}
pub fn with_tooltips(mut self) -> Self {
self.show_tooltips = true;
self
}
pub fn with_tooltip_labels(mut self, labels: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.tooltip_labels = Some(labels.into_iter().map(|s| s.into()).collect());
self
}
}