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;
10
11fn validate_size(caller: &str, size: [f32; 2]) -> Result<(), PlotError> {
12    if size[0].is_finite() && size[1].is_finite() {
13        Ok(())
14    } else {
15        Err(PlotError::InvalidData(format!(
16            "{caller} size must be finite"
17        )))
18    }
19}
20
21fn validate_positive_count(caller: &str, name: &str, value: i32) -> Result<(), PlotError> {
22    if value > 0 {
23        Ok(())
24    } else {
25        Err(PlotError::InvalidData(format!(
26            "{caller} {name} must be positive"
27        )))
28    }
29}
30
31fn validate_ratios(caller: &str, name: &str, ratios: &[f32]) -> Result<(), PlotError> {
32    if ratios.iter().all(|value| value.is_finite() && *value > 0.0) {
33        Ok(())
34    } else {
35        Err(PlotError::InvalidData(format!(
36            "{caller} {name} must contain only positive finite values"
37        )))
38    }
39}
40
41fn validate_range(caller: &str, min: f64, max: f64) -> Result<(), PlotError> {
42    if min.is_finite() && max.is_finite() && min != max {
43        Ok(())
44    } else {
45        Err(PlotError::InvalidData(format!(
46            "{caller} range values must be finite and distinct"
47        )))
48    }
49}
50
51/// Multi-plot layout manager for creating subplot grids
52pub struct SubplotGrid<'a> {
53    title: &'a str,
54    rows: i32,
55    cols: i32,
56    size: Option<[f32; 2]>,
57    flags: SubplotFlags,
58    row_ratios: Option<Vec<f32>>,
59    col_ratios: Option<Vec<f32>>,
60}
61
62bitflags::bitflags! {
63    /// Flags for subplot configuration
64    pub struct SubplotFlags: u32 {
65        const NONE = 0;
66        const NO_TITLE = 1 << 0;
67        const NO_RESIZE = 1 << 1;
68        const NO_ALIGN = 1 << 2;
69        const SHARE_ITEMS = 1 << 3;
70        const LINK_ROWS = 1 << 4;
71        const LINK_COLS = 1 << 5;
72        const LINK_ALL_X = 1 << 6;
73        const LINK_ALL_Y = 1 << 7;
74        const COLUMN_MAJOR = 1 << 8;
75    }
76}
77
78impl<'a> SubplotGrid<'a> {
79    /// Create a new subplot grid
80    pub fn new(title: &'a str, rows: i32, cols: i32) -> Self {
81        Self {
82            title,
83            rows,
84            cols,
85            size: None,
86            flags: SubplotFlags::NONE,
87            row_ratios: None,
88            col_ratios: None,
89        }
90    }
91
92    /// Set the size of the subplot grid
93    pub fn with_size(mut self, size: [f32; 2]) -> Self {
94        self.size = Some(size);
95        self
96    }
97
98    /// Set subplot flags
99    pub fn with_flags(mut self, flags: SubplotFlags) -> Self {
100        self.flags = flags;
101        self
102    }
103
104    /// Set row height ratios
105    pub fn with_row_ratios(mut self, ratios: &[f32]) -> Self {
106        self.row_ratios = if ratios.is_empty() {
107            None
108        } else {
109            Some(ratios.to_vec())
110        };
111        self
112    }
113
114    /// Set column width ratios
115    pub fn with_col_ratios(mut self, ratios: &[f32]) -> Self {
116        self.col_ratios = if ratios.is_empty() {
117            None
118        } else {
119            Some(ratios.to_vec())
120        };
121        self
122    }
123
124    /// Begin the subplot grid and return a token
125    pub fn begin(self) -> Result<SubplotToken<'a>, PlotError> {
126        validate_positive_count("SubplotGrid::begin()", "rows", self.rows)?;
127        validate_positive_count("SubplotGrid::begin()", "cols", self.cols)?;
128        let title_cstr =
129            CString::new(self.title).map_err(|e| PlotError::StringConversion(e.to_string()))?;
130
131        let size = self.size.unwrap_or([-1.0, -1.0]);
132        validate_size("SubplotGrid::begin()", size)?;
133        let size_vec = sys::ImVec2_c {
134            x: size[0],
135            y: size[1],
136        };
137
138        // The C API takes `float*` for ratios. Keep owned copies alive in the token to avoid
139        // casting away constness and to stay sound even if the backend ever writes to them.
140        let mut row_ratios = self.row_ratios;
141        let mut col_ratios = self.col_ratios;
142        if let Some(row_ratios) = &row_ratios {
143            let rows = usize::try_from(self.rows).map_err(|_| {
144                PlotError::InvalidData("SubplotGrid::begin() rows out of range".to_string())
145            })?;
146            if row_ratios.len() != rows {
147                return Err(PlotError::InvalidData(format!(
148                    "SubplotGrid::begin() row_ratios length must equal rows ({rows})"
149                )));
150            }
151            validate_ratios("SubplotGrid::begin()", "row_ratios", row_ratios)?;
152        }
153        if let Some(col_ratios) = &col_ratios {
154            let cols = usize::try_from(self.cols).map_err(|_| {
155                PlotError::InvalidData("SubplotGrid::begin() cols out of range".to_string())
156            })?;
157            if col_ratios.len() != cols {
158                return Err(PlotError::InvalidData(format!(
159                    "SubplotGrid::begin() col_ratios length must equal cols ({cols})"
160                )));
161            }
162            validate_ratios("SubplotGrid::begin()", "col_ratios", col_ratios)?;
163        }
164        let row_ratios_ptr = row_ratios
165            .as_mut()
166            .map(|r| r.as_mut_ptr())
167            .unwrap_or(std::ptr::null_mut());
168        let col_ratios_ptr = col_ratios
169            .as_mut()
170            .map(|c| c.as_mut_ptr())
171            .unwrap_or(std::ptr::null_mut());
172
173        let success = unsafe {
174            sys::ImPlot_BeginSubplots(
175                title_cstr.as_ptr(),
176                self.rows,
177                self.cols,
178                size_vec,
179                self.flags.bits() as i32,
180                row_ratios_ptr,
181                col_ratios_ptr,
182            )
183        };
184
185        if success {
186            Ok(SubplotToken {
187                _title: title_cstr,
188                _row_ratios: row_ratios,
189                _col_ratios: col_ratios,
190                _phantom: PhantomData,
191            })
192        } else {
193            Err(PlotError::PlotCreationFailed(
194                "Failed to begin subplots".to_string(),
195            ))
196        }
197    }
198}
199
200/// Token representing an active subplot grid
201pub struct SubplotToken<'a> {
202    _title: CString,
203    _row_ratios: Option<Vec<f32>>,
204    _col_ratios: Option<Vec<f32>>,
205    _phantom: PhantomData<&'a ()>,
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 { _label: label_cstr })
403        } else {
404            Err(PlotError::PlotCreationFailed(
405                "Failed to begin legend".to_string(),
406            ))
407        }
408    }
409}
410
411/// Legend location options (ImPlotLocation)
412#[repr(i32)]
413pub enum LegendLocation {
414    Center = sys::ImPlotLocation_Center as i32,
415    North = sys::ImPlotLocation_North as i32,
416    South = sys::ImPlotLocation_South as i32,
417    West = sys::ImPlotLocation_West as i32,
418    East = sys::ImPlotLocation_East as i32,
419    NorthWest = sys::ImPlotLocation_NorthWest as i32,
420    NorthEast = sys::ImPlotLocation_NorthEast as i32,
421    SouthWest = sys::ImPlotLocation_SouthWest as i32,
422    SouthEast = sys::ImPlotLocation_SouthEast as i32,
423}
424
425bitflags::bitflags! {
426    /// Flags for legend configuration (ImPlotLegendFlags)
427    pub struct LegendFlags: u32 {
428        const NONE              = sys::ImPlotLegendFlags_None as u32;
429        const NO_BUTTONS        = sys::ImPlotLegendFlags_NoButtons as u32;
430        const NO_HIGHLIGHT_ITEM = sys::ImPlotLegendFlags_NoHighlightItem as u32;
431        const NO_HIGHLIGHT_AXIS = sys::ImPlotLegendFlags_NoHighlightAxis as u32;
432        const NO_MENUS          = sys::ImPlotLegendFlags_NoMenus as u32;
433        const OUTSIDE           = sys::ImPlotLegendFlags_Outside as u32;
434        const HORIZONTAL        = sys::ImPlotLegendFlags_Horizontal as u32;
435        const SORT              = sys::ImPlotLegendFlags_Sort as u32;
436        // Note: ImPlotLegendFlags_Reverse is currently not exposed.
437    }
438}
439
440/// Token representing an active legend
441pub struct LegendToken {
442    _label: CString,
443}
444
445impl LegendToken {
446    /// End the legend
447    pub fn end(self) {
448        // The actual ending happens in Drop.
449    }
450}
451
452impl Drop for LegendToken {
453    fn drop(&mut self) {
454        unsafe {
455            sys::ImPlot_EndLegendPopup();
456        }
457    }
458}