1use 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
51pub 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 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 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 pub fn with_size(mut self, size: [f32; 2]) -> Self {
94 self.size = Some(size);
95 self
96 }
97
98 pub fn with_flags(mut self, flags: SubplotFlags) -> Self {
100 self.flags = flags;
101 self
102 }
103
104 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 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 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 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
200pub 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 pub fn end(self) {
211 }
213}
214
215impl<'a> Drop for SubplotToken<'a> {
216 fn drop(&mut self) {
217 unsafe {
218 sys::ImPlot_EndSubplots();
219 }
220 }
221}
222
223pub struct MultiAxisPlot<'a> {
225 title: &'a str,
226 size: Option<[f32; 2]>,
227 y_axes: Vec<YAxisConfig<'a>>,
228}
229
230pub 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 pub fn new(title: &'a str) -> Self {
240 Self {
241 title,
242 size: None,
243 y_axes: Vec::new(),
244 }
245 }
246
247 pub fn with_size(mut self, size: [f32; 2]) -> Self {
249 self.size = Some(size);
250 self
251 }
252
253 pub fn add_y_axis(mut self, config: YAxisConfig<'a>) -> Self {
255 self.y_axes.push(config);
256 self
257 }
258
259 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 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; 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
330pub 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 pub fn set_y_axis(&self, axis: YAxis) {
341 unsafe {
342 sys::ImPlot_SetAxes(
343 0, axis as i32,
345 );
346 }
347 }
348
349 pub unsafe fn set_y_axis_unchecked(&self, axis: sys::ImAxis) {
356 unsafe {
357 sys::ImPlot_SetAxes(
358 0, axis,
360 );
361 }
362 }
363
364 pub fn end(self) {
366 }
368}
369
370impl<'a> Drop for MultiAxisToken<'a> {
371 fn drop(&mut self) {
372 unsafe {
373 sys::ImPlot_EndPlot();
374 }
375 }
376}
377
378pub struct LegendManager;
380
381impl LegendManager {
382 pub fn setup(location: LegendLocation, flags: LegendFlags) {
384 unsafe {
385 sys::ImPlot_SetupLegend(location as i32, flags.bits() as i32);
386 }
387 }
388
389 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, )
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#[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 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 }
438}
439
440pub struct LegendToken {
442 _label: CString,
443}
444
445impl LegendToken {
446 pub fn end(self) {
448 }
450}
451
452impl Drop for LegendToken {
453 fn drop(&mut self) {
454 unsafe {
455 sys::ImPlot_EndLegendPopup();
456 }
457 }
458}