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        plot_ui.with_bound_context(|| {
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
147    fn label(&self) -> &str {
148        self.label
149    }
150}
151
152/// Builder for 2D histogram plots (bivariate histograms as heatmaps)
153pub struct Histogram2DPlot<'a> {
154    label: &'a str,
155    x_values: &'a [f64],
156    y_values: &'a [f64],
157    style: PlotItemStyle,
158    x_bins: HistogramBins,
159    y_bins: HistogramBins,
160    range: Option<sys::ImPlotRect>,
161    flags: HistogramFlags,
162    item_flags: ItemFlags,
163}
164
165impl<'a> super::PlotItemStyled for Histogram2DPlot<'a> {
166    fn style_mut(&mut self) -> &mut PlotItemStyle {
167        &mut self.style
168    }
169}
170
171impl<'a> Histogram2DPlot<'a> {
172    /// Create a new 2D histogram plot with the given label and data
173    pub fn new(label: &'a str, x_values: &'a [f64], y_values: &'a [f64]) -> Self {
174        Self {
175            label,
176            x_values,
177            y_values,
178            style: PlotItemStyle::default(),
179            x_bins: HistogramBins::DEFAULT,
180            y_bins: HistogramBins::DEFAULT,
181            range: None, // Auto-range
182            flags: HistogramFlags::NONE,
183            item_flags: ItemFlags::NONE,
184        }
185    }
186
187    /// Set the number of bins for both X and Y axes
188    pub fn with_bins(
189        mut self,
190        x_bins: impl Into<HistogramBins>,
191        y_bins: impl Into<HistogramBins>,
192    ) -> Self {
193        self.x_bins = x_bins.into();
194        self.y_bins = y_bins.into();
195        self
196    }
197
198    /// Set the data range for binning
199    pub fn with_range(mut self, x_min: f64, x_max: f64, y_min: f64, y_max: f64) -> Self {
200        self.range = Some(sys::ImPlotRect {
201            X: sys::ImPlotRange {
202                Min: x_min,
203                Max: x_max,
204            },
205            Y: sys::ImPlotRange {
206                Min: y_min,
207                Max: y_max,
208            },
209        });
210        self
211    }
212
213    /// Set the data range using ImPlotRect
214    pub fn with_range_struct(mut self, range: sys::ImPlotRect) -> Self {
215        self.range = Some(range);
216        self
217    }
218
219    /// Set histogram flags for customization
220    pub fn with_flags(mut self, flags: HistogramFlags) -> Self {
221        self.flags = flags;
222        self
223    }
224
225    /// Set common item flags for this plot item (applies to all plot types)
226    pub fn with_item_flags(mut self, flags: ItemFlags) -> Self {
227        self.item_flags = flags;
228        self
229    }
230
231    /// Normalize the histogram to show density
232    pub fn density(mut self) -> Self {
233        self.flags |= HistogramFlags::DENSITY;
234        self
235    }
236
237    /// Exclude outliers from normalization
238    pub fn no_outliers(mut self) -> Self {
239        self.flags |= HistogramFlags::NO_OUTLIERS;
240        self
241    }
242
243    /// Use column-major data ordering
244    pub fn column_major(mut self) -> Self {
245        self.flags |= HistogramFlags::COL_MAJOR;
246        self
247    }
248
249    /// Validate the plot data
250    pub fn validate(&self) -> Result<(), PlotError> {
251        super::validate_data_lengths(self.x_values, self.y_values)
252    }
253}
254
255impl<'a> Plot for Histogram2DPlot<'a> {
256    fn plot(&self, plot_ui: &crate::PlotUi<'_>) {
257        if self.validate().is_err() {
258            return;
259        }
260        let Ok(count) = i32::try_from(self.x_values.len()) else {
261            return;
262        };
263
264        let range = if let Some(range) = &self.range {
265            *range
266        } else {
267            sys::ImPlotRect {
268                X: sys::ImPlotRange { Min: 0.0, Max: 0.0 },
269                Y: sys::ImPlotRange { Min: 0.0, Max: 0.0 },
270            }
271        };
272
273        plot_ui.with_bound_context(|| {
274            with_plot_str_or_empty(self.label, |label_ptr| unsafe {
275                let spec = plot_spec_with_style(
276                    self.style,
277                    self.flags.bits() | self.item_flags.bits(),
278                    PlotDataLayout::DEFAULT,
279                );
280                sys::ImPlot_PlotHistogram2D_doublePtr(
281                    label_ptr,
282                    self.x_values.as_ptr(),
283                    self.y_values.as_ptr(),
284                    count,
285                    self.x_bins.raw("Histogram2DPlot::plot()"),
286                    self.y_bins.raw("Histogram2DPlot::plot()"),
287                    range,
288                    spec,
289                );
290            })
291        })
292    }
293
294    fn label(&self) -> &str {
295        self.label
296    }
297}
298
299/// Convenience functions for quick histogram plotting
300impl<'ui> crate::PlotUi<'ui> {
301    /// Plot a 1D histogram with default settings
302    pub fn histogram_plot(&self, label: &str, values: &[f64]) -> Result<(), PlotError> {
303        let plot = HistogramPlot::new(label, values);
304        plot.validate()?;
305        plot.plot(self);
306        Ok(())
307    }
308
309    /// Plot a 1D histogram with custom bin count
310    pub fn histogram_plot_with_bins(
311        &self,
312        label: &str,
313        values: &[f64],
314        bins: impl Into<HistogramBins>,
315    ) -> Result<(), PlotError> {
316        let plot = HistogramPlot::new(label, values).with_bins(bins);
317        plot.validate()?;
318        plot.plot(self);
319        Ok(())
320    }
321
322    /// Plot a 2D histogram (bivariate histogram as heatmap)
323    pub fn histogram_2d_plot(
324        &self,
325        label: &str,
326        x_values: &[f64],
327        y_values: &[f64],
328    ) -> Result<(), PlotError> {
329        let plot = Histogram2DPlot::new(label, x_values, y_values);
330        plot.validate()?;
331        plot.plot(self);
332        Ok(())
333    }
334
335    /// Plot a 2D histogram with custom bin counts
336    pub fn histogram_2d_plot_with_bins(
337        &self,
338        label: &str,
339        x_values: &[f64],
340        y_values: &[f64],
341        x_bins: impl Into<HistogramBins>,
342        y_bins: impl Into<HistogramBins>,
343    ) -> Result<(), PlotError> {
344        let plot = Histogram2DPlot::new(label, x_values, y_values).with_bins(x_bins, y_bins);
345        plot.validate()?;
346        plot.plot(self);
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}