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, plots::PlotError, sys};
8use std::ffi::CString;
9use std::marker::PhantomData;
10
11/// Multi-plot layout manager for creating subplot grids
12pub struct SubplotGrid<'a> {
13    title: &'a str,
14    rows: i32,
15    cols: i32,
16    size: Option<[f32; 2]>,
17    flags: SubplotFlags,
18    row_ratios: Option<Vec<f32>>,
19    col_ratios: Option<Vec<f32>>,
20}
21
22bitflags::bitflags! {
23    /// Flags for subplot configuration
24    pub struct SubplotFlags: u32 {
25        const NONE = 0;
26        const NO_TITLE = 1 << 0;
27        const NO_RESIZE = 1 << 1;
28        const NO_ALIGN = 1 << 2;
29        const SHARE_ITEMS = 1 << 3;
30        const LINK_ROWS = 1 << 4;
31        const LINK_COLS = 1 << 5;
32        const LINK_ALL_X = 1 << 6;
33        const LINK_ALL_Y = 1 << 7;
34        const COLUMN_MAJOR = 1 << 8;
35    }
36}
37
38impl<'a> SubplotGrid<'a> {
39    /// Create a new subplot grid
40    pub fn new(title: &'a str, rows: i32, cols: i32) -> Self {
41        Self {
42            title,
43            rows,
44            cols,
45            size: None,
46            flags: SubplotFlags::NONE,
47            row_ratios: None,
48            col_ratios: None,
49        }
50    }
51
52    /// Set the size of the subplot grid
53    pub fn with_size(mut self, size: [f32; 2]) -> Self {
54        self.size = Some(size);
55        self
56    }
57
58    /// Set subplot flags
59    pub fn with_flags(mut self, flags: SubplotFlags) -> Self {
60        self.flags = flags;
61        self
62    }
63
64    /// Set row height ratios
65    pub fn with_row_ratios(mut self, ratios: &[f32]) -> Self {
66        self.row_ratios = if ratios.is_empty() {
67            None
68        } else {
69            Some(ratios.to_vec())
70        };
71        self
72    }
73
74    /// Set column width ratios
75    pub fn with_col_ratios(mut self, ratios: &[f32]) -> Self {
76        self.col_ratios = if ratios.is_empty() {
77            None
78        } else {
79            Some(ratios.to_vec())
80        };
81        self
82    }
83
84    /// Begin the subplot grid and return a token
85    pub fn begin(self) -> Result<SubplotToken<'a>, PlotError> {
86        let title_cstr =
87            CString::new(self.title).map_err(|e| PlotError::StringConversion(e.to_string()))?;
88
89        let size = self.size.unwrap_or([-1.0, -1.0]);
90        let size_vec = sys::ImVec2_c {
91            x: size[0],
92            y: size[1],
93        };
94
95        // The C API takes `float*` for ratios. Keep owned copies alive in the token to avoid
96        // casting away constness and to stay sound even if the backend ever writes to them.
97        let mut row_ratios = self.row_ratios;
98        let mut col_ratios = self.col_ratios;
99        let row_ratios_ptr = row_ratios
100            .as_mut()
101            .map(|r| r.as_mut_ptr())
102            .unwrap_or(std::ptr::null_mut());
103        let col_ratios_ptr = col_ratios
104            .as_mut()
105            .map(|c| c.as_mut_ptr())
106            .unwrap_or(std::ptr::null_mut());
107
108        let success = unsafe {
109            sys::ImPlot_BeginSubplots(
110                title_cstr.as_ptr(),
111                self.rows,
112                self.cols,
113                size_vec,
114                self.flags.bits() as i32,
115                row_ratios_ptr,
116                col_ratios_ptr,
117            )
118        };
119
120        if success {
121            Ok(SubplotToken {
122                _title: title_cstr,
123                _row_ratios: row_ratios,
124                _col_ratios: col_ratios,
125                _phantom: PhantomData,
126            })
127        } else {
128            Err(PlotError::PlotCreationFailed(
129                "Failed to begin subplots".to_string(),
130            ))
131        }
132    }
133}
134
135/// Token representing an active subplot grid
136pub struct SubplotToken<'a> {
137    _title: CString,
138    _row_ratios: Option<Vec<f32>>,
139    _col_ratios: Option<Vec<f32>>,
140    _phantom: PhantomData<&'a ()>,
141}
142
143impl<'a> SubplotToken<'a> {
144    /// End the subplot grid
145    pub fn end(self) {
146        // The actual ending happens in Drop.
147    }
148}
149
150impl<'a> Drop for SubplotToken<'a> {
151    fn drop(&mut self) {
152        unsafe {
153            sys::ImPlot_EndSubplots();
154        }
155    }
156}
157
158/// Multi-axis plot support
159pub struct MultiAxisPlot<'a> {
160    title: &'a str,
161    size: Option<[f32; 2]>,
162    y_axes: Vec<YAxisConfig<'a>>,
163}
164
165/// Configuration for a Y-axis
166pub struct YAxisConfig<'a> {
167    pub label: Option<&'a str>,
168    pub flags: AxisFlags,
169    pub range: Option<(f64, f64)>,
170}
171
172impl<'a> MultiAxisPlot<'a> {
173    /// Create a new multi-axis plot
174    pub fn new(title: &'a str) -> Self {
175        Self {
176            title,
177            size: None,
178            y_axes: Vec::new(),
179        }
180    }
181
182    /// Set the plot size
183    pub fn with_size(mut self, size: [f32; 2]) -> Self {
184        self.size = Some(size);
185        self
186    }
187
188    /// Add a Y-axis
189    pub fn add_y_axis(mut self, config: YAxisConfig<'a>) -> Self {
190        self.y_axes.push(config);
191        self
192    }
193
194    /// Begin the multi-axis plot
195    pub fn begin(self) -> Result<MultiAxisToken<'a>, PlotError> {
196        let title_cstr =
197            CString::new(self.title).map_err(|e| PlotError::StringConversion(e.to_string()))?;
198
199        for axis in &self.y_axes {
200            if let Some(label) = axis.label
201                && label.contains('\0')
202            {
203                return Err(PlotError::StringConversion(
204                    "Axis label contained an interior NUL byte".to_string(),
205                ));
206            }
207        }
208
209        let size = self.size.unwrap_or([-1.0, -1.0]);
210        let size_vec = sys::ImVec2_c {
211            x: size[0],
212            y: size[1],
213        };
214
215        let success = unsafe { sys::ImPlot_BeginPlot(title_cstr.as_ptr(), size_vec, 0) };
216
217        if success {
218            let mut axis_labels: Vec<CString> = Vec::new();
219
220            // Setup Y-axes (Y1..), matching `token.set_y_axis(0..)` convention.
221            for (i, axis_config) in self.y_axes.iter().enumerate() {
222                let label_ptr = if let Some(label) = axis_config.label {
223                    let cstr = CString::new(label)
224                        .map_err(|e| PlotError::StringConversion(e.to_string()))?;
225                    let ptr = cstr.as_ptr();
226                    axis_labels.push(cstr);
227                    ptr
228                } else {
229                    std::ptr::null()
230                };
231
232                unsafe {
233                    let axis_enum = (i as i32) + 3; // ImAxis_Y1 = 3
234                    sys::ImPlot_SetupAxis(axis_enum, label_ptr, axis_config.flags.bits() as i32);
235
236                    if let Some((min, max)) = axis_config.range {
237                        sys::ImPlot_SetupAxisLimits(axis_enum, min, max, 0);
238                    }
239                }
240            }
241
242            Ok(MultiAxisToken {
243                _title: title_cstr,
244                _axis_labels: axis_labels,
245                _scope: PlotScopeGuard::new(),
246                _phantom: PhantomData,
247            })
248        } else {
249            Err(PlotError::PlotCreationFailed(
250                "Failed to begin multi-axis plot".to_string(),
251            ))
252        }
253    }
254}
255
256/// Token representing an active multi-axis plot
257pub struct MultiAxisToken<'a> {
258    _title: CString,
259    _axis_labels: Vec<CString>,
260    _scope: PlotScopeGuard,
261    _phantom: PhantomData<&'a ()>,
262}
263
264impl<'a> MultiAxisToken<'a> {
265    /// Set the current Y-axis for subsequent plots
266    pub fn set_y_axis(&self, axis: i32) {
267        unsafe {
268            sys::ImPlot_SetAxes(
269                0,        // ImAxis_X1
270                axis + 3, // ImAxis_Y1 = 3
271            );
272        }
273    }
274
275    /// End the multi-axis plot
276    pub fn end(self) {
277        // The actual ending happens in Drop.
278    }
279}
280
281impl<'a> Drop for MultiAxisToken<'a> {
282    fn drop(&mut self) {
283        unsafe {
284            sys::ImPlot_EndPlot();
285        }
286    }
287}
288
289/// Legend management utilities
290pub struct LegendManager;
291
292impl LegendManager {
293    /// Setup legend with custom position and flags
294    pub fn setup(location: LegendLocation, flags: LegendFlags) {
295        unsafe {
296            sys::ImPlot_SetupLegend(location as i32, flags.bits() as i32);
297        }
298    }
299
300    /// Begin a custom legend
301    pub fn begin_custom(label: &str, _size: [f32; 2]) -> Result<LegendToken, PlotError> {
302        let label_cstr =
303            CString::new(label).map_err(|e| PlotError::StringConversion(e.to_string()))?;
304
305        let success = unsafe {
306            sys::ImPlot_BeginLegendPopup(
307                label_cstr.as_ptr(),
308                1, // mouse button
309            )
310        };
311
312        if success {
313            Ok(LegendToken { _label: label_cstr })
314        } else {
315            Err(PlotError::PlotCreationFailed(
316                "Failed to begin legend".to_string(),
317            ))
318        }
319    }
320}
321
322/// Legend location options (ImPlotLocation)
323#[repr(i32)]
324pub enum LegendLocation {
325    Center = sys::ImPlotLocation_Center as i32,
326    North = sys::ImPlotLocation_North as i32,
327    South = sys::ImPlotLocation_South as i32,
328    West = sys::ImPlotLocation_West as i32,
329    East = sys::ImPlotLocation_East as i32,
330    NorthWest = sys::ImPlotLocation_NorthWest as i32,
331    NorthEast = sys::ImPlotLocation_NorthEast as i32,
332    SouthWest = sys::ImPlotLocation_SouthWest as i32,
333    SouthEast = sys::ImPlotLocation_SouthEast as i32,
334}
335
336bitflags::bitflags! {
337    /// Flags for legend configuration (ImPlotLegendFlags)
338    pub struct LegendFlags: u32 {
339        const NONE              = sys::ImPlotLegendFlags_None as u32;
340        const NO_BUTTONS        = sys::ImPlotLegendFlags_NoButtons as u32;
341        const NO_HIGHLIGHT_ITEM = sys::ImPlotLegendFlags_NoHighlightItem as u32;
342        const NO_HIGHLIGHT_AXIS = sys::ImPlotLegendFlags_NoHighlightAxis as u32;
343        const NO_MENUS          = sys::ImPlotLegendFlags_NoMenus as u32;
344        const OUTSIDE           = sys::ImPlotLegendFlags_Outside as u32;
345        const HORIZONTAL        = sys::ImPlotLegendFlags_Horizontal as u32;
346        const SORT              = sys::ImPlotLegendFlags_Sort as u32;
347        // Note: ImPlotLegendFlags_Reverse is currently not exposed.
348    }
349}
350
351/// Token representing an active legend
352pub struct LegendToken {
353    _label: CString,
354}
355
356impl LegendToken {
357    /// End the legend
358    pub fn end(self) {
359        // The actual ending happens in Drop.
360    }
361}
362
363impl Drop for LegendToken {
364    fn drop(&mut self) {
365        unsafe {
366            sys::ImPlot_EndLegendPopup();
367        }
368    }
369}