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, plot_ui: &crate::PlotUi<'_>) {
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        let _guard = plot_ui.bind();
128        with_plot_str_or_empty(self.label, |label_ptr| unsafe {
129            let spec = plot_spec_with_style(
130                self.style,
131                self.flags.bits() | self.item_flags.bits(),
132                PlotDataLayout::DEFAULT,
133            );
134            sys::ImPlot_PlotHistogram_doublePtr(
135                label_ptr,
136                self.values.as_ptr(),
137                count,
138                self.bins.raw("HistogramPlot::plot()"),
139                self.bar_scale,
140                range,
141                spec,
142            );
143        })
144    }
145
146    fn label(&self) -> &str {
147        self.label
148    }
149}
150
151/// Builder for 2D histogram plots (bivariate histograms as heatmaps)
152pub struct Histogram2DPlot<'a> {
153    label: &'a str,
154    x_values: &'a [f64],
155    y_values: &'a [f64],
156    style: PlotItemStyle,
157    x_bins: HistogramBins,
158    y_bins: HistogramBins,
159    range: Option<sys::ImPlotRect>,
160    flags: HistogramFlags,
161    item_flags: ItemFlags,
162}
163
164impl<'a> super::PlotItemStyled for Histogram2DPlot<'a> {
165    fn style_mut(&mut self) -> &mut PlotItemStyle {
166        &mut self.style
167    }
168}
169
170impl<'a> Histogram2DPlot<'a> {
171    /// Create a new 2D histogram plot with the given label and data
172    pub fn new(label: &'a str, x_values: &'a [f64], y_values: &'a [f64]) -> Self {
173        Self {
174            label,
175            x_values,
176            y_values,
177            style: PlotItemStyle::default(),
178            x_bins: HistogramBins::DEFAULT,
179            y_bins: HistogramBins::DEFAULT,
180            range: None, // Auto-range
181            flags: HistogramFlags::NONE,
182            item_flags: ItemFlags::NONE,
183        }
184    }
185
186    /// Set the number of bins for both X and Y axes
187    pub fn with_bins(
188        mut self,
189        x_bins: impl Into<HistogramBins>,
190        y_bins: impl Into<HistogramBins>,
191    ) -> Self {
192        self.x_bins = x_bins.into();
193        self.y_bins = y_bins.into();
194        self
195    }
196
197    /// Set the data range for binning
198    pub fn with_range(mut self, x_min: f64, x_max: f64, y_min: f64, y_max: f64) -> Self {
199        self.range = Some(sys::ImPlotRect {
200            X: sys::ImPlotRange {
201                Min: x_min,
202                Max: x_max,
203            },
204            Y: sys::ImPlotRange {
205                Min: y_min,
206                Max: y_max,
207            },
208        });
209        self
210    }
211
212    /// Set the data range using ImPlotRect
213    pub fn with_range_struct(mut self, range: sys::ImPlotRect) -> Self {
214        self.range = Some(range);
215        self
216    }
217
218    /// Set histogram flags for customization
219    pub fn with_flags(mut self, flags: HistogramFlags) -> Self {
220        self.flags = flags;
221        self
222    }
223
224    /// Set common item flags for this plot item (applies to all plot types)
225    pub fn with_item_flags(mut self, flags: ItemFlags) -> Self {
226        self.item_flags = flags;
227        self
228    }
229
230    /// Normalize the histogram to show density
231    pub fn density(mut self) -> Self {
232        self.flags |= HistogramFlags::DENSITY;
233        self
234    }
235
236    /// Exclude outliers from normalization
237    pub fn no_outliers(mut self) -> Self {
238        self.flags |= HistogramFlags::NO_OUTLIERS;
239        self
240    }
241
242    /// Use column-major data ordering
243    pub fn column_major(mut self) -> Self {
244        self.flags |= HistogramFlags::COL_MAJOR;
245        self
246    }
247
248    /// Validate the plot data
249    pub fn validate(&self) -> Result<(), PlotError> {
250        super::validate_data_lengths(self.x_values, self.y_values)
251    }
252}
253
254impl<'a> Plot for Histogram2DPlot<'a> {
255    fn plot(&self, plot_ui: &crate::PlotUi<'_>) {
256        if self.validate().is_err() {
257            return;
258        }
259        let Ok(count) = i32::try_from(self.x_values.len()) else {
260            return;
261        };
262
263        let range = if let Some(range) = &self.range {
264            *range
265        } else {
266            sys::ImPlotRect {
267                X: sys::ImPlotRange { Min: 0.0, Max: 0.0 },
268                Y: sys::ImPlotRange { Min: 0.0, Max: 0.0 },
269            }
270        };
271
272        let _guard = plot_ui.bind();
273        with_plot_str_or_empty(self.label, |label_ptr| unsafe {
274            let spec = plot_spec_with_style(
275                self.style,
276                self.flags.bits() | self.item_flags.bits(),
277                PlotDataLayout::DEFAULT,
278            );
279            sys::ImPlot_PlotHistogram2D_doublePtr(
280                label_ptr,
281                self.x_values.as_ptr(),
282                self.y_values.as_ptr(),
283                count,
284                self.x_bins.raw("Histogram2DPlot::plot()"),
285                self.y_bins.raw("Histogram2DPlot::plot()"),
286                range,
287                spec,
288            );
289        })
290    }
291
292    fn label(&self) -> &str {
293        self.label
294    }
295}
296
297/// Convenience functions for quick histogram plotting
298impl<'ui> crate::PlotUi<'ui> {
299    /// Plot a 1D histogram with default settings
300    pub fn histogram_plot(&self, label: &str, values: &[f64]) -> Result<(), PlotError> {
301        let plot = HistogramPlot::new(label, values);
302        plot.validate()?;
303        plot.plot(self);
304        Ok(())
305    }
306
307    /// Plot a 1D histogram with custom bin count
308    pub fn histogram_plot_with_bins(
309        &self,
310        label: &str,
311        values: &[f64],
312        bins: impl Into<HistogramBins>,
313    ) -> Result<(), PlotError> {
314        let plot = HistogramPlot::new(label, values).with_bins(bins);
315        plot.validate()?;
316        plot.plot(self);
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        plot.plot(self);
330        Ok(())
331    }
332
333    /// Plot a 2D histogram with custom bin counts
334    pub fn histogram_2d_plot_with_bins(
335        &self,
336        label: &str,
337        x_values: &[f64],
338        y_values: &[f64],
339        x_bins: impl Into<HistogramBins>,
340        y_bins: impl Into<HistogramBins>,
341    ) -> Result<(), PlotError> {
342        let plot = Histogram2DPlot::new(label, x_values, y_values).with_bins(x_bins, y_bins);
343        plot.validate()?;
344        plot.plot(self);
345        Ok(())
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::{Histogram2DPlot, HistogramPlot};
352    use crate::{BinMethod, HistogramBins};
353
354    #[test]
355    fn histogram_bins_distinguish_counts_from_methods() {
356        assert_eq!(HistogramBins::from(8usize).raw("test"), 8);
357        assert_eq!(
358            HistogramBins::from(BinMethod::Rice).raw("test"),
359            BinMethod::Rice as i32
360        );
361        assert_eq!(
362            HistogramBins::DEFAULT.raw("test"),
363            BinMethod::Sturges as i32
364        );
365    }
366
367    #[test]
368    #[should_panic(expected = "test bin count must be positive")]
369    fn histogram_bins_reject_zero_counts_before_ffi() {
370        let _ = HistogramBins::from(0usize).raw("test");
371    }
372
373    #[test]
374    #[should_panic(expected = "test bin count exceeded ImPlot's i32 range")]
375    fn histogram_bins_reject_oversized_counts_before_ffi() {
376        let _ = HistogramBins::from(i32::MAX as usize + 1).raw("test");
377    }
378
379    #[test]
380    fn histogram_builders_accept_typed_bins() {
381        let values = [1.0, 2.0, 3.0];
382        let _ = HistogramPlot::new("hist", &values).with_bins(8usize);
383        let _ = HistogramPlot::new("hist", &values).with_bins(BinMethod::Scott);
384        let _ = Histogram2DPlot::new("hist2d", &values, &values).with_bins(4usize, BinMethod::Rice);
385    }
386}