Skip to main content

dear_implot/
advanced.rs

1//! Advanced plotting features for complex visualizations
2//!
3//! This module provides high-level functionality for creating complex plots
4//! with multiple subplots, legends, and advanced layout management.
5
6use crate::context::PlotScopeGuard;
7use crate::{AxisFlags, YAxis, plots::PlotError, sys};
8use std::ffi::CString;
9use std::marker::PhantomData;
10use std::rc::Rc;
11
12fn validate_size(caller: &str, size: [f32; 2]) -> Result<(), PlotError> {
13    if size[0].is_finite() && size[1].is_finite() {
14        Ok(())
15    } else {
16        Err(PlotError::InvalidData(format!(
17            "{caller} size must be finite"
18        )))
19    }
20}
21
22fn count_to_i32(caller: &str, name: &str, value: usize) -> Result<i32, PlotError> {
23    if value == 0 {
24        return Err(PlotError::InvalidData(format!(
25            "{caller} {name} must be positive"
26        )));
27    }
28
29    i32::try_from(value)
30        .map_err(|_| PlotError::InvalidData(format!("{caller} {name} exceeded ImPlot's i32 range")))
31}
32
33fn validate_ratios(caller: &str, name: &str, ratios: &[f32]) -> Result<(), PlotError> {
34    if ratios.iter().all(|value| value.is_finite() && *value > 0.0) {
35        Ok(())
36    } else {
37        Err(PlotError::InvalidData(format!(
38            "{caller} {name} must contain only positive finite values"
39        )))
40    }
41}
42
43fn validate_range(caller: &str, min: f64, max: f64) -> Result<(), PlotError> {
44    if min.is_finite() && max.is_finite() && min != max {
45        Ok(())
46    } else {
47        Err(PlotError::InvalidData(format!(
48            "{caller} range values must be finite and distinct"
49        )))
50    }
51}
52
53/// Multi-plot layout manager for creating subplot grids
54pub struct SubplotGrid<'a> {
55    title: &'a str,
56    rows: usize,
57    cols: usize,
58    size: Option<[f32; 2]>,
59    flags: SubplotFlags,
60    row_ratios: Option<Vec<f32>>,
61    col_ratios: Option<Vec<f32>>,
62}
63
64bitflags::bitflags! {
65    /// Flags for subplot configuration
66    pub struct SubplotFlags: u32 {
67        const NONE = 0;
68        const NO_TITLE = 1 << 0;
69        const NO_RESIZE = 1 << 1;
70        const NO_ALIGN = 1 << 2;
71        const SHARE_ITEMS = 1 << 3;
72        const LINK_ROWS = 1 << 4;
73        const LINK_COLS = 1 << 5;
74        const LINK_ALL_X = 1 << 6;
75        const LINK_ALL_Y = 1 << 7;
76        const COLUMN_MAJOR = 1 << 8;
77    }
78}
79
80impl<'a> SubplotGrid<'a> {
81    /// Create a new subplot grid
82    pub fn new(title: &'a str, rows: usize, cols: usize) -> Self {
83        Self {
84            title,
85            rows,
86            cols,
87            size: None,
88            flags: SubplotFlags::NONE,
89            row_ratios: None,
90            col_ratios: None,
91        }
92    }
93
94    /// Set the size of the subplot grid
95    pub fn with_size(mut self, size: [f32; 2]) -> Self {
96        self.size = Some(size);
97        self
98    }
99
100    /// Set subplot flags
101    pub fn with_flags(mut self, flags: SubplotFlags) -> Self {
102        self.flags = flags;
103        self
104    }
105
106    /// Set row height ratios
107    pub fn with_row_ratios(mut self, ratios: &[f32]) -> Self {
108        self.row_ratios = if ratios.is_empty() {
109            None
110        } else {
111            Some(ratios.to_vec())
112        };
113        self
114    }
115
116    /// Set column width ratios
117    pub fn with_col_ratios(mut self, ratios: &[f32]) -> Self {
118        self.col_ratios = if ratios.is_empty() {
119            None
120        } else {
121            Some(ratios.to_vec())
122        };
123        self
124    }
125
126    /// Begin the subplot grid and return a token
127    pub fn begin(self) -> Result<SubplotToken<'a>, PlotError> {
128        let rows = count_to_i32("SubplotGrid::begin()", "rows", self.rows)?;
129        let cols = count_to_i32("SubplotGrid::begin()", "cols", self.cols)?;
130        let title_cstr =
131            CString::new(self.title).map_err(|e| PlotError::StringConversion(e.to_string()))?;
132
133        let size = self.size.unwrap_or([-1.0, -1.0]);
134        validate_size("SubplotGrid::begin()", size)?;
135        let size_vec = sys::ImVec2_c {
136            x: size[0],
137            y: size[1],
138        };
139
140        // The C API takes `float*` for ratios. Keep owned copies alive in the token to avoid
141        // casting away constness and to stay sound even if the backend ever writes to them.
142        let mut row_ratios = self.row_ratios;
143        let mut col_ratios = self.col_ratios;
144        if let Some(row_ratios) = &row_ratios {
145            if row_ratios.len() != self.rows {
146                return Err(PlotError::InvalidData(format!(
147                    "SubplotGrid::begin() row_ratios length must equal rows ({})",
148                    self.rows
149                )));
150            }
151            validate_ratios("SubplotGrid::begin()", "row_ratios", row_ratios)?;
152        }
153        if let Some(col_ratios) = &col_ratios {
154            if col_ratios.len() != self.cols {
155                return Err(PlotError::InvalidData(format!(
156                    "SubplotGrid::begin() col_ratios length must equal cols ({})",
157                    self.cols
158                )));
159            }
160            validate_ratios("SubplotGrid::begin()", "col_ratios", col_ratios)?;
161        }
162        let row_ratios_ptr = row_ratios
163            .as_mut()
164            .map(|r| r.as_mut_ptr())
165            .unwrap_or(std::ptr::null_mut());
166        let col_ratios_ptr = col_ratios
167            .as_mut()
168            .map(|c| c.as_mut_ptr())
169            .unwrap_or(std::ptr::null_mut());
170
171        let success = unsafe {
172            sys::ImPlot_BeginSubplots(
173                title_cstr.as_ptr(),
174                rows,
175                cols,
176                size_vec,
177                self.flags.bits() as i32,
178                row_ratios_ptr,
179                col_ratios_ptr,
180            )
181        };
182
183        if success {
184            Ok(SubplotToken {
185                _title: title_cstr,
186                _row_ratios: row_ratios,
187                _col_ratios: col_ratios,
188                _phantom: PhantomData,
189                _not_send_or_sync: PhantomData,
190            })
191        } else {
192            Err(PlotError::PlotCreationFailed(
193                "Failed to begin subplots".to_string(),
194            ))
195        }
196    }
197}
198
199/// Token representing an active subplot grid
200pub struct SubplotToken<'a> {
201    _title: CString,
202    _row_ratios: Option<Vec<f32>>,
203    _col_ratios: Option<Vec<f32>>,
204    _phantom: PhantomData<&'a ()>,
205    _not_send_or_sync: PhantomData<Rc<()>>,
206}
207
208impl<'a> SubplotToken<'a> {
209    /// End the subplot grid
210    pub fn end(self) {
211        // The actual ending happens in Drop.
212    }
213}
214
215impl<'a> Drop for SubplotToken<'a> {
216    fn drop(&mut self) {
217        unsafe {
218            sys::ImPlot_EndSubplots();
219        }
220    }
221}
222
223/// Multi-axis plot support
224pub struct MultiAxisPlot<'a> {
225    title: &'a str,
226    size: Option<[f32; 2]>,
227    y_axes: Vec<YAxisConfig<'a>>,
228}
229
230/// Configuration for a Y-axis
231pub struct YAxisConfig<'a> {
232    pub label: Option<&'a str>,
233    pub flags: AxisFlags,
234    pub range: Option<(f64, f64)>,
235}
236
237impl<'a> MultiAxisPlot<'a> {
238    /// Create a new multi-axis plot
239    pub fn new(title: &'a str) -> Self {
240        Self {
241            title,
242            size: None,
243            y_axes: Vec::new(),
244        }
245    }
246
247    /// Set the plot size
248    pub fn with_size(mut self, size: [f32; 2]) -> Self {
249        self.size = Some(size);
250        self
251    }
252
253    /// Add a Y-axis
254    pub fn add_y_axis(mut self, config: YAxisConfig<'a>) -> Self {
255        self.y_axes.push(config);
256        self
257    }
258
259    /// Begin the multi-axis plot
260    pub fn begin(self) -> Result<MultiAxisToken<'a>, PlotError> {
261        let title_cstr =
262            CString::new(self.title).map_err(|e| PlotError::StringConversion(e.to_string()))?;
263
264        for axis in &self.y_axes {
265            if let Some(label) = axis.label
266                && label.contains('\0')
267            {
268                return Err(PlotError::StringConversion(
269                    "Axis label contained an interior NUL byte".to_string(),
270                ));
271            }
272            if let Some((min, max)) = axis.range {
273                validate_range("MultiAxisPlot::begin()", min, max)?;
274            }
275        }
276        if self.y_axes.len() > 3 {
277            return Err(PlotError::InvalidData(
278                "MultiAxisPlot::begin() supports at most 3 Y axes".to_string(),
279            ));
280        }
281
282        let size = self.size.unwrap_or([-1.0, -1.0]);
283        validate_size("MultiAxisPlot::begin()", size)?;
284        let size_vec = sys::ImVec2_c {
285            x: size[0],
286            y: size[1],
287        };
288
289        let success = unsafe { sys::ImPlot_BeginPlot(title_cstr.as_ptr(), size_vec, 0) };
290
291        if success {
292            let mut axis_labels: Vec<CString> = Vec::new();
293
294            // Setup Y-axes (Y1..), matching `token.set_y_axis(YAxis::Y*)` convention.
295            for (i, axis_config) in self.y_axes.iter().enumerate() {
296                let label_ptr = if let Some(label) = axis_config.label {
297                    let cstr = CString::new(label)
298                        .map_err(|e| PlotError::StringConversion(e.to_string()))?;
299                    let ptr = cstr.as_ptr();
300                    axis_labels.push(cstr);
301                    ptr
302                } else {
303                    std::ptr::null()
304                };
305
306                unsafe {
307                    let axis_enum = (i as i32) + 3; // ImAxis_Y1 = 3
308                    sys::ImPlot_SetupAxis(axis_enum, label_ptr, axis_config.flags.bits() as i32);
309
310                    if let Some((min, max)) = axis_config.range {
311                        sys::ImPlot_SetupAxisLimits(axis_enum, min, max, 0);
312                    }
313                }
314            }
315
316            Ok(MultiAxisToken {
317                _title: title_cstr,
318                _axis_labels: axis_labels,
319                _scope: PlotScopeGuard::new(),
320                _phantom: PhantomData,
321            })
322        } else {
323            Err(PlotError::PlotCreationFailed(
324                "Failed to begin multi-axis plot".to_string(),
325            ))
326        }
327    }
328}
329
330/// Token representing an active multi-axis plot
331pub struct MultiAxisToken<'a> {
332    _title: CString,
333    _axis_labels: Vec<CString>,
334    _scope: PlotScopeGuard,
335    _phantom: PhantomData<&'a ()>,
336}
337
338impl<'a> MultiAxisToken<'a> {
339    /// Set the current Y-axis for subsequent plots
340    pub fn set_y_axis(&self, axis: YAxis) {
341        unsafe {
342            sys::ImPlot_SetAxes(
343                0, // ImAxis_X1
344                axis as i32,
345            );
346        }
347    }
348
349    /// Set the current raw Y-axis for subsequent plots.
350    ///
351    /// # Safety
352    ///
353    /// `axis` must be a valid ImPlot Y-axis value for the active plot. Passing an
354    /// out-of-range value lets ImPlot index internal axis arrays out of bounds.
355    pub unsafe fn set_y_axis_unchecked(&self, axis: sys::ImAxis) {
356        unsafe {
357            sys::ImPlot_SetAxes(
358                0, // ImAxis_X1
359                axis,
360            );
361        }
362    }
363
364    /// End the multi-axis plot
365    pub fn end(self) {
366        // The actual ending happens in Drop.
367    }
368}
369
370impl<'a> Drop for MultiAxisToken<'a> {
371    fn drop(&mut self) {
372        unsafe {
373            sys::ImPlot_EndPlot();
374        }
375    }
376}
377
378/// Legend management utilities
379pub struct LegendManager;
380
381impl LegendManager {
382    /// Setup legend with custom position and flags
383    pub fn setup(location: LegendLocation, flags: LegendFlags) {
384        unsafe {
385            sys::ImPlot_SetupLegend(location as i32, flags.bits() as i32);
386        }
387    }
388
389    /// Begin a custom legend
390    pub fn begin_custom(label: &str, _size: [f32; 2]) -> Result<LegendToken, PlotError> {
391        let label_cstr =
392            CString::new(label).map_err(|e| PlotError::StringConversion(e.to_string()))?;
393
394        let success = unsafe {
395            sys::ImPlot_BeginLegendPopup(
396                label_cstr.as_ptr(),
397                1, // mouse button
398            )
399        };
400
401        if success {
402            Ok(LegendToken {
403                _label: label_cstr,
404                _not_send_or_sync: PhantomData,
405            })
406        } else {
407            Err(PlotError::PlotCreationFailed(
408                "Failed to begin legend".to_string(),
409            ))
410        }
411    }
412}
413
414/// Legend location options (ImPlotLocation)
415#[repr(i32)]
416pub enum LegendLocation {
417    Center = sys::ImPlotLocation_Center as i32,
418    North = sys::ImPlotLocation_North as i32,
419    South = sys::ImPlotLocation_South as i32,
420    West = sys::ImPlotLocation_West as i32,
421    East = sys::ImPlotLocation_East as i32,
422    NorthWest = sys::ImPlotLocation_NorthWest as i32,
423    NorthEast = sys::ImPlotLocation_NorthEast as i32,
424    SouthWest = sys::ImPlotLocation_SouthWest as i32,
425    SouthEast = sys::ImPlotLocation_SouthEast as i32,
426}
427
428bitflags::bitflags! {
429    /// Flags for legend configuration (ImPlotLegendFlags)
430    pub struct LegendFlags: u32 {
431        const NONE              = sys::ImPlotLegendFlags_None as u32;
432        const NO_BUTTONS        = sys::ImPlotLegendFlags_NoButtons as u32;
433        const NO_HIGHLIGHT_ITEM = sys::ImPlotLegendFlags_NoHighlightItem as u32;
434        const NO_HIGHLIGHT_AXIS = sys::ImPlotLegendFlags_NoHighlightAxis as u32;
435        const NO_MENUS          = sys::ImPlotLegendFlags_NoMenus as u32;
436        const OUTSIDE           = sys::ImPlotLegendFlags_Outside as u32;
437        const HORIZONTAL        = sys::ImPlotLegendFlags_Horizontal as u32;
438        const SORT              = sys::ImPlotLegendFlags_Sort as u32;
439        // Note: ImPlotLegendFlags_Reverse is currently not exposed.
440    }
441}
442
443/// Token representing an active legend
444pub struct LegendToken {
445    _label: CString,
446    _not_send_or_sync: PhantomData<Rc<()>>,
447}
448
449impl LegendToken {
450    /// End the legend
451    pub fn end(self) {
452        // The actual ending happens in Drop.
453    }
454}
455
456impl Drop for LegendToken {
457    fn drop(&mut self) {
458        unsafe {
459            sys::ImPlot_EndLegendPopup();
460        }
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::{PlotError, SubplotGrid};
467    use crate::PlotContext;
468    use std::sync::{Mutex, OnceLock};
469
470    fn test_guard() -> std::sync::MutexGuard<'static, ()> {
471        static GUARD: OnceLock<Mutex<()>> = OnceLock::new();
472        GUARD.get_or_init(|| Mutex::new(())).lock().unwrap()
473    }
474
475    fn setup_context() -> (dear_imgui_rs::Context, PlotContext) {
476        let mut imgui = dear_imgui_rs::Context::create();
477        let _ = imgui.font_atlas_mut().build();
478        imgui.io_mut().set_display_size([256.0, 256.0]);
479        imgui.io_mut().set_delta_time(1.0 / 60.0);
480        let plot = PlotContext::create(&imgui);
481        (imgui, plot)
482    }
483
484    fn invalid_data_message(err: PlotError) -> String {
485        match err {
486            PlotError::InvalidData(message) => message,
487            other => panic!("expected invalid data error, got {other:?}"),
488        }
489    }
490
491    fn expect_invalid_data(result: Result<super::SubplotToken<'_>, PlotError>) -> String {
492        match result {
493            Err(err) => invalid_data_message(err),
494            Ok(_) => panic!("expected SubplotGrid::begin() to reject invalid input"),
495        }
496    }
497
498    #[test]
499    fn subplot_grid_rejects_invalid_counts_before_ffi() {
500        let _guard = test_guard();
501        let (mut imgui, _plot) = setup_context();
502        let _ui = imgui.frame();
503
504        let rows = expect_invalid_data(SubplotGrid::new("bad_rows", 0, 1).begin());
505        assert!(rows.contains("rows must be positive"));
506
507        let cols = expect_invalid_data(SubplotGrid::new("bad_cols", 1, 0).begin());
508        assert!(cols.contains("cols must be positive"));
509
510        let overflow = expect_invalid_data(
511            SubplotGrid::new("too_many_rows", i32::MAX as usize + 1, 1).begin(),
512        );
513        assert!(overflow.contains("rows exceeded"));
514    }
515
516    #[test]
517    fn subplot_grid_ratio_lengths_follow_usize_counts() {
518        let _guard = test_guard();
519        let (mut imgui, _plot) = setup_context();
520        let _ui = imgui.frame();
521
522        let err = expect_invalid_data(
523            SubplotGrid::new("bad_ratios", 2usize, 1usize)
524                .with_row_ratios(&[1.0])
525                .begin(),
526        );
527
528        assert!(err.contains("row_ratios length must equal rows (2)"));
529    }
530}