Skip to main content

dear_implot/plots/
histogram.rs

1//! Histogram plot implementation
2
3use super::{
4    Plot, PlotDataLayout, PlotError, PlotItemStyle, plot_spec_with_style, with_plot_str_or_empty,
5};
6use crate::sys;
7use crate::{HistogramBins, HistogramFlags, ItemFlags};
8
9/// Builder for 1D histogram plots
10pub struct HistogramPlot<'a> {
11    label: &'a str,
12    values: &'a [f64],
13    style: PlotItemStyle,
14    bins: HistogramBins,
15    bar_scale: f64,
16    range: Option<sys::ImPlotRange>,
17    flags: HistogramFlags,
18    item_flags: ItemFlags,
19}
20
21impl<'a> super::PlotItemStyled for HistogramPlot<'a> {
22    fn style_mut(&mut self) -> &mut PlotItemStyle {
23        &mut self.style
24    }
25}
26
27impl<'a> HistogramPlot<'a> {
28    /// Create a new histogram plot with the given label and data
29    pub fn new(label: &'a str, values: &'a [f64]) -> Self {
30        Self {
31            label,
32            values,
33            style: PlotItemStyle::default(),
34            bins: HistogramBins::DEFAULT,
35            bar_scale: 1.0,
36            range: None, // Auto-range
37            flags: HistogramFlags::NONE,
38            item_flags: ItemFlags::NONE,
39        }
40    }
41
42    /// Set a concrete positive bin count or automatic binning method.
43    pub fn with_bins(mut self, bins: impl Into<HistogramBins>) -> Self {
44        self.bins = bins.into();
45        self
46    }
47
48    /// Set the bar scale factor
49    pub fn with_bar_scale(mut self, scale: f64) -> Self {
50        self.bar_scale = scale;
51        self
52    }
53
54    /// Set the data range for binning
55    /// Values outside this range will be treated as outliers
56    pub fn with_range(mut self, min: f64, max: f64) -> Self {
57        self.range = Some(sys::ImPlotRange { Min: min, Max: max });
58        self
59    }
60
61    /// Set the data range using ImPlotRange
62    pub fn with_range_struct(mut self, range: sys::ImPlotRange) -> Self {
63        self.range = Some(range);
64        self
65    }
66
67    /// Set histogram flags for customization
68    pub fn with_flags(mut self, flags: HistogramFlags) -> Self {
69        self.flags = flags;
70        self
71    }
72
73    /// Set common item flags for this plot item (applies to all plot types)
74    pub fn with_item_flags(mut self, flags: ItemFlags) -> Self {
75        self.item_flags = flags;
76        self
77    }
78
79    /// Make the histogram horizontal instead of vertical
80    pub fn horizontal(mut self) -> Self {
81        self.flags |= HistogramFlags::HORIZONTAL;
82        self
83    }
84
85    /// Make the histogram cumulative
86    pub fn cumulative(mut self) -> Self {
87        self.flags |= HistogramFlags::CUMULATIVE;
88        self
89    }
90
91    /// Normalize the histogram to show density (PDF)
92    pub fn density(mut self) -> Self {
93        self.flags |= HistogramFlags::DENSITY;
94        self
95    }
96
97    /// Exclude outliers from normalization
98    pub fn no_outliers(mut self) -> Self {
99        self.flags |= HistogramFlags::NO_OUTLIERS;
100        self
101    }
102
103    /// Validate the plot data
104    pub fn validate(&self) -> Result<(), PlotError> {
105        if self.values.is_empty() {
106            return Err(PlotError::EmptyData);
107        }
108        Ok(())
109    }
110}
111
112impl<'a> Plot for HistogramPlot<'a> {
113    fn plot(&self) {
114        if self.validate().is_err() {
115            return;
116        }
117        let Ok(count) = i32::try_from(self.values.len()) else {
118            return;
119        };
120
121        let range = if let Some(range) = &self.range {
122            *range
123        } else {
124            sys::ImPlotRange { Min: 0.0, Max: 0.0 }
125        };
126
127        with_plot_str_or_empty(self.label, |label_ptr| unsafe {
128            let spec = plot_spec_with_style(
129                self.style,
130                self.flags.bits() | self.item_flags.bits(),
131                PlotDataLayout::DEFAULT,
132            );
133            sys::ImPlot_PlotHistogram_doublePtr(
134                label_ptr,
135                self.values.as_ptr(),
136                count,
137                self.bins.raw("HistogramPlot::plot()"),
138                self.bar_scale,
139                range,
140                spec,
141            );
142        })
143    }
144
145    fn label(&self) -> &str {
146        self.label
147    }
148}
149
150/// Builder for 2D histogram plots (bivariate histograms as heatmaps)
151pub struct Histogram2DPlot<'a> {
152    label: &'a str,
153    x_values: &'a [f64],
154    y_values: &'a [f64],
155    style: PlotItemStyle,
156    x_bins: HistogramBins,
157    y_bins: HistogramBins,
158    range: Option<sys::ImPlotRect>,
159    flags: HistogramFlags,
160    item_flags: ItemFlags,
161}
162
163impl<'a> super::PlotItemStyled for Histogram2DPlot<'a> {
164    fn style_mut(&mut self) -> &mut PlotItemStyle {
165        &mut self.style
166    }
167}
168
169impl<'a> Histogram2DPlot<'a> {
170    /// Create a new 2D histogram plot with the given label and data
171    pub fn new(label: &'a str, x_values: &'a [f64], y_values: &'a [f64]) -> Self {
172        Self {
173            label,
174            x_values,
175            y_values,
176            style: PlotItemStyle::default(),
177            x_bins: HistogramBins::DEFAULT,
178            y_bins: HistogramBins::DEFAULT,
179            range: None, // Auto-range
180            flags: HistogramFlags::NONE,
181            item_flags: ItemFlags::NONE,
182        }
183    }
184
185    /// Set the number of bins for both X and Y axes
186    pub fn with_bins(
187        mut self,
188        x_bins: impl Into<HistogramBins>,
189        y_bins: impl Into<HistogramBins>,
190    ) -> Self {
191        self.x_bins = x_bins.into();
192        self.y_bins = y_bins.into();
193        self
194    }
195
196    /// Set the data range for binning
197    pub fn with_range(mut self, x_min: f64, x_max: f64, y_min: f64, y_max: f64) -> Self {
198        self.range = Some(sys::ImPlotRect {
199            X: sys::ImPlotRange {
200                Min: x_min,
201                Max: x_max,
202            },
203            Y: sys::ImPlotRange {
204                Min: y_min,
205                Max: y_max,
206            },
207        });
208        self
209    }
210
211    /// Set the data range using ImPlotRect
212    pub fn with_range_struct(mut self, range: sys::ImPlotRect) -> Self {
213        self.range = Some(range);
214        self
215    }
216
217    /// Set histogram flags for customization
218    pub fn with_flags(mut self, flags: HistogramFlags) -> Self {
219        self.flags = flags;
220        self
221    }
222
223    /// Set common item flags for this plot item (applies to all plot types)
224    pub fn with_item_flags(mut self, flags: ItemFlags) -> Self {
225        self.item_flags = flags;
226        self
227    }
228
229    /// Normalize the histogram to show density
230    pub fn density(mut self) -> Self {
231        self.flags |= HistogramFlags::DENSITY;
232        self
233    }
234
235    /// Exclude outliers from normalization
236    pub fn no_outliers(mut self) -> Self {
237        self.flags |= HistogramFlags::NO_OUTLIERS;
238        self
239    }
240
241    /// Use column-major data ordering
242    pub fn column_major(mut self) -> Self {
243        self.flags |= HistogramFlags::COL_MAJOR;
244        self
245    }
246
247    /// Validate the plot data
248    pub fn validate(&self) -> Result<(), PlotError> {
249        super::validate_data_lengths(self.x_values, self.y_values)
250    }
251}
252
253impl<'a> Plot for Histogram2DPlot<'a> {
254    fn plot(&self) {
255        if self.validate().is_err() {
256            return;
257        }
258        let Ok(count) = i32::try_from(self.x_values.len()) else {
259            return;
260        };
261
262        let range = if let Some(range) = &self.range {
263            *range
264        } else {
265            sys::ImPlotRect {
266                X: sys::ImPlotRange { Min: 0.0, Max: 0.0 },
267                Y: sys::ImPlotRange { Min: 0.0, Max: 0.0 },
268            }
269        };
270
271        with_plot_str_or_empty(self.label, |label_ptr| unsafe {
272            let spec = plot_spec_with_style(
273                self.style,
274                self.flags.bits() | self.item_flags.bits(),
275                PlotDataLayout::DEFAULT,
276            );
277            sys::ImPlot_PlotHistogram2D_doublePtr(
278                label_ptr,
279                self.x_values.as_ptr(),
280                self.y_values.as_ptr(),
281                count,
282                self.x_bins.raw("Histogram2DPlot::plot()"),
283                self.y_bins.raw("Histogram2DPlot::plot()"),
284                range,
285                spec,
286            );
287        })
288    }
289
290    fn label(&self) -> &str {
291        self.label
292    }
293}
294
295/// Convenience functions for quick histogram plotting
296impl<'ui> crate::PlotUi<'ui> {
297    /// Plot a 1D histogram with default settings
298    pub fn histogram_plot(&self, label: &str, values: &[f64]) -> Result<(), PlotError> {
299        let plot = HistogramPlot::new(label, values);
300        plot.validate()?;
301        self.bind();
302        plot.plot();
303        Ok(())
304    }
305
306    /// Plot a 1D histogram with custom bin count
307    pub fn histogram_plot_with_bins(
308        &self,
309        label: &str,
310        values: &[f64],
311        bins: impl Into<HistogramBins>,
312    ) -> Result<(), PlotError> {
313        let plot = HistogramPlot::new(label, values).with_bins(bins);
314        plot.validate()?;
315        self.bind();
316        plot.plot();
317        Ok(())
318    }
319
320    /// Plot a 2D histogram (bivariate histogram as heatmap)
321    pub fn histogram_2d_plot(
322        &self,
323        label: &str,
324        x_values: &[f64],
325        y_values: &[f64],
326    ) -> Result<(), PlotError> {
327        let plot = Histogram2DPlot::new(label, x_values, y_values);
328        plot.validate()?;
329        self.bind();
330        plot.plot();
331        Ok(())
332    }
333
334    /// Plot a 2D histogram with custom bin counts
335    pub fn histogram_2d_plot_with_bins(
336        &self,
337        label: &str,
338        x_values: &[f64],
339        y_values: &[f64],
340        x_bins: impl Into<HistogramBins>,
341        y_bins: impl Into<HistogramBins>,
342    ) -> Result<(), PlotError> {
343        let plot = Histogram2DPlot::new(label, x_values, y_values).with_bins(x_bins, y_bins);
344        plot.validate()?;
345        self.bind();
346        plot.plot();
347        Ok(())
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::{Histogram2DPlot, HistogramPlot};
354    use crate::{BinMethod, HistogramBins};
355
356    #[test]
357    fn histogram_bins_distinguish_counts_from_methods() {
358        assert_eq!(HistogramBins::from(8usize).raw("test"), 8);
359        assert_eq!(
360            HistogramBins::from(BinMethod::Rice).raw("test"),
361            BinMethod::Rice as i32
362        );
363        assert_eq!(
364            HistogramBins::DEFAULT.raw("test"),
365            BinMethod::Sturges as i32
366        );
367    }
368
369    #[test]
370    #[should_panic(expected = "test bin count must be positive")]
371    fn histogram_bins_reject_zero_counts_before_ffi() {
372        let _ = HistogramBins::from(0usize).raw("test");
373    }
374
375    #[test]
376    #[should_panic(expected = "test bin count exceeded ImPlot's i32 range")]
377    fn histogram_bins_reject_oversized_counts_before_ffi() {
378        let _ = HistogramBins::from(i32::MAX as usize + 1).raw("test");
379    }
380
381    #[test]
382    fn histogram_builders_accept_typed_bins() {
383        let values = [1.0, 2.0, 3.0];
384        let _ = HistogramPlot::new("hist", &values).with_bins(8usize);
385        let _ = HistogramPlot::new("hist", &values).with_bins(BinMethod::Scott);
386        let _ = Histogram2DPlot::new("hist2d", &values, &values).with_bins(4usize, BinMethod::Rice);
387    }
388}