Skip to main content

runmat_plot/plots/
figure.rs

1//! Figure management for multiple overlaid plots
2//!
3//! This module provides the `Figure` struct that manages multiple plots in a single
4//! coordinate system, handling overlays, legends, and proper rendering order.
5
6use crate::core::{BoundingBox, GpuPackContext, RenderData};
7use crate::plots::surface::ColorMap;
8use crate::plots::{
9    AreaPlot, BarChart, ContourFillPlot, ContourPlot, ErrorBar, Line3Plot, LinePlot, MeshPlot,
10    PatchPlot, PieChart, QuiverPlot, ReferenceLine, ReferenceLineOrientation, Scatter3Plot,
11    ScatterPlot, StairsPlot, StemPlot, SurfacePlot,
12};
13use glam::Vec4;
14use log::trace;
15use std::collections::HashMap;
16
17type ViewBounds2D = (f64, f64, f64, f64);
18type PerAxesViewBoundsRef<'a> = &'a [Option<ViewBounds2D>];
19
20/// A figure that can contain multiple overlaid plots
21#[derive(Debug, Clone)]
22pub struct Figure {
23    /// All plots in this figure
24    plots: Vec<PlotElement>,
25
26    /// Figure-level settings
27    pub name: Option<String>,
28    pub number_title: bool,
29    pub visible: bool,
30    pub position: [f64; 4],
31    pub title: Option<String>,
32    pub sg_title: Option<String>,
33    pub x_label: Option<String>,
34    pub y_label: Option<String>,
35    pub z_label: Option<String>,
36    pub legend_enabled: bool,
37    pub grid_enabled: bool,
38    pub minor_grid_enabled: bool,
39    pub box_enabled: bool,
40    pub background_color: Vec4,
41
42    /// Axis limits (None = auto-scale)
43    pub x_limits: Option<(f64, f64)>,
44    pub y_limits: Option<(f64, f64)>,
45    pub z_limits: Option<(f64, f64)>,
46
47    /// Axis scales
48    pub x_log: bool,
49    pub y_log: bool,
50
51    /// Axis aspect handling
52    pub axis_equal: bool,
53
54    /// Global colormap and colorbar
55    pub colormap: ColorMap,
56    pub colorbar_enabled: bool,
57
58    /// Color mapping limits for all color-mapped plots in this figure (caxis)
59    pub color_limits: Option<(f64, f64)>,
60
61    /// Cached data
62    bounds: Option<BoundingBox>,
63    dirty: bool,
64
65    /// Subplot grid configuration (rows x cols). Defaults to 1x1.
66    pub axes_rows: usize,
67    pub axes_cols: usize,
68    /// For each plot element, the axes index (row-major, 0..rows*cols-1)
69    plot_axes_indices: Vec<usize>,
70
71    /// The axes index whose annotation metadata is currently active.
72    pub active_axes_index: usize,
73
74    /// Per-axes metadata used for subplot-correct annotations and legend state.
75    pub axes_metadata: Vec<AxesMetadata>,
76    pub sg_title_style: TextStyle,
77}
78
79#[derive(Debug, Clone)]
80pub struct TextStyle {
81    pub color: Option<Vec4>,
82    pub font_size: Option<f32>,
83    pub font_weight: Option<String>,
84    pub font_angle: Option<String>,
85    pub interpreter: Option<String>,
86    pub visible: bool,
87}
88
89impl Default for TextStyle {
90    fn default() -> Self {
91        Self {
92            color: None,
93            font_size: None,
94            font_weight: None,
95            font_angle: None,
96            interpreter: None,
97            visible: true,
98        }
99    }
100}
101
102#[derive(Debug, Clone)]
103pub struct LegendStyle {
104    pub location: Option<String>,
105    pub visible: bool,
106    pub font_size: Option<f32>,
107    pub font_weight: Option<String>,
108    pub font_angle: Option<String>,
109    pub interpreter: Option<String>,
110    pub box_visible: Option<bool>,
111    pub orientation: Option<String>,
112    pub text_color: Option<Vec4>,
113}
114
115impl Default for LegendStyle {
116    fn default() -> Self {
117        Self {
118            location: None,
119            visible: true,
120            font_size: None,
121            font_weight: None,
122            font_angle: None,
123            interpreter: None,
124            box_visible: None,
125            orientation: None,
126            text_color: None,
127        }
128    }
129}
130
131#[derive(Debug, Clone, Default)]
132pub struct AxesMetadata {
133    pub axes_kind: AxesKind,
134    pub overlay_parent: Option<usize>,
135    pub y_axis_location: String,
136    pub position: [f64; 4],
137    pub position_explicit: bool,
138    pub units: String,
139    pub title: Option<String>,
140    pub subtitle: Option<String>,
141    pub x_label: Option<String>,
142    pub y_label: Option<String>,
143    pub z_label: Option<String>,
144    pub x_ticks: Option<Vec<f64>>,
145    pub y_ticks: Option<Vec<f64>>,
146    pub x_tick_labels: Option<Vec<String>>,
147    pub y_tick_labels: Option<Vec<String>>,
148    pub x_tick_format: Option<String>,
149    pub y_tick_format: Option<String>,
150    pub x_tick_label_rotation: Option<f64>,
151    pub y_tick_label_rotation: Option<f64>,
152    pub x_limits: Option<(f64, f64)>,
153    pub y_limits: Option<(f64, f64)>,
154    pub z_limits: Option<(f64, f64)>,
155    pub x_log: bool,
156    pub y_log: bool,
157    pub view_azimuth_deg: Option<f32>,
158    pub view_elevation_deg: Option<f32>,
159    pub view_revision: u64,
160    pub grid_enabled: bool,
161    pub minor_grid_enabled: bool,
162    pub minor_grid_explicit: bool,
163    pub hidden_line_removal: bool,
164    pub box_enabled: bool,
165    pub axis_equal: bool,
166    pub data_aspect_ratio: [f64; 3],
167    pub data_aspect_ratio_mode: String,
168    pub legend_enabled: bool,
169    pub colorbar_enabled: bool,
170    pub colormap: ColorMap,
171    pub color_order: Option<Vec<Vec4>>,
172    pub color_limits: Option<(f64, f64)>,
173    pub axes_style: TextStyle,
174    pub title_style: TextStyle,
175    pub subtitle_style: TextStyle,
176    pub x_label_style: TextStyle,
177    pub y_label_style: TextStyle,
178    pub z_label_style: TextStyle,
179    pub legend_style: LegendStyle,
180    pub world_text_annotations: Vec<TextAnnotation>,
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
184pub enum AxesKind {
185    #[default]
186    Cartesian,
187    Polar,
188}
189
190#[derive(Debug, Clone)]
191pub struct TextAnnotation {
192    pub position: glam::Vec3,
193    pub text: String,
194    pub style: TextStyle,
195}
196
197/// A plot element that can be any type of plot
198#[derive(Debug, Clone)]
199pub enum PlotElement {
200    Line(LinePlot),
201    Scatter(ScatterPlot),
202    Bar(BarChart),
203    ErrorBar(Box<ErrorBar>),
204    Stairs(StairsPlot),
205    Stem(StemPlot),
206    Area(AreaPlot),
207    Quiver(QuiverPlot),
208    Pie(PieChart),
209    Surface(SurfacePlot),
210    Mesh(Box<MeshPlot>),
211    Patch(PatchPlot),
212    Line3(Line3Plot),
213    Scatter3(Scatter3Plot),
214    Contour(ContourPlot),
215    ContourFill(ContourFillPlot),
216    ReferenceLine(ReferenceLine),
217}
218
219/// Legend entry for a plot
220#[derive(Debug, Clone)]
221pub struct LegendEntry {
222    pub label: String,
223    pub color: Vec4,
224    pub plot_type: PlotType,
225}
226
227#[derive(Debug, Clone)]
228pub struct PieLabelEntry {
229    pub label: String,
230    pub position: glam::Vec2,
231}
232
233/// Type of plot for legend rendering
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
235pub enum PlotType {
236    Line,
237    Scatter,
238    Bar,
239    ErrorBar,
240    Stairs,
241    Stem,
242    Area,
243    Quiver,
244    Pie,
245    Surface,
246    Mesh,
247    Patch,
248    Line3,
249    Scatter3,
250    Contour,
251    ContourFill,
252    ReferenceLine,
253}
254
255impl Figure {
256    fn default_axes_metadata() -> AxesMetadata {
257        AxesMetadata {
258            axes_kind: AxesKind::Cartesian,
259            overlay_parent: None,
260            y_axis_location: "left".into(),
261            position: [0.13, 0.11, 0.775, 0.815],
262            position_explicit: false,
263            units: "normalized".into(),
264            x_limits: None,
265            y_limits: None,
266            z_limits: None,
267            grid_enabled: true,
268            minor_grid_enabled: false,
269            hidden_line_removal: true,
270            box_enabled: true,
271            axis_equal: false,
272            data_aspect_ratio: [1.0, 1.0, 1.0],
273            data_aspect_ratio_mode: "auto".into(),
274            legend_enabled: true,
275            colorbar_enabled: false,
276            colormap: ColorMap::Parula,
277            color_limits: None,
278            ..Default::default()
279        }
280    }
281
282    /// Create a new empty figure
283    pub fn new() -> Self {
284        Self {
285            plots: Vec::new(),
286            name: None,
287            number_title: true,
288            visible: true,
289            position: [0.0, 0.0, 560.0, 420.0],
290            title: None,
291            sg_title: None,
292            x_label: None,
293            y_label: None,
294            z_label: None,
295            legend_enabled: true,
296            grid_enabled: true,
297            minor_grid_enabled: false,
298            box_enabled: true,
299            background_color: Vec4::new(1.0, 1.0, 1.0, 1.0), // White background
300            x_limits: None,
301            y_limits: None,
302            z_limits: None,
303            x_log: false,
304            y_log: false,
305            axis_equal: false,
306            colormap: ColorMap::Parula,
307            colorbar_enabled: false,
308            color_limits: None,
309            bounds: None,
310            dirty: true,
311            axes_rows: 1,
312            axes_cols: 1,
313            plot_axes_indices: Vec::new(),
314            active_axes_index: 0,
315            axes_metadata: vec![Self::default_axes_metadata()],
316            sg_title_style: TextStyle::default(),
317        }
318    }
319
320    fn ensure_axes_metadata_capacity(&mut self, min_len: usize) {
321        while self.axes_metadata.len() < min_len.max(1) {
322            self.axes_metadata.push(Self::default_axes_metadata());
323        }
324    }
325
326    fn sync_legacy_fields_from_active_axes(&mut self) {
327        self.ensure_axes_metadata_capacity(self.active_axes_index + 1);
328        if let Some(meta) = self.axes_metadata.get(self.active_axes_index).cloned() {
329            self.title = meta.title;
330            self.x_label = meta.x_label;
331            self.y_label = meta.y_label;
332            self.z_label = meta.z_label;
333            self.x_limits = meta.x_limits;
334            self.y_limits = meta.y_limits;
335            self.z_limits = meta.z_limits;
336            self.x_log = meta.x_log;
337            self.y_log = meta.y_log;
338            self.grid_enabled = meta.grid_enabled;
339            self.box_enabled = meta.box_enabled;
340            self.axis_equal = meta.axis_equal;
341            self.legend_enabled = meta.legend_enabled;
342            self.colorbar_enabled = meta.colorbar_enabled;
343            self.colormap = meta.colormap.clone();
344            self.color_limits = meta.color_limits;
345        }
346    }
347
348    pub fn set_active_axes_index(&mut self, axes_index: usize) {
349        self.ensure_axes_metadata_capacity(axes_index + 1);
350        self.active_axes_index = axes_index;
351        self.sync_legacy_fields_from_active_axes();
352        self.dirty = true;
353    }
354
355    pub fn ensure_axes(&mut self, axes_index: usize) {
356        self.ensure_axes_metadata_capacity(axes_index + 1);
357        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
358            meta.axes_kind = AxesKind::Cartesian;
359            meta.overlay_parent = None;
360        }
361        self.dirty = true;
362    }
363
364    pub fn axes_metadata(&self, axes_index: usize) -> Option<&AxesMetadata> {
365        self.axes_metadata.get(axes_index)
366    }
367
368    pub fn active_axes_metadata(&self) -> Option<&AxesMetadata> {
369        self.axes_metadata(self.active_axes_index)
370    }
371
372    pub fn with_sg_title<S: Into<String>>(mut self, title: S) -> Self {
373        self.set_sg_title(title);
374        self
375    }
376
377    pub fn set_sg_title<S: Into<String>>(&mut self, title: S) {
378        self.sg_title = Some(title.into());
379        self.dirty = true;
380    }
381
382    pub fn clear_sg_title(&mut self) {
383        self.sg_title = None;
384        self.dirty = true;
385    }
386
387    pub fn set_sg_title_style(&mut self, style: TextStyle) {
388        self.sg_title_style = style;
389        self.dirty = true;
390    }
391
392    pub fn set_name<S: Into<String>>(&mut self, name: S) {
393        self.name = Some(name.into());
394        self.dirty = true;
395    }
396
397    pub fn set_number_title(&mut self, enabled: bool) {
398        self.number_title = enabled;
399        self.dirty = true;
400    }
401
402    pub fn set_visible(&mut self, visible: bool) {
403        self.visible = visible;
404        self.dirty = true;
405    }
406
407    pub fn set_position(&mut self, position: [f64; 4]) {
408        self.position = position;
409        self.dirty = true;
410    }
411
412    pub fn window_title(&self, handle: Option<u32>) -> String {
413        let name = self.name.as_deref().map(str::trim).unwrap_or_default();
414        let numbered = if self.number_title {
415            handle.filter(|h| *h > 0).map(|h| format!("Figure {h}"))
416        } else {
417            None
418        };
419        match (numbered, name.is_empty()) {
420            (Some(numbered), false) => format!("{numbered}: {name}"),
421            (Some(numbered), true) => numbered,
422            (None, false) => name.to_string(),
423            (None, true) => "RunMat Plot".to_string(),
424        }
425    }
426
427    pub fn has_any_titles(&self) -> bool {
428        let non_empty = |s: Option<&str>| s.map(str::trim).is_some_and(|t| !t.is_empty());
429        non_empty(self.sg_title.as_deref())
430            || non_empty(self.title.as_deref())
431            || self
432                .axes_metadata
433                .iter()
434                .any(|meta| non_empty(meta.title.as_deref()) || non_empty(meta.subtitle.as_deref()))
435    }
436
437    /// Set the figure title
438    pub fn with_title<S: Into<String>>(mut self, title: S) -> Self {
439        self.set_title(title);
440        self
441    }
442
443    /// Set the figure title in-place
444    pub fn set_title<S: Into<String>>(&mut self, title: S) {
445        self.set_axes_title(self.active_axes_index, title);
446    }
447
448    /// Set axis labels
449    pub fn with_labels<S: Into<String>>(mut self, x_label: S, y_label: S) -> Self {
450        self.set_axis_labels(x_label, y_label);
451        self
452    }
453
454    /// Set axis labels in-place
455    pub fn set_axis_labels<S: Into<String>>(&mut self, x_label: S, y_label: S) {
456        self.set_axes_labels(self.active_axes_index, x_label, y_label);
457        self.dirty = true;
458    }
459
460    pub fn set_axes_title<S: Into<String>>(&mut self, axes_index: usize, title: S) {
461        self.ensure_axes_metadata_capacity(axes_index + 1);
462        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
463            meta.title = Some(title.into());
464        }
465        if axes_index == self.active_axes_index {
466            self.sync_legacy_fields_from_active_axes();
467        }
468        self.dirty = true;
469    }
470
471    pub fn set_axes_subtitle<S: Into<String>>(&mut self, axes_index: usize, subtitle: S) {
472        self.ensure_axes_metadata_capacity(axes_index + 1);
473        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
474            meta.subtitle = Some(subtitle.into());
475        }
476        self.dirty = true;
477    }
478
479    pub fn set_axes_xlabel<S: Into<String>>(&mut self, axes_index: usize, label: S) {
480        self.ensure_axes_metadata_capacity(axes_index + 1);
481        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
482            meta.x_label = Some(label.into());
483        }
484        if axes_index == self.active_axes_index {
485            self.sync_legacy_fields_from_active_axes();
486        }
487        self.dirty = true;
488    }
489
490    pub fn set_axes_ylabel<S: Into<String>>(&mut self, axes_index: usize, label: S) {
491        self.ensure_axes_metadata_capacity(axes_index + 1);
492        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
493            meta.y_label = Some(label.into());
494        }
495        if axes_index == self.active_axes_index {
496            self.sync_legacy_fields_from_active_axes();
497        }
498        self.dirty = true;
499    }
500
501    pub fn set_axes_zlabel<S: Into<String>>(&mut self, axes_index: usize, label: S) {
502        self.ensure_axes_metadata_capacity(axes_index + 1);
503        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
504            meta.z_label = Some(label.into());
505        }
506        if axes_index == self.active_axes_index {
507            self.sync_legacy_fields_from_active_axes();
508        }
509        self.dirty = true;
510    }
511
512    pub fn add_axes_text_annotation<S: Into<String>>(
513        &mut self,
514        axes_index: usize,
515        position: glam::Vec3,
516        text: S,
517        style: TextStyle,
518    ) -> usize {
519        self.ensure_axes_metadata_capacity(axes_index + 1);
520        let Some(meta) = self.axes_metadata.get_mut(axes_index) else {
521            return 0;
522        };
523        meta.world_text_annotations.push(TextAnnotation {
524            position,
525            text: text.into(),
526            style,
527        });
528        self.dirty = true;
529        meta.world_text_annotations.len() - 1
530    }
531
532    pub fn axes_text_annotation(
533        &self,
534        axes_index: usize,
535        annotation_index: usize,
536    ) -> Option<&TextAnnotation> {
537        self.axes_metadata
538            .get(axes_index)
539            .and_then(|meta| meta.world_text_annotations.get(annotation_index))
540    }
541
542    pub fn set_axes_text_annotation_text<S: Into<String>>(
543        &mut self,
544        axes_index: usize,
545        annotation_index: usize,
546        text: S,
547    ) {
548        if let Some(annotation) = self
549            .axes_metadata
550            .get_mut(axes_index)
551            .and_then(|meta| meta.world_text_annotations.get_mut(annotation_index))
552        {
553            annotation.text = text.into();
554            self.dirty = true;
555        }
556    }
557
558    pub fn set_axes_text_annotation_position(
559        &mut self,
560        axes_index: usize,
561        annotation_index: usize,
562        position: glam::Vec3,
563    ) {
564        if let Some(annotation) = self
565            .axes_metadata
566            .get_mut(axes_index)
567            .and_then(|meta| meta.world_text_annotations.get_mut(annotation_index))
568        {
569            annotation.position = position;
570            self.dirty = true;
571        }
572    }
573
574    pub fn set_axes_text_annotation_style(
575        &mut self,
576        axes_index: usize,
577        annotation_index: usize,
578        style: TextStyle,
579    ) {
580        if let Some(annotation) = self
581            .axes_metadata
582            .get_mut(axes_index)
583            .and_then(|meta| meta.world_text_annotations.get_mut(annotation_index))
584        {
585            annotation.style = style;
586            self.dirty = true;
587        }
588    }
589
590    pub fn axes_text_annotations(&self, axes_index: usize) -> &[TextAnnotation] {
591        self.axes_metadata
592            .get(axes_index)
593            .map(|meta| meta.world_text_annotations.as_slice())
594            .unwrap_or(&[])
595    }
596
597    pub fn set_axes_labels<S: Into<String>>(&mut self, axes_index: usize, x_label: S, y_label: S) {
598        self.ensure_axes_metadata_capacity(axes_index + 1);
599        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
600            meta.x_label = Some(x_label.into());
601            meta.y_label = Some(y_label.into());
602        }
603        if axes_index == self.active_axes_index {
604            self.sync_legacy_fields_from_active_axes();
605        }
606        self.dirty = true;
607    }
608
609    pub fn set_axes_tick_labels(
610        &mut self,
611        axes_index: usize,
612        x_labels: Option<Vec<String>>,
613        y_labels: Option<Vec<String>>,
614    ) {
615        self.ensure_axes_metadata_capacity(axes_index + 1);
616        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
617            meta.x_tick_labels = x_labels;
618            meta.y_tick_labels = y_labels;
619        }
620        self.dirty = true;
621    }
622
623    pub fn set_axes_tick_formats(
624        &mut self,
625        axes_index: usize,
626        x_format: Option<String>,
627        y_format: Option<String>,
628    ) {
629        self.ensure_axes_metadata_capacity(axes_index + 1);
630        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
631            meta.x_tick_format = x_format;
632            meta.y_tick_format = y_format;
633        }
634        self.dirty = true;
635    }
636
637    pub fn set_axes_tick_label_rotations(
638        &mut self,
639        axes_index: usize,
640        x_angle: Option<f64>,
641        y_angle: Option<f64>,
642    ) {
643        self.ensure_axes_metadata_capacity(axes_index + 1);
644        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
645            meta.x_tick_label_rotation = x_angle;
646            meta.y_tick_label_rotation = y_angle;
647        }
648        self.dirty = true;
649    }
650
651    pub fn set_axes_ticks(
652        &mut self,
653        axes_index: usize,
654        x_ticks: Option<Vec<f64>>,
655        y_ticks: Option<Vec<f64>>,
656    ) {
657        self.ensure_axes_metadata_capacity(axes_index + 1);
658        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
659            meta.x_ticks = x_ticks;
660            meta.y_ticks = y_ticks;
661        }
662        self.dirty = true;
663    }
664
665    pub fn set_axes_style(&mut self, axes_index: usize, style: TextStyle) {
666        self.ensure_axes_metadata_capacity(axes_index + 1);
667        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
668            meta.axes_style = style;
669        }
670        self.dirty = true;
671    }
672
673    pub fn set_axes_title_style(&mut self, axes_index: usize, style: TextStyle) {
674        self.ensure_axes_metadata_capacity(axes_index + 1);
675        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
676            meta.title_style = style;
677        }
678        self.dirty = true;
679    }
680
681    pub fn set_axes_subtitle_style(&mut self, axes_index: usize, style: TextStyle) {
682        self.ensure_axes_metadata_capacity(axes_index + 1);
683        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
684            meta.subtitle_style = style;
685        }
686        self.dirty = true;
687    }
688
689    pub fn set_axes_xlabel_style(&mut self, axes_index: usize, style: TextStyle) {
690        self.ensure_axes_metadata_capacity(axes_index + 1);
691        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
692            meta.x_label_style = style;
693        }
694        self.dirty = true;
695    }
696
697    pub fn set_axes_ylabel_style(&mut self, axes_index: usize, style: TextStyle) {
698        self.ensure_axes_metadata_capacity(axes_index + 1);
699        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
700            meta.y_label_style = style;
701        }
702        self.dirty = true;
703    }
704
705    pub fn set_axes_zlabel_style(&mut self, axes_index: usize, style: TextStyle) {
706        self.ensure_axes_metadata_capacity(axes_index + 1);
707        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
708            meta.z_label_style = style;
709        }
710        self.dirty = true;
711    }
712
713    /// Set axis limits manually
714    pub fn with_limits(mut self, x_limits: (f64, f64), y_limits: (f64, f64)) -> Self {
715        self.x_limits = Some(x_limits);
716        self.y_limits = Some(y_limits);
717        self.dirty = true;
718        self
719    }
720
721    /// Enable or disable the legend
722    pub fn with_legend(mut self, enabled: bool) -> Self {
723        self.set_legend(enabled);
724        self
725    }
726
727    pub fn set_legend(&mut self, enabled: bool) {
728        self.set_axes_legend_enabled(self.active_axes_index, enabled);
729    }
730
731    pub fn set_axes_legend_enabled(&mut self, axes_index: usize, enabled: bool) {
732        self.ensure_axes_metadata_capacity(axes_index + 1);
733        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
734            meta.legend_enabled = enabled;
735        }
736        if axes_index == self.active_axes_index {
737            self.sync_legacy_fields_from_active_axes();
738        }
739        self.dirty = true;
740    }
741
742    pub fn set_axes_legend_style(&mut self, axes_index: usize, style: LegendStyle) {
743        self.ensure_axes_metadata_capacity(axes_index + 1);
744        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
745            meta.legend_style = style;
746        }
747        self.dirty = true;
748    }
749
750    pub fn set_axes_log_modes(&mut self, axes_index: usize, x_log: bool, y_log: bool) {
751        self.ensure_axes_metadata_capacity(axes_index + 1);
752        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
753            meta.x_log = x_log;
754            meta.y_log = y_log;
755        }
756        if axes_index == self.active_axes_index {
757            self.sync_legacy_fields_from_active_axes();
758        }
759        self.dirty = true;
760    }
761
762    pub fn set_axes_view(&mut self, axes_index: usize, azimuth_deg: f32, elevation_deg: f32) {
763        self.ensure_axes_metadata_capacity(axes_index + 1);
764        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
765            meta.view_azimuth_deg = Some(azimuth_deg);
766            meta.view_elevation_deg = Some(elevation_deg);
767            meta.view_revision = meta.view_revision.wrapping_add(1);
768        }
769        self.dirty = true;
770    }
771
772    /// Enable or disable the grid
773    pub fn with_grid(mut self, enabled: bool) -> Self {
774        self.set_grid(enabled);
775        self
776    }
777
778    pub fn set_grid(&mut self, enabled: bool) {
779        self.set_axes_grid_enabled(self.active_axes_index, enabled);
780        self.dirty = true;
781    }
782
783    pub fn set_axes_grid_enabled(&mut self, axes_index: usize, enabled: bool) {
784        self.ensure_axes_metadata_capacity(axes_index + 1);
785        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
786            meta.grid_enabled = enabled;
787        }
788        if axes_index == self.active_axes_index {
789            self.sync_legacy_fields_from_active_axes();
790        }
791        self.dirty = true;
792    }
793
794    pub fn set_axes_kind(&mut self, axes_index: usize, axes_kind: AxesKind) {
795        self.ensure_axes_metadata_capacity(axes_index + 1);
796        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
797            meta.axes_kind = axes_kind;
798        }
799        if axes_index == self.active_axes_index {
800            self.sync_legacy_fields_from_active_axes();
801        }
802        self.dirty = true;
803    }
804
805    pub fn axes_kind(&self, axes_index: usize) -> AxesKind {
806        self.axes_metadata(axes_index)
807            .map(|meta| meta.axes_kind)
808            .unwrap_or(AxesKind::Cartesian)
809    }
810
811    pub fn with_minor_grid(mut self, enabled: bool) -> Self {
812        self.set_minor_grid(enabled);
813        self
814    }
815
816    pub fn set_minor_grid(&mut self, enabled: bool) {
817        self.set_axes_minor_grid_enabled(self.active_axes_index, enabled);
818        self.dirty = true;
819    }
820
821    pub fn set_axes_minor_grid_enabled(&mut self, axes_index: usize, enabled: bool) {
822        self.ensure_axes_metadata_capacity(axes_index + 1);
823        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
824            meta.minor_grid_enabled = enabled;
825            meta.minor_grid_explicit = true;
826        }
827        if axes_index == self.active_axes_index {
828            self.sync_legacy_fields_from_active_axes();
829        }
830        self.dirty = true;
831    }
832
833    pub fn minor_grid_enabled_for_axes(&self, axes_index: usize) -> bool {
834        self.axes_metadata(axes_index)
835            .map(|meta| {
836                if meta.minor_grid_explicit {
837                    meta.minor_grid_enabled
838                } else {
839                    self.minor_grid_enabled
840                }
841            })
842            .unwrap_or(self.minor_grid_enabled)
843    }
844
845    /// Set background color
846    pub fn with_background_color(mut self, color: Vec4) -> Self {
847        self.set_background_color(color);
848        self
849    }
850
851    pub fn set_background_color(&mut self, color: Vec4) {
852        self.background_color = color;
853        self.dirty = true;
854    }
855
856    /// Set log scale flags
857    pub fn with_xlog(mut self, enabled: bool) -> Self {
858        self.set_axes_log_modes(self.active_axes_index, enabled, self.y_log);
859        self
860    }
861    pub fn with_ylog(mut self, enabled: bool) -> Self {
862        self.set_axes_log_modes(self.active_axes_index, self.x_log, enabled);
863        self
864    }
865    pub fn with_axis_equal(mut self, enabled: bool) -> Self {
866        self.set_axis_equal(enabled);
867        self
868    }
869
870    pub fn set_axis_equal(&mut self, enabled: bool) {
871        self.set_axes_axis_equal(self.active_axes_index, enabled);
872        self.dirty = true;
873    }
874    pub fn set_axes_axis_equal(&mut self, axes_index: usize, enabled: bool) {
875        self.ensure_axes_metadata_capacity(axes_index + 1);
876        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
877            meta.axis_equal = enabled;
878            meta.data_aspect_ratio_mode = if enabled { "manual" } else { "auto" }.into();
879            if enabled {
880                meta.data_aspect_ratio = [1.0, 1.0, 1.0];
881            }
882        }
883        if axes_index == self.active_axes_index {
884            self.sync_legacy_fields_from_active_axes();
885        }
886        self.dirty = true;
887    }
888
889    pub fn set_axes_data_aspect_ratio(
890        &mut self,
891        axes_index: usize,
892        ratio: [f64; 3],
893        mode: impl Into<String>,
894    ) {
895        self.ensure_axes_metadata_capacity(axes_index + 1);
896        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
897            meta.data_aspect_ratio = ratio;
898            meta.data_aspect_ratio_mode = mode.into();
899            meta.axis_equal = meta.data_aspect_ratio_mode == "manual" && ratio == [1.0, 1.0, 1.0];
900        }
901        if axes_index == self.active_axes_index {
902            self.sync_legacy_fields_from_active_axes();
903        }
904        self.dirty = true;
905    }
906    pub fn with_colormap(mut self, cmap: ColorMap) -> Self {
907        self.set_axes_colormap(self.active_axes_index, cmap);
908        self
909    }
910    pub fn with_colorbar(mut self, enabled: bool) -> Self {
911        self.set_axes_colorbar_enabled(self.active_axes_index, enabled);
912        self
913    }
914    pub fn with_color_limits(mut self, limits: Option<(f64, f64)>) -> Self {
915        self.set_axes_color_limits(self.active_axes_index, limits);
916        self
917    }
918
919    /// Configure subplot grid (rows x cols). Axes are indexed row-major starting at 0.
920    pub fn with_subplot_grid(mut self, rows: usize, cols: usize) -> Self {
921        self.set_subplot_grid(rows, cols);
922        self
923    }
924
925    /// Return subplot grid (rows, cols)
926    pub fn axes_grid(&self) -> (usize, usize) {
927        (self.axes_rows, self.axes_cols)
928    }
929
930    pub fn axes_count(&self) -> usize {
931        let plotted_axes = self
932            .plot_axes_indices
933            .iter()
934            .copied()
935            .max()
936            .map(|index| index + 1)
937            .unwrap_or(0);
938        let overlay_axes = self
939            .axes_metadata
940            .iter()
941            .enumerate()
942            .filter(|(_, meta)| meta.overlay_parent.is_some())
943            .map(|(index, _)| index + 1)
944            .max()
945            .unwrap_or(0);
946        self.total_axes()
947            .max(plotted_axes)
948            .max(overlay_axes)
949            .max(self.axes_metadata.len())
950            .max(1)
951    }
952
953    pub fn ensure_overlay_axes(&mut self, parent_axes_index: usize) -> usize {
954        let parent = parent_axes_index.min(self.total_axes().saturating_sub(1));
955        if let Some((index, _)) = self
956            .axes_metadata
957            .iter()
958            .enumerate()
959            .find(|(_, meta)| meta.overlay_parent == Some(parent))
960        {
961            return index;
962        }
963
964        let index = self.axes_metadata.len().max(self.total_axes());
965        self.ensure_axes_metadata_capacity(index + 1);
966        if let Some(parent_meta) = self.axes_metadata.get(parent).cloned() {
967            if let Some(meta) = self.axes_metadata.get_mut(index) {
968                *meta = parent_meta;
969                meta.overlay_parent = Some(parent);
970                meta.y_axis_location = "right".into();
971                meta.y_limits = None;
972                meta.y_ticks = None;
973                meta.y_tick_labels = None;
974                meta.y_tick_format = None;
975                meta.y_label = None;
976                meta.legend_enabled = false;
977                meta.grid_enabled = false;
978                meta.minor_grid_enabled = false;
979            }
980        }
981        self.dirty = true;
982        index
983    }
984
985    pub fn axes_overlay_parent(&self, axes_index: usize) -> Option<usize> {
986        self.axes_metadata
987            .get(axes_index)
988            .and_then(|meta| meta.overlay_parent)
989    }
990
991    pub fn set_axes_y_axis_location(&mut self, axes_index: usize, location: impl Into<String>) {
992        self.ensure_axes_metadata_capacity(axes_index + 1);
993        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
994            meta.y_axis_location = location.into();
995        }
996        self.dirty = true;
997    }
998
999    pub fn set_axes_position(&mut self, axes_index: usize, position: [f64; 4]) {
1000        self.ensure_axes_metadata_capacity(axes_index + 1);
1001        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1002            meta.position = position;
1003            meta.position_explicit = true;
1004        }
1005        self.dirty = true;
1006    }
1007
1008    pub fn set_axes_units(&mut self, axes_index: usize, units: impl Into<String>) {
1009        self.ensure_axes_metadata_capacity(axes_index + 1);
1010        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1011            meta.units = units.into();
1012        }
1013        self.dirty = true;
1014    }
1015
1016    /// Axes index mapping for plots (length equals number of plots)
1017    pub fn plot_axes_indices(&self) -> &[usize] {
1018        &self.plot_axes_indices
1019    }
1020
1021    /// Assign a specific plot (by index) to an axes index in the subplot grid
1022    pub fn assign_plot_to_axes(
1023        &mut self,
1024        plot_index: usize,
1025        axes_index: usize,
1026    ) -> Result<(), String> {
1027        if plot_index >= self.plot_axes_indices.len() {
1028            return Err(format!(
1029                "assign_plot_to_axes: index {plot_index} out of bounds"
1030            ));
1031        }
1032        let max_axes = self.axes_count();
1033        let ai = axes_index.min(max_axes.saturating_sub(1));
1034        self.plot_axes_indices[plot_index] = ai;
1035        self.dirty = true;
1036        Ok(())
1037    }
1038    /// Mutably set subplot grid (rows x cols)
1039    pub fn set_subplot_grid(&mut self, rows: usize, cols: usize) {
1040        self.axes_rows = rows.max(1);
1041        self.axes_cols = cols.max(1);
1042        let grid_axes = self.axes_rows * self.axes_cols;
1043        self.ensure_axes_metadata_capacity(grid_axes);
1044        for axes_index in 0..grid_axes {
1045            if self
1046                .axes_metadata
1047                .get(axes_index)
1048                .is_some_and(|meta| meta.overlay_parent.is_some())
1049            {
1050                self.clear_axes(axes_index);
1051                if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1052                    *meta = Self::default_axes_metadata();
1053                }
1054            }
1055        }
1056        self.active_axes_index = self.active_axes_index.min(
1057            self.axes_rows
1058                .saturating_mul(self.axes_cols)
1059                .saturating_sub(1),
1060        );
1061        self.sync_legacy_fields_from_active_axes();
1062        self.dirty = true;
1063    }
1064
1065    /// Set color limits and propagate to existing surface plots
1066    pub fn set_color_limits(&mut self, limits: Option<(f64, f64)>) {
1067        self.set_axes_color_limits(self.active_axes_index, limits);
1068        self.dirty = true;
1069    }
1070
1071    pub fn set_z_limits(&mut self, limits: Option<(f64, f64)>) {
1072        self.set_axes_z_limits(self.active_axes_index, limits);
1073        self.dirty = true;
1074    }
1075
1076    pub fn set_axes_limits(
1077        &mut self,
1078        axes_index: usize,
1079        x: Option<(f64, f64)>,
1080        y: Option<(f64, f64)>,
1081    ) {
1082        self.ensure_axes_metadata_capacity(axes_index + 1);
1083        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1084            meta.x_limits = x;
1085            meta.y_limits = y;
1086        }
1087        if axes_index == self.active_axes_index {
1088            self.sync_legacy_fields_from_active_axes();
1089        }
1090        self.dirty = true;
1091    }
1092
1093    pub fn set_axes_z_limits(&mut self, axes_index: usize, limits: Option<(f64, f64)>) {
1094        self.ensure_axes_metadata_capacity(axes_index + 1);
1095        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1096            meta.z_limits = limits;
1097        }
1098        if axes_index == self.active_axes_index {
1099            self.sync_legacy_fields_from_active_axes();
1100        }
1101        self.dirty = true;
1102    }
1103
1104    pub fn set_axes_box_enabled(&mut self, axes_index: usize, enabled: bool) {
1105        self.ensure_axes_metadata_capacity(axes_index + 1);
1106        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1107            meta.box_enabled = enabled;
1108        }
1109        if axes_index == self.active_axes_index {
1110            self.sync_legacy_fields_from_active_axes();
1111        }
1112        self.dirty = true;
1113    }
1114
1115    pub fn set_axes_hidden_line_removal(&mut self, axes_index: usize, enabled: bool) {
1116        self.ensure_axes_metadata_capacity(axes_index + 1);
1117        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1118            meta.hidden_line_removal = enabled;
1119        }
1120        self.dirty = true;
1121    }
1122
1123    pub fn set_axes_colorbar_enabled(&mut self, axes_index: usize, enabled: bool) {
1124        self.ensure_axes_metadata_capacity(axes_index + 1);
1125        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1126            meta.colorbar_enabled = enabled;
1127        }
1128        if axes_index == self.active_axes_index {
1129            self.sync_legacy_fields_from_active_axes();
1130        }
1131        self.dirty = true;
1132    }
1133
1134    pub fn set_axes_colormap(&mut self, axes_index: usize, cmap: ColorMap) {
1135        self.ensure_axes_metadata_capacity(axes_index + 1);
1136        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1137            meta.colormap = cmap.clone();
1138        }
1139        for (idx, plot) in self.plots.iter_mut().enumerate() {
1140            if self.plot_axes_indices.get(idx).copied().unwrap_or(0) != axes_index {
1141                continue;
1142            }
1143            if let PlotElement::Surface(surface) = plot {
1144                *surface = surface.clone().with_colormap(cmap.clone());
1145            }
1146        }
1147        if axes_index == self.active_axes_index {
1148            self.sync_legacy_fields_from_active_axes();
1149        }
1150        self.dirty = true;
1151    }
1152
1153    pub fn set_axes_color_limits(&mut self, axes_index: usize, limits: Option<(f64, f64)>) {
1154        self.ensure_axes_metadata_capacity(axes_index + 1);
1155        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1156            meta.color_limits = limits;
1157        }
1158        for (idx, plot) in self.plots.iter_mut().enumerate() {
1159            if self.plot_axes_indices.get(idx).copied().unwrap_or(0) != axes_index {
1160                continue;
1161            }
1162            if let PlotElement::Surface(surface) = plot {
1163                surface.set_color_limits(limits);
1164            }
1165        }
1166        if axes_index == self.active_axes_index {
1167            self.sync_legacy_fields_from_active_axes();
1168        }
1169        self.dirty = true;
1170    }
1171
1172    pub fn set_axes_color_order(&mut self, axes_index: usize, colors: Vec<Vec4>) {
1173        self.ensure_axes_metadata_capacity(axes_index + 1);
1174        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1175            meta.color_order = Some(colors.clone());
1176        }
1177        self.recolor_axes_plots(axes_index, &colors);
1178        self.dirty = true;
1179    }
1180
1181    pub fn set_all_axes_color_order(&mut self, colors: Vec<Vec4>) {
1182        let total_axes = self.total_axes().max(1);
1183        self.ensure_axes_metadata_capacity(total_axes);
1184        for axes_index in 0..total_axes {
1185            if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1186                meta.color_order = Some(colors.clone());
1187            }
1188            self.recolor_axes_plots(axes_index, &colors);
1189        }
1190        self.dirty = true;
1191    }
1192
1193    fn recolor_axes_plots(&mut self, axes_index: usize, colors: &[Vec4]) {
1194        if colors.is_empty() {
1195            return;
1196        }
1197        let mut series = 0usize;
1198        for (plot_index, plot) in self.plots.iter_mut().enumerate() {
1199            if self.plot_axes_indices.get(plot_index).copied().unwrap_or(0) != axes_index {
1200                continue;
1201            }
1202            let color = colors[series % colors.len()];
1203            match plot {
1204                PlotElement::Line(line) => {
1205                    line.set_color(color);
1206                    series += 1;
1207                }
1208                PlotElement::Scatter(scatter) => {
1209                    scatter.set_color(color);
1210                    scatter.set_edge_color(color);
1211                    series += 1;
1212                }
1213                PlotElement::Bar(bar) => {
1214                    bar.set_color(color);
1215                    series += 1;
1216                }
1217                PlotElement::Stairs(stairs) => {
1218                    stairs.color = color;
1219                    series += 1;
1220                }
1221                PlotElement::Stem(stem) => {
1222                    stem.color = color;
1223                    series += 1;
1224                }
1225                PlotElement::Area(area) => {
1226                    area.color = Vec4::new(color.x, color.y, color.z, area.color.w);
1227                    series += 1;
1228                }
1229                PlotElement::ErrorBar(errorbar) => {
1230                    errorbar.color = color;
1231                    series += 1;
1232                }
1233                PlotElement::Quiver(quiver) => {
1234                    quiver.color = color;
1235                    series += 1;
1236                }
1237                PlotElement::Line3(line) => {
1238                    line.color = color;
1239                    series += 1;
1240                }
1241                PlotElement::Scatter3(scatter) => {
1242                    *scatter = scatter.clone().with_color(color);
1243                    scatter.edge_color = color;
1244                    series += 1;
1245                }
1246                _ => {}
1247            }
1248        }
1249    }
1250
1251    fn total_axes(&self) -> usize {
1252        self.axes_rows.max(1) * self.axes_cols.max(1)
1253    }
1254
1255    fn normalize_axes_index(&self, axes_index: usize) -> usize {
1256        let total = self.axes_count();
1257        axes_index.min(total - 1)
1258    }
1259
1260    fn push_plot(&mut self, element: PlotElement, axes_index: usize) -> usize {
1261        let idx = self.normalize_axes_index(axes_index);
1262        self.plots.push(element);
1263        self.plot_axes_indices.push(idx);
1264        self.dirty = true;
1265        self.plots.len() - 1
1266    }
1267
1268    /// Add an already-built plot element to the figure.
1269    pub fn add_plot_element_on_axes(&mut self, element: PlotElement, axes_index: usize) -> usize {
1270        self.push_plot(element, axes_index)
1271    }
1272
1273    /// Add a line plot to the figure
1274    pub fn add_line_plot(&mut self, plot: LinePlot) -> usize {
1275        self.add_line_plot_on_axes(plot, 0)
1276    }
1277
1278    pub fn add_line_plot_on_axes(&mut self, plot: LinePlot, axes_index: usize) -> usize {
1279        self.push_plot(PlotElement::Line(plot), axes_index)
1280    }
1281
1282    pub fn add_reference_line_on_axes(&mut self, plot: ReferenceLine, axes_index: usize) -> usize {
1283        self.push_plot(PlotElement::ReferenceLine(plot), axes_index)
1284    }
1285
1286    /// Add a scatter plot to the figure
1287    pub fn add_scatter_plot(&mut self, plot: ScatterPlot) -> usize {
1288        self.add_scatter_plot_on_axes(plot, 0)
1289    }
1290
1291    pub fn add_scatter_plot_on_axes(&mut self, plot: ScatterPlot, axes_index: usize) -> usize {
1292        self.push_plot(PlotElement::Scatter(plot), axes_index)
1293    }
1294
1295    /// Add a bar chart to the figure
1296    pub fn add_bar_chart(&mut self, plot: BarChart) -> usize {
1297        self.add_bar_chart_on_axes(plot, 0)
1298    }
1299
1300    pub fn add_bar_chart_on_axes(&mut self, plot: BarChart, axes_index: usize) -> usize {
1301        self.push_plot(PlotElement::Bar(plot), axes_index)
1302    }
1303
1304    /// Add an errorbar plot
1305    pub fn add_errorbar(&mut self, plot: ErrorBar) -> usize {
1306        self.add_errorbar_on_axes(plot, 0)
1307    }
1308
1309    pub fn add_errorbar_on_axes(&mut self, plot: ErrorBar, axes_index: usize) -> usize {
1310        self.push_plot(PlotElement::ErrorBar(Box::new(plot)), axes_index)
1311    }
1312
1313    /// Add a stairs plot
1314    pub fn add_stairs_plot(&mut self, plot: StairsPlot) -> usize {
1315        self.add_stairs_plot_on_axes(plot, 0)
1316    }
1317
1318    pub fn add_stairs_plot_on_axes(&mut self, plot: StairsPlot, axes_index: usize) -> usize {
1319        self.push_plot(PlotElement::Stairs(plot), axes_index)
1320    }
1321
1322    /// Add a stem plot
1323    pub fn add_stem_plot(&mut self, plot: StemPlot) -> usize {
1324        self.add_stem_plot_on_axes(plot, 0)
1325    }
1326
1327    pub fn add_stem_plot_on_axes(&mut self, plot: StemPlot, axes_index: usize) -> usize {
1328        self.push_plot(PlotElement::Stem(plot), axes_index)
1329    }
1330
1331    /// Add an area plot
1332    pub fn add_area_plot(&mut self, plot: AreaPlot) -> usize {
1333        self.add_area_plot_on_axes(plot, 0)
1334    }
1335
1336    pub fn add_area_plot_on_axes(&mut self, plot: AreaPlot, axes_index: usize) -> usize {
1337        self.push_plot(PlotElement::Area(plot), axes_index)
1338    }
1339
1340    pub fn add_quiver_plot(&mut self, plot: QuiverPlot) -> usize {
1341        self.add_quiver_plot_on_axes(plot, 0)
1342    }
1343
1344    pub fn add_quiver_plot_on_axes(&mut self, plot: QuiverPlot, axes_index: usize) -> usize {
1345        self.push_plot(PlotElement::Quiver(plot), axes_index)
1346    }
1347
1348    pub fn add_pie_chart(&mut self, plot: PieChart) -> usize {
1349        self.add_pie_chart_on_axes(plot, 0)
1350    }
1351
1352    pub fn add_pie_chart_on_axes(&mut self, plot: PieChart, axes_index: usize) -> usize {
1353        self.push_plot(PlotElement::Pie(plot), axes_index)
1354    }
1355
1356    /// Add a surface plot to the figure
1357    pub fn add_surface_plot(&mut self, plot: SurfacePlot) -> usize {
1358        self.add_surface_plot_on_axes(plot, 0)
1359    }
1360
1361    pub fn add_surface_plot_on_axes(&mut self, plot: SurfacePlot, axes_index: usize) -> usize {
1362        self.push_plot(PlotElement::Surface(plot), axes_index)
1363    }
1364
1365    pub fn add_patch_plot(&mut self, plot: PatchPlot) -> usize {
1366        self.add_patch_plot_on_axes(plot, 0)
1367    }
1368
1369    pub fn add_patch_plot_on_axes(&mut self, plot: PatchPlot, axes_index: usize) -> usize {
1370        self.push_plot(PlotElement::Patch(plot), axes_index)
1371    }
1372
1373    pub fn add_mesh_plot(&mut self, plot: MeshPlot) -> usize {
1374        self.add_mesh_plot_on_axes(plot, 0)
1375    }
1376
1377    pub fn add_mesh_plot_on_axes(&mut self, plot: MeshPlot, axes_index: usize) -> usize {
1378        self.push_plot(PlotElement::Mesh(Box::new(plot)), axes_index)
1379    }
1380
1381    pub fn add_line3_plot(&mut self, plot: Line3Plot) -> usize {
1382        self.add_line3_plot_on_axes(plot, self.active_axes_index)
1383    }
1384
1385    pub fn add_line3_plot_on_axes(&mut self, plot: Line3Plot, axes_index: usize) -> usize {
1386        self.push_plot(PlotElement::Line3(plot), axes_index)
1387    }
1388
1389    /// Add a 3D scatter plot to the figure
1390    pub fn add_scatter3_plot(&mut self, plot: Scatter3Plot) -> usize {
1391        self.add_scatter3_plot_on_axes(plot, 0)
1392    }
1393
1394    pub fn add_scatter3_plot_on_axes(&mut self, plot: Scatter3Plot, axes_index: usize) -> usize {
1395        self.push_plot(PlotElement::Scatter3(plot), axes_index)
1396    }
1397
1398    pub fn add_contour_plot(&mut self, plot: ContourPlot) -> usize {
1399        self.add_contour_plot_on_axes(plot, 0)
1400    }
1401
1402    pub fn add_contour_plot_on_axes(&mut self, plot: ContourPlot, axes_index: usize) -> usize {
1403        self.push_plot(PlotElement::Contour(plot), axes_index)
1404    }
1405
1406    pub fn add_contour_fill_plot(&mut self, plot: ContourFillPlot) -> usize {
1407        self.add_contour_fill_plot_on_axes(plot, 0)
1408    }
1409
1410    pub fn add_contour_fill_plot_on_axes(
1411        &mut self,
1412        plot: ContourFillPlot,
1413        axes_index: usize,
1414    ) -> usize {
1415        self.push_plot(PlotElement::ContourFill(plot), axes_index)
1416    }
1417
1418    /// Remove a plot by index
1419    pub fn remove_plot(&mut self, index: usize) -> Result<(), String> {
1420        if index >= self.plots.len() {
1421            return Err(format!("Plot index {index} out of bounds"));
1422        }
1423        self.plots.remove(index);
1424        self.plot_axes_indices.remove(index);
1425        self.dirty = true;
1426        Ok(())
1427    }
1428
1429    /// Clear all plots
1430    pub fn clear(&mut self) {
1431        self.plots.clear();
1432        self.plot_axes_indices.clear();
1433        self.dirty = true;
1434    }
1435
1436    /// Clear all plots assigned to a specific axes index
1437    pub fn clear_axes(&mut self, axes_index: usize) {
1438        let mut i = 0usize;
1439        while i < self.plots.len() {
1440            let ax = *self.plot_axes_indices.get(i).unwrap_or(&0);
1441            if ax == axes_index {
1442                self.plots.remove(i);
1443                self.plot_axes_indices.remove(i);
1444            } else {
1445                i += 1;
1446            }
1447        }
1448        self.ensure_axes_metadata_capacity(axes_index + 1);
1449        if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1450            meta.world_text_annotations.clear();
1451        }
1452        self.dirty = true;
1453    }
1454
1455    /// Get the number of plots
1456    pub fn len(&self) -> usize {
1457        self.plots.len()
1458    }
1459
1460    /// Check if figure has no plots
1461    pub fn is_empty(&self) -> bool {
1462        self.plots.is_empty()
1463    }
1464
1465    /// Get an iterator over all plots in this figure
1466    pub fn plots(&self) -> impl Iterator<Item = &PlotElement> {
1467        self.plots.iter()
1468    }
1469
1470    /// Get a mutable reference to a plot
1471    pub fn get_plot_mut(&mut self, index: usize) -> Option<&mut PlotElement> {
1472        self.dirty = true;
1473        self.plots.get_mut(index)
1474    }
1475
1476    /// Get the combined bounds of all visible plots
1477    pub fn bounds(&mut self) -> BoundingBox {
1478        if self.dirty || self.bounds.is_none() {
1479            self.compute_bounds();
1480        }
1481        self.bounds.unwrap()
1482    }
1483
1484    /// Get the combined bounds of visible data plots assigned to one axes.
1485    ///
1486    /// Reference lines are intentionally excluded so helpers that need the axes'
1487    /// data extent do not feed previously-created reference annotations back into
1488    /// their own range calculations.
1489    pub fn data_bounds_for_axes(&mut self, axes_index: usize) -> BoundingBox {
1490        let axes_index = self.normalize_axes_index(axes_index);
1491        let mut combined_bounds = None;
1492
1493        for (plot_index, plot) in self.plots.iter_mut().enumerate() {
1494            if self.plot_axes_indices.get(plot_index).copied().unwrap_or(0) != axes_index {
1495                continue;
1496            }
1497            if !plot.is_visible() || matches!(plot, PlotElement::ReferenceLine(_)) {
1498                continue;
1499            }
1500
1501            let plot_bounds = plot.bounds();
1502            combined_bounds = match combined_bounds {
1503                None => Some(plot_bounds),
1504                Some(existing) => Some(existing.union(&plot_bounds)),
1505            };
1506        }
1507
1508        combined_bounds.unwrap_or_default()
1509    }
1510
1511    /// Compute the combined bounds from all plots
1512    fn compute_bounds(&mut self) {
1513        if self.plots.is_empty() {
1514            self.bounds = Some(BoundingBox::default());
1515            return;
1516        }
1517
1518        let mut combined_bounds = None;
1519        let mut reference_lines = Vec::new();
1520
1521        for plot in &mut self.plots {
1522            if !plot.is_visible() {
1523                continue;
1524            }
1525            if let PlotElement::ReferenceLine(reference_line) = plot {
1526                reference_lines.push(reference_line.clone());
1527                continue;
1528            }
1529
1530            let plot_bounds = plot.bounds();
1531
1532            combined_bounds = match combined_bounds {
1533                None => Some(plot_bounds),
1534                Some(existing) => Some(existing.union(&plot_bounds)),
1535            };
1536        }
1537
1538        for line in reference_lines {
1539            let mut point_bounds = line.coordinate_bounds();
1540            if let Some(existing) = combined_bounds {
1541                match line.orientation {
1542                    ReferenceLineOrientation::Vertical => {
1543                        point_bounds.min.y = existing.min.y;
1544                        point_bounds.max.y = existing.max.y;
1545                    }
1546                    ReferenceLineOrientation::Horizontal => {
1547                        point_bounds.min.x = existing.min.x;
1548                        point_bounds.max.x = existing.max.x;
1549                    }
1550                }
1551            } else {
1552                let (x_range, y_range) =
1553                    Self::reference_line_ranges(self.x_limits, self.y_limits, None, None, &line);
1554                point_bounds.min.x = x_range.0 as f32;
1555                point_bounds.max.x = x_range.1 as f32;
1556                point_bounds.min.y = y_range.0 as f32;
1557                point_bounds.max.y = y_range.1 as f32;
1558            }
1559            combined_bounds = match combined_bounds {
1560                None => Some(point_bounds),
1561                Some(existing) => Some(existing.union(&point_bounds)),
1562            };
1563        }
1564
1565        self.bounds = combined_bounds.or_else(|| Some(BoundingBox::default()));
1566        self.dirty = false;
1567    }
1568
1569    /// Generate all render data for all visible plots
1570    pub fn render_data(&mut self) -> Vec<RenderData> {
1571        self.render_data_with_viewport(None)
1572    }
1573
1574    /// Generate all render data for all visible plots, optionally providing the
1575    /// pixel size of the target viewport (width, height).
1576    ///
1577    /// Some plot types (notably thick 2D lines) need a viewport hint to convert
1578    /// pixel-based style parameters (e.g. `LineWidth`) into data-space geometry.
1579    pub fn render_data_with_viewport(
1580        &mut self,
1581        viewport_px: Option<(u32, u32)>,
1582    ) -> Vec<RenderData> {
1583        self.render_data_with_viewport_and_gpu(viewport_px, None)
1584    }
1585
1586    pub fn render_data_with_viewport_and_gpu(
1587        &mut self,
1588        viewport_px: Option<(u32, u32)>,
1589        gpu: Option<&GpuPackContext<'_>>,
1590    ) -> Vec<RenderData> {
1591        self.render_data_with_axes_with_viewport_and_gpu(viewport_px, None, None, gpu)
1592            .into_iter()
1593            .map(|(_, render_data)| render_data)
1594            .collect()
1595    }
1596
1597    pub fn render_data_with_axes_with_viewport_and_gpu(
1598        &mut self,
1599        viewport_px: Option<(u32, u32)>,
1600        axes_viewports_px: Option<&[(u32, u32)]>,
1601        axes_view_bounds: Option<PerAxesViewBoundsRef<'_>>,
1602        gpu: Option<&GpuPackContext<'_>>,
1603    ) -> Vec<(usize, RenderData)> {
1604        fn push_with_optional_markers(
1605            out: &mut Vec<(usize, RenderData)>,
1606            axes_index: usize,
1607            render_data: RenderData,
1608            marker_data: Option<RenderData>,
1609        ) {
1610            out.push((axes_index, render_data));
1611            if let Some(marker_data) = marker_data {
1612                out.push((axes_index, marker_data));
1613            }
1614        }
1615
1616        let reference_base_bounds = self.reference_base_bounds_by_axes();
1617        let mut out = Vec::new();
1618        for (plot_idx, p) in self.plots.iter_mut().enumerate() {
1619            if !p.is_visible() {
1620                continue;
1621            }
1622            let axes_index = self.plot_axes_indices.get(plot_idx).copied().unwrap_or(0);
1623            let axes_view_bounds = axes_view_bounds
1624                .and_then(|bounds| bounds.get(axes_index).copied())
1625                .flatten();
1626            let hidden_line_removal = self
1627                .axes_metadata
1628                .get(axes_index)
1629                .map(|meta| meta.hidden_line_removal)
1630                .unwrap_or(true);
1631            if let PlotElement::Surface(s) = p {
1632                if let Some(meta) = self.axes_metadata.get(axes_index) {
1633                    s.set_color_limits(meta.color_limits);
1634                    *s = s.clone().with_colormap(meta.colormap.clone());
1635                }
1636            }
1637
1638            match p {
1639                PlotElement::Line(plot) => {
1640                    let axes_viewport_px = axes_viewports_px
1641                        .and_then(|viewports| viewports.get(axes_index).copied())
1642                        .or(viewport_px);
1643                    trace!(
1644                        target: "runmat_plot",
1645                        "figure: render_data line viewport_px={:?} axes_index={} axes_viewport_px={:?} axes_view_bounds={:?} gpu_ctx_present={} gpu_line_inputs_present={} gpu_vertices_present={}",
1646                        viewport_px,
1647                        axes_index,
1648                        axes_viewport_px,
1649                        axes_view_bounds,
1650                        gpu.is_some(),
1651                        plot.has_gpu_line_inputs(),
1652                        plot.has_gpu_vertices()
1653                    );
1654                    push_with_optional_markers(
1655                        &mut out,
1656                        axes_index,
1657                        plot.render_data_with_viewport_gpu(axes_viewport_px, axes_view_bounds, gpu),
1658                        plot.marker_render_data(),
1659                    );
1660                }
1661                PlotElement::ErrorBar(plot) => {
1662                    push_with_optional_markers(
1663                        &mut out,
1664                        axes_index,
1665                        plot.render_data_with_viewport_gpu(
1666                            axes_viewports_px
1667                                .and_then(|viewports| viewports.get(axes_index).copied())
1668                                .or(viewport_px),
1669                            gpu,
1670                        ),
1671                        plot.marker_render_data(),
1672                    );
1673                }
1674                PlotElement::Stairs(plot) => {
1675                    push_with_optional_markers(
1676                        &mut out,
1677                        axes_index,
1678                        plot.render_data_with_viewport(
1679                            axes_viewports_px
1680                                .and_then(|viewports| viewports.get(axes_index).copied())
1681                                .or(viewport_px),
1682                        ),
1683                        plot.marker_render_data(),
1684                    );
1685                }
1686                PlotElement::Stem(plot) => {
1687                    push_with_optional_markers(
1688                        &mut out,
1689                        axes_index,
1690                        plot.render_data_with_viewport(
1691                            axes_viewports_px
1692                                .and_then(|viewports| viewports.get(axes_index).copied())
1693                                .or(viewport_px),
1694                        ),
1695                        plot.marker_render_data(),
1696                    );
1697                }
1698                PlotElement::Contour(plot) => out.push((
1699                    axes_index,
1700                    plot.render_data_with_viewport(
1701                        axes_viewports_px
1702                            .and_then(|viewports| viewports.get(axes_index).copied())
1703                            .or(viewport_px),
1704                    ),
1705                )),
1706                PlotElement::ReferenceLine(plot) => {
1707                    let (x_range, y_range) = Self::reference_line_ranges(
1708                        self.x_limits,
1709                        self.y_limits,
1710                        self.axes_metadata.get(axes_index),
1711                        reference_base_bounds.get(axes_index).copied().flatten(),
1712                        plot,
1713                    );
1714                    out.push((
1715                        axes_index,
1716                        plot.render_data_with_range(
1717                            x_range,
1718                            y_range,
1719                            axes_viewports_px
1720                                .and_then(|viewports| viewports.get(axes_index).copied())
1721                                .or(viewport_px),
1722                        ),
1723                    ));
1724                }
1725                PlotElement::Patch(plot) => {
1726                    out.push((axes_index, plot.render_data()));
1727                    if let Some(edge_data) = plot.edge_render_data_with_viewport(
1728                        axes_viewports_px
1729                            .and_then(|viewports| viewports.get(axes_index).copied())
1730                            .or(viewport_px),
1731                    ) {
1732                        out.push((axes_index, edge_data));
1733                    }
1734                }
1735                PlotElement::Mesh(plot) => {
1736                    out.push((axes_index, plot.render_data()));
1737                    if let Some(mut edge_data) = plot.edge_render_data() {
1738                        if !hidden_line_removal {
1739                            edge_data.pipeline_type = crate::core::PipelineType::LinesNoDepth;
1740                        }
1741                        out.push((axes_index, edge_data));
1742                    }
1743                    if let Some(vector_data) = plot.vector_render_data() {
1744                        out.push((axes_index, vector_data));
1745                    }
1746                }
1747                PlotElement::Line3(plot) => out.push((
1748                    axes_index,
1749                    plot.render_data_with_viewport_gpu(
1750                        axes_viewports_px
1751                            .and_then(|viewports| viewports.get(axes_index).copied())
1752                            .or(viewport_px),
1753                        self.axes_metadata.get(axes_index).and_then(|meta| {
1754                            match (meta.view_azimuth_deg, meta.view_elevation_deg) {
1755                                (Some(az), Some(el)) => Some((az, el)),
1756                                _ => None,
1757                            }
1758                        }),
1759                        gpu,
1760                    ),
1761                )),
1762                PlotElement::Surface(plot) => {
1763                    let mut render_data = plot.render_data();
1764                    if !hidden_line_removal
1765                        && render_data.pipeline_type == crate::core::PipelineType::Lines
1766                    {
1767                        render_data.pipeline_type = crate::core::PipelineType::LinesNoDepth;
1768                    }
1769                    out.push((axes_index, render_data));
1770                }
1771                _ => out.push((axes_index, p.render_data())),
1772            }
1773        }
1774        out
1775    }
1776
1777    fn reference_base_bounds_by_axes(&mut self) -> Vec<Option<BoundingBox>> {
1778        let axes_count = self.total_axes().max(1);
1779        let mut bounds: Vec<Option<BoundingBox>> = vec![None; axes_count];
1780        for (plot_idx, plot) in self.plots.iter_mut().enumerate() {
1781            if !plot.is_visible() || matches!(plot, PlotElement::ReferenceLine(_)) {
1782                continue;
1783            }
1784            let axes_index = self
1785                .plot_axes_indices
1786                .get(plot_idx)
1787                .copied()
1788                .unwrap_or(0)
1789                .min(axes_count - 1);
1790            let plot_bounds = plot.bounds();
1791            bounds[axes_index] = Some(match bounds[axes_index] {
1792                None => plot_bounds,
1793                Some(existing) => existing.union(&plot_bounds),
1794            });
1795        }
1796        bounds
1797    }
1798
1799    fn reference_line_ranges(
1800        x_limits: Option<(f64, f64)>,
1801        y_limits: Option<(f64, f64)>,
1802        meta: Option<&AxesMetadata>,
1803        base: Option<BoundingBox>,
1804        line: &ReferenceLine,
1805    ) -> ((f64, f64), (f64, f64)) {
1806        let x_range = x_limits
1807            .or_else(|| meta.and_then(|m| m.x_limits))
1808            .or_else(|| base.map(|b| (b.min.x as f64, b.max.x as f64)))
1809            .unwrap_or(match line.orientation {
1810                ReferenceLineOrientation::Vertical => (line.value - 0.5, line.value + 0.5),
1811                ReferenceLineOrientation::Horizontal => (0.0, 1.0),
1812            });
1813        let y_range = y_limits
1814            .or_else(|| meta.and_then(|m| m.y_limits))
1815            .or_else(|| base.map(|b| (b.min.y as f64, b.max.y as f64)))
1816            .unwrap_or(match line.orientation {
1817                ReferenceLineOrientation::Vertical => (0.0, 1.0),
1818                ReferenceLineOrientation::Horizontal => (line.value - 0.5, line.value + 0.5),
1819            });
1820        (
1821            normalize_reference_range(x_range),
1822            normalize_reference_range(y_range),
1823        )
1824    }
1825
1826    /// Get legend entries for all labeled plots
1827    pub fn legend_entries(&self) -> Vec<LegendEntry> {
1828        let mut entries = Vec::new();
1829
1830        for plot in &self.plots {
1831            if plot.is_hidden_from_legend() {
1832                continue;
1833            }
1834            if let Some(label) = plot.label() {
1835                entries.push(LegendEntry {
1836                    label,
1837                    color: plot.color(),
1838                    plot_type: plot.plot_type(),
1839                });
1840            }
1841        }
1842
1843        entries
1844    }
1845
1846    pub fn legend_entries_for_axes(&self, axes_index: usize) -> Vec<LegendEntry> {
1847        let mut entries = Vec::new();
1848        for (plot_idx, plot) in self.plots.iter().enumerate() {
1849            let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
1850            if plot_axes != axes_index {
1851                continue;
1852            }
1853            match plot {
1854                PlotElement::Pie(pie) => {
1855                    for slice in pie.slice_meta() {
1856                        entries.push(LegendEntry {
1857                            label: slice.label,
1858                            color: slice.color,
1859                            plot_type: plot.plot_type(),
1860                        });
1861                    }
1862                }
1863                _ => {
1864                    if plot.is_hidden_from_legend() {
1865                        continue;
1866                    }
1867                    if let Some(label) = plot.label() {
1868                        entries.push(LegendEntry {
1869                            label,
1870                            color: plot.color(),
1871                            plot_type: plot.plot_type(),
1872                        });
1873                    }
1874                }
1875            }
1876        }
1877        entries
1878    }
1879
1880    pub fn pie_labels_for_axes(&self, axes_index: usize) -> Vec<PieLabelEntry> {
1881        let mut out = Vec::new();
1882        for (plot_idx, plot) in self.plots.iter().enumerate() {
1883            let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
1884            if plot_axes != axes_index {
1885                continue;
1886            }
1887            if let PlotElement::Pie(pie) = plot {
1888                for slice in pie.slice_meta() {
1889                    out.push(PieLabelEntry {
1890                        label: slice.label,
1891                        position: glam::Vec2::new(
1892                            slice.mid_angle.cos() * 1.15 + slice.offset.x,
1893                            slice.mid_angle.sin() * 1.15 + slice.offset.y,
1894                        ),
1895                    });
1896                }
1897            }
1898        }
1899        out
1900    }
1901
1902    /// Assign labels to visible plots in order
1903    pub fn set_labels(&mut self, labels: &[String]) {
1904        self.set_labels_for_axes(self.active_axes_index, labels);
1905    }
1906
1907    pub fn set_labels_for_axes(&mut self, axes_index: usize, labels: &[String]) {
1908        let mut idx = 0usize;
1909        for (plot_idx, plot) in self.plots.iter_mut().enumerate() {
1910            let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
1911            if plot_axes != axes_index {
1912                continue;
1913            }
1914            if !plot.is_visible() {
1915                continue;
1916            }
1917            if idx >= labels.len() {
1918                break;
1919            }
1920            match plot {
1921                PlotElement::Pie(pie) => {
1922                    let remaining = &labels[idx..];
1923                    if remaining.len() >= pie.values.len() {
1924                        pie.set_slice_labels(remaining[..pie.values.len()].to_vec());
1925                        idx += pie.values.len();
1926                    } else {
1927                        pie.set_slice_labels(remaining.to_vec());
1928                        idx = labels.len();
1929                    }
1930                }
1931                _ => {
1932                    plot.set_label(Some(labels[idx].clone()));
1933                    idx += 1;
1934                }
1935            }
1936        }
1937        self.dirty = true;
1938    }
1939
1940    /// Get figure statistics
1941    pub fn statistics(&self) -> FigureStatistics {
1942        let plot_counts = self.plots.iter().fold(HashMap::new(), |mut acc, plot| {
1943            let plot_type = plot.plot_type();
1944            *acc.entry(plot_type).or_insert(0) += 1;
1945            acc
1946        });
1947
1948        let total_memory: usize = self
1949            .plots
1950            .iter()
1951            .map(|plot| plot.estimated_memory_usage())
1952            .sum();
1953
1954        let visible_count = self.plots.iter().filter(|plot| plot.is_visible()).count();
1955
1956        FigureStatistics {
1957            total_plots: self.plots.len(),
1958            visible_plots: visible_count,
1959            plot_type_counts: plot_counts,
1960            total_memory_usage: total_memory,
1961            has_legend: self.legend_enabled && !self.legend_entries().is_empty(),
1962        }
1963    }
1964
1965    /// If the figure contains a bar/barh plot, return its categorical axis labels.
1966    /// Returns (is_x_axis, labels) where is_x_axis=true means X is categorical (vertical bars),
1967    /// false means Y is categorical (horizontal bars).
1968    pub fn categorical_axis_labels(&self) -> Option<(bool, Vec<String>)> {
1969        for plot in &self.plots {
1970            if let PlotElement::Bar(b) = plot {
1971                if b.histogram_bin_edges().is_some() {
1972                    continue;
1973                }
1974                let is_x = matches!(b.orientation, crate::plots::bar::Orientation::Vertical);
1975                return Some((is_x, b.labels.clone()));
1976            }
1977        }
1978        None
1979    }
1980
1981    pub fn categorical_axis_labels_for_axes(
1982        &self,
1983        axes_index: usize,
1984    ) -> Option<(bool, Vec<String>)> {
1985        for (plot_idx, plot) in self.plots.iter().enumerate() {
1986            let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
1987            if plot_axes != axes_index {
1988                continue;
1989            }
1990            if let PlotElement::Bar(b) = plot {
1991                if b.histogram_bin_edges().is_some() {
1992                    continue;
1993                }
1994                let is_x = matches!(b.orientation, crate::plots::bar::Orientation::Vertical);
1995                return Some((is_x, b.labels.clone()));
1996            }
1997        }
1998        None
1999    }
2000
2001    pub fn x_axis_tick_labels_for_axes(&self, axes_index: usize) -> Option<Vec<String>> {
2002        self.axes_metadata
2003            .get(axes_index)
2004            .and_then(|meta| meta.x_tick_labels.clone())
2005    }
2006
2007    pub fn y_axis_tick_labels_for_axes(&self, axes_index: usize) -> Option<Vec<String>> {
2008        self.axes_metadata
2009            .get(axes_index)
2010            .and_then(|meta| meta.y_tick_labels.clone())
2011    }
2012
2013    pub fn x_axis_tick_format_for_axes(&self, axes_index: usize) -> Option<String> {
2014        self.axes_metadata
2015            .get(axes_index)
2016            .and_then(|meta| meta.x_tick_format.clone())
2017    }
2018
2019    pub fn y_axis_tick_format_for_axes(&self, axes_index: usize) -> Option<String> {
2020        self.axes_metadata
2021            .get(axes_index)
2022            .and_then(|meta| meta.y_tick_format.clone())
2023    }
2024
2025    pub fn x_axis_tick_label_rotation_for_axes(&self, axes_index: usize) -> Option<f64> {
2026        self.axes_metadata
2027            .get(axes_index)
2028            .and_then(|meta| meta.x_tick_label_rotation)
2029    }
2030
2031    pub fn y_axis_tick_label_rotation_for_axes(&self, axes_index: usize) -> Option<f64> {
2032        self.axes_metadata
2033            .get(axes_index)
2034            .and_then(|meta| meta.y_tick_label_rotation)
2035    }
2036
2037    pub fn x_axis_ticks_for_axes(&self, axes_index: usize) -> Option<Vec<f64>> {
2038        self.axes_metadata
2039            .get(axes_index)
2040            .and_then(|meta| meta.x_ticks.clone())
2041    }
2042
2043    pub fn y_axis_ticks_for_axes(&self, axes_index: usize) -> Option<Vec<f64>> {
2044        self.axes_metadata
2045            .get(axes_index)
2046            .and_then(|meta| meta.y_ticks.clone())
2047    }
2048
2049    pub fn histogram_axis_edges_for_axes(&self, axes_index: usize) -> Option<(bool, Vec<f64>)> {
2050        for (plot_idx, plot) in self.plots.iter().enumerate() {
2051            let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
2052            if plot_axes != axes_index {
2053                continue;
2054            }
2055            if let PlotElement::Bar(b) = plot {
2056                if let Some(edges) = b.histogram_bin_edges() {
2057                    let is_x = matches!(b.orientation, crate::plots::bar::Orientation::Vertical);
2058                    return Some((is_x, edges.to_vec()));
2059                }
2060            }
2061        }
2062        None
2063    }
2064}
2065
2066impl Default for Figure {
2067    fn default() -> Self {
2068        Self::new()
2069    }
2070}
2071
2072fn normalize_reference_range(range: (f64, f64)) -> (f64, f64) {
2073    let (mut lo, mut hi) = range;
2074    if !lo.is_finite() || !hi.is_finite() {
2075        return (0.0, 1.0);
2076    }
2077    if hi < lo {
2078        std::mem::swap(&mut lo, &mut hi);
2079    }
2080    if (hi - lo).abs() < f64::EPSILON {
2081        let pad = lo.abs().max(1.0) * 0.5;
2082        return (lo - pad, hi + pad);
2083    }
2084    (lo, hi)
2085}
2086
2087impl PlotElement {
2088    /// Check if the plot is visible
2089    pub fn is_visible(&self) -> bool {
2090        match self {
2091            PlotElement::Line(plot) => plot.visible,
2092            PlotElement::Scatter(plot) => plot.visible,
2093            PlotElement::Bar(plot) => plot.visible,
2094            PlotElement::ErrorBar(plot) => plot.visible,
2095            PlotElement::Stairs(plot) => plot.visible,
2096            PlotElement::Stem(plot) => plot.visible,
2097            PlotElement::Area(plot) => plot.visible,
2098            PlotElement::Quiver(plot) => plot.visible,
2099            PlotElement::Pie(plot) => plot.visible,
2100            PlotElement::Surface(plot) => plot.visible,
2101            PlotElement::Mesh(plot) => plot.is_visible(),
2102            PlotElement::Patch(plot) => plot.is_visible(),
2103            PlotElement::Line3(plot) => plot.visible,
2104            PlotElement::Scatter3(plot) => plot.visible,
2105            PlotElement::Contour(plot) => plot.visible,
2106            PlotElement::ContourFill(plot) => plot.visible,
2107            PlotElement::ReferenceLine(plot) => plot.visible,
2108        }
2109    }
2110
2111    /// Get the plot's label
2112    pub fn label(&self) -> Option<String> {
2113        match self {
2114            PlotElement::Line(plot) => plot.label.clone(),
2115            PlotElement::Scatter(plot) => plot.label.clone(),
2116            PlotElement::Bar(plot) => plot.label.clone(),
2117            PlotElement::ErrorBar(plot) => plot.label.clone(),
2118            PlotElement::Stairs(plot) => plot.label.clone(),
2119            PlotElement::Stem(plot) => plot.label.clone(),
2120            PlotElement::Area(plot) => plot.label.clone(),
2121            PlotElement::Quiver(plot) => plot.label.clone(),
2122            PlotElement::Pie(plot) => plot.label.clone(),
2123            PlotElement::Surface(plot) => plot.label.clone(),
2124            PlotElement::Mesh(plot) => plot.label().map(str::to_string),
2125            PlotElement::Patch(plot) => plot.label().map(str::to_string),
2126            PlotElement::Line3(plot) => plot.label.clone(),
2127            PlotElement::Scatter3(plot) => plot.label.clone(),
2128            PlotElement::Contour(plot) => plot.label.clone(),
2129            PlotElement::ContourFill(plot) => plot.label.clone(),
2130            PlotElement::ReferenceLine(plot) => plot.label_for_legend(),
2131        }
2132    }
2133
2134    fn is_hidden_from_legend(&self) -> bool {
2135        matches!(
2136            self,
2137            PlotElement::Line(plot) if plot.handle_visibility.eq_ignore_ascii_case("off")
2138        )
2139    }
2140
2141    /// Mutate label
2142    pub fn set_label(&mut self, label: Option<String>) {
2143        match self {
2144            PlotElement::Line(plot) => plot.label = label,
2145            PlotElement::Scatter(plot) => plot.label = label,
2146            PlotElement::Bar(plot) => plot.label = label,
2147            PlotElement::ErrorBar(plot) => plot.label = label,
2148            PlotElement::Stairs(plot) => plot.label = label,
2149            PlotElement::Stem(plot) => plot.label = label,
2150            PlotElement::Area(plot) => plot.label = label,
2151            PlotElement::Quiver(plot) => plot.label = label,
2152            PlotElement::Pie(plot) => plot.label = label,
2153            PlotElement::Surface(plot) => plot.label = label,
2154            PlotElement::Mesh(plot) => plot.set_label(label),
2155            PlotElement::Patch(plot) => plot.set_label(label),
2156            PlotElement::Line3(plot) => plot.label = label,
2157            PlotElement::Scatter3(plot) => plot.label = label,
2158            PlotElement::Contour(plot) => plot.label = label,
2159            PlotElement::ContourFill(plot) => plot.label = label,
2160            PlotElement::ReferenceLine(plot) => plot.label = label,
2161        }
2162    }
2163
2164    /// Get the plot's primary color
2165    pub fn color(&self) -> Vec4 {
2166        match self {
2167            PlotElement::Line(plot) => plot.color,
2168            PlotElement::Scatter(plot) => plot.color,
2169            PlotElement::Bar(plot) => plot.color,
2170            PlotElement::ErrorBar(plot) => plot.color,
2171            PlotElement::Stairs(plot) => plot.color,
2172            PlotElement::Stem(plot) => plot.color,
2173            PlotElement::Area(plot) => plot.color,
2174            PlotElement::Quiver(plot) => plot.color,
2175            PlotElement::Pie(_plot) => Vec4::new(1.0, 1.0, 1.0, 1.0),
2176            PlotElement::Surface(_plot) => Vec4::new(1.0, 1.0, 1.0, 1.0),
2177            PlotElement::Mesh(plot) => plot.effective_face_color(),
2178            PlotElement::Patch(plot) => plot.effective_face_color(),
2179            PlotElement::Line3(plot) => plot.color,
2180            PlotElement::Scatter3(plot) => plot.colors.first().copied().unwrap_or(Vec4::ONE),
2181            PlotElement::Contour(_plot) => Vec4::new(1.0, 1.0, 1.0, 1.0),
2182            PlotElement::ContourFill(_plot) => Vec4::new(0.9, 0.9, 0.9, 1.0),
2183            PlotElement::ReferenceLine(plot) => plot.color,
2184        }
2185    }
2186
2187    /// Get the plot type
2188    pub fn plot_type(&self) -> PlotType {
2189        match self {
2190            PlotElement::Line(_) => PlotType::Line,
2191            PlotElement::Scatter(_) => PlotType::Scatter,
2192            PlotElement::Bar(_) => PlotType::Bar,
2193            PlotElement::ErrorBar(_) => PlotType::ErrorBar,
2194            PlotElement::Stairs(_) => PlotType::Stairs,
2195            PlotElement::Stem(_) => PlotType::Stem,
2196            PlotElement::Area(_) => PlotType::Area,
2197            PlotElement::Quiver(_) => PlotType::Quiver,
2198            PlotElement::Pie(_) => PlotType::Pie,
2199            PlotElement::Surface(_) => PlotType::Surface,
2200            PlotElement::Mesh(_) => PlotType::Mesh,
2201            PlotElement::Patch(_) => PlotType::Patch,
2202            PlotElement::Line3(_) => PlotType::Line3,
2203            PlotElement::Scatter3(_) => PlotType::Scatter3,
2204            PlotElement::Contour(_) => PlotType::Contour,
2205            PlotElement::ContourFill(_) => PlotType::ContourFill,
2206            PlotElement::ReferenceLine(_) => PlotType::ReferenceLine,
2207        }
2208    }
2209
2210    /// Get the plot's bounds
2211    pub fn bounds(&mut self) -> BoundingBox {
2212        match self {
2213            PlotElement::Line(plot) => plot.bounds(),
2214            PlotElement::Scatter(plot) => plot.bounds(),
2215            PlotElement::Bar(plot) => plot.bounds(),
2216            PlotElement::ErrorBar(plot) => plot.bounds(),
2217            PlotElement::Stairs(plot) => plot.bounds(),
2218            PlotElement::Stem(plot) => plot.bounds(),
2219            PlotElement::Area(plot) => plot.bounds(),
2220            PlotElement::Quiver(plot) => plot.bounds(),
2221            PlotElement::Pie(plot) => plot.bounds(),
2222            PlotElement::Surface(plot) => plot.bounds(),
2223            PlotElement::Mesh(plot) => plot.bounds(),
2224            PlotElement::Patch(plot) => plot.bounds(),
2225            PlotElement::Line3(plot) => plot.bounds(),
2226            PlotElement::Scatter3(plot) => plot.bounds(),
2227            PlotElement::Contour(plot) => plot.bounds(),
2228            PlotElement::ContourFill(plot) => plot.bounds(),
2229            PlotElement::ReferenceLine(plot) => plot.coordinate_bounds(),
2230        }
2231    }
2232
2233    /// Generate render data for this plot
2234    pub fn render_data(&mut self) -> RenderData {
2235        match self {
2236            PlotElement::Line(plot) => plot.render_data(),
2237            PlotElement::Scatter(plot) => plot.render_data(),
2238            PlotElement::Bar(plot) => plot.render_data(),
2239            PlotElement::ErrorBar(plot) => plot.render_data(),
2240            PlotElement::Stairs(plot) => plot.render_data(),
2241            PlotElement::Stem(plot) => plot.render_data(),
2242            PlotElement::Area(plot) => plot.render_data(),
2243            PlotElement::Quiver(plot) => plot.render_data(),
2244            PlotElement::Pie(plot) => plot.render_data(),
2245            PlotElement::Surface(plot) => plot.render_data(),
2246            PlotElement::Mesh(plot) => plot.render_data(),
2247            PlotElement::Patch(plot) => plot.render_data(),
2248            PlotElement::Line3(plot) => plot.render_data(),
2249            PlotElement::Scatter3(plot) => plot.render_data(),
2250            PlotElement::Contour(plot) => plot.render_data(),
2251            PlotElement::ContourFill(plot) => plot.render_data(),
2252            PlotElement::ReferenceLine(plot) => {
2253                plot.render_data_with_range((0.0, 1.0), (0.0, 1.0), None)
2254            }
2255        }
2256    }
2257
2258    /// Estimate memory usage
2259    pub fn estimated_memory_usage(&self) -> usize {
2260        match self {
2261            PlotElement::Line(plot) => plot.estimated_memory_usage(),
2262            PlotElement::Scatter(plot) => plot.estimated_memory_usage(),
2263            PlotElement::Bar(plot) => plot.estimated_memory_usage(),
2264            PlotElement::ErrorBar(plot) => plot.estimated_memory_usage(),
2265            PlotElement::Stairs(plot) => plot.estimated_memory_usage(),
2266            PlotElement::Stem(plot) => plot.estimated_memory_usage(),
2267            PlotElement::Area(plot) => plot.estimated_memory_usage(),
2268            PlotElement::Quiver(plot) => plot.estimated_memory_usage(),
2269            PlotElement::Pie(plot) => plot.estimated_memory_usage(),
2270            PlotElement::Surface(_plot) => 0,
2271            PlotElement::Mesh(plot) => plot.estimated_memory_usage(),
2272            PlotElement::Patch(plot) => plot.estimated_memory_usage(),
2273            PlotElement::Line3(plot) => plot.estimated_memory_usage(),
2274            PlotElement::Scatter3(plot) => plot.estimated_memory_usage(),
2275            PlotElement::Contour(plot) => plot.estimated_memory_usage(),
2276            PlotElement::ContourFill(plot) => plot.estimated_memory_usage(),
2277            PlotElement::ReferenceLine(plot) => plot.estimated_memory_usage(),
2278        }
2279    }
2280}
2281
2282/// Figure statistics for debugging and optimization
2283#[derive(Debug)]
2284pub struct FigureStatistics {
2285    pub total_plots: usize,
2286    pub visible_plots: usize,
2287    pub plot_type_counts: HashMap<PlotType, usize>,
2288    pub total_memory_usage: usize,
2289    pub has_legend: bool,
2290}
2291
2292/// MATLAB-compatible figure creation utilities
2293pub mod matlab_compat {
2294    use super::*;
2295    use crate::plots::{LinePlot, ScatterPlot};
2296
2297    /// Create a new figure (equivalent to MATLAB's `figure`)
2298    pub fn figure() -> Figure {
2299        Figure::new()
2300    }
2301
2302    /// Create a figure with a title
2303    pub fn figure_with_title<S: Into<String>>(title: S) -> Figure {
2304        Figure::new().with_title(title)
2305    }
2306
2307    /// Add multiple line plots to a figure (`hold on` behavior)
2308    pub fn plot_multiple_lines(
2309        figure: &mut Figure,
2310        data_sets: Vec<(Vec<f64>, Vec<f64>, Option<String>)>,
2311    ) -> Result<Vec<usize>, String> {
2312        let mut indices = Vec::new();
2313
2314        for (i, (x, y, label)) in data_sets.into_iter().enumerate() {
2315            let mut line = LinePlot::new(x, y)?;
2316
2317            // Automatic color cycling (similar to MATLAB)
2318            let colors = [
2319                Vec4::new(0.0, 0.4470, 0.7410, 1.0),    // Blue
2320                Vec4::new(0.8500, 0.3250, 0.0980, 1.0), // Orange
2321                Vec4::new(0.9290, 0.6940, 0.1250, 1.0), // Yellow
2322                Vec4::new(0.4940, 0.1840, 0.5560, 1.0), // Purple
2323                Vec4::new(0.4660, 0.6740, 0.1880, 1.0), // Green
2324                Vec4::new(std::f64::consts::LOG10_2 as f32, 0.7450, 0.9330, 1.0), // Cyan
2325                Vec4::new(0.6350, 0.0780, 0.1840, 1.0), // Red
2326            ];
2327            let color = colors[i % colors.len()];
2328            line.set_color(color);
2329
2330            if let Some(label) = label {
2331                line = line.with_label(label);
2332            }
2333
2334            indices.push(figure.add_line_plot(line));
2335        }
2336
2337        Ok(indices)
2338    }
2339
2340    /// Add multiple scatter plots to a figure
2341    pub fn scatter_multiple(
2342        figure: &mut Figure,
2343        data_sets: Vec<(Vec<f64>, Vec<f64>, Option<String>)>,
2344    ) -> Result<Vec<usize>, String> {
2345        let mut indices = Vec::new();
2346
2347        for (i, (x, y, label)) in data_sets.into_iter().enumerate() {
2348            let mut scatter = ScatterPlot::new(x, y)?;
2349
2350            // Automatic color cycling
2351            let colors = [
2352                Vec4::new(1.0, 0.0, 0.0, 1.0), // Red
2353                Vec4::new(0.0, 1.0, 0.0, 1.0), // Green
2354                Vec4::new(0.0, 0.0, 1.0, 1.0), // Blue
2355                Vec4::new(1.0, 1.0, 0.0, 1.0), // Yellow
2356                Vec4::new(1.0, 0.0, 1.0, 1.0), // Magenta
2357                Vec4::new(0.0, 1.0, 1.0, 1.0), // Cyan
2358                Vec4::new(0.5, 0.5, 0.5, 1.0), // Gray
2359            ];
2360            let color = colors[i % colors.len()];
2361            scatter.set_color(color);
2362
2363            if let Some(label) = label {
2364                scatter = scatter.with_label(label);
2365            }
2366
2367            indices.push(figure.add_scatter_plot(scatter));
2368        }
2369
2370        Ok(indices)
2371    }
2372}
2373
2374#[cfg(test)]
2375mod tests {
2376    use super::*;
2377    use crate::plots::line::LineStyle;
2378    use glam::Vec3;
2379
2380    #[test]
2381    fn test_figure_creation() {
2382        let figure = Figure::new();
2383
2384        assert_eq!(figure.len(), 0);
2385        assert!(figure.is_empty());
2386        assert!(figure.legend_enabled);
2387        assert!(figure.grid_enabled);
2388    }
2389
2390    #[test]
2391    fn test_figure_styling() {
2392        let figure = Figure::new()
2393            .with_title("Test Figure")
2394            .with_sg_title("Overview")
2395            .with_labels("X Axis", "Y Axis")
2396            .with_legend(false)
2397            .with_grid(false);
2398
2399        assert_eq!(figure.title, Some("Test Figure".to_string()));
2400        assert_eq!(figure.sg_title, Some("Overview".to_string()));
2401        assert_eq!(figure.x_label, Some("X Axis".to_string()));
2402        assert_eq!(figure.y_label, Some("Y Axis".to_string()));
2403        assert!(!figure.legend_enabled);
2404        assert!(!figure.grid_enabled);
2405    }
2406
2407    #[test]
2408    fn test_window_title_follows_name_and_number_title() {
2409        let mut figure = Figure::new();
2410        assert_eq!(figure.window_title(Some(7)), "Figure 7");
2411
2412        figure.set_name("demo");
2413        assert_eq!(figure.window_title(Some(7)), "Figure 7: demo");
2414
2415        figure.set_number_title(false);
2416        assert_eq!(figure.window_title(Some(7)), "demo");
2417
2418        figure.set_name("   ");
2419        assert_eq!(figure.window_title(Some(7)), "RunMat Plot");
2420    }
2421
2422    #[test]
2423    fn test_has_any_titles_tracks_super_and_axes_titles() {
2424        let mut figure = Figure::new();
2425        assert!(!figure.has_any_titles());
2426
2427        figure.set_sg_title("Summary");
2428        assert!(figure.has_any_titles());
2429
2430        figure.clear_sg_title();
2431        assert!(!figure.has_any_titles());
2432
2433        figure.set_axes_title(0, "Panel");
2434        assert!(figure.has_any_titles());
2435    }
2436
2437    #[test]
2438    fn test_multiple_line_plots() {
2439        let mut figure = Figure::new();
2440
2441        // Add first line plot
2442        let line1 = LinePlot::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 4.0])
2443            .unwrap()
2444            .with_label("Quadratic");
2445        let index1 = figure.add_line_plot(line1);
2446
2447        // Add second line plot
2448        let line2 = LinePlot::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 2.0])
2449            .unwrap()
2450            .with_style(Vec4::new(1.0, 0.0, 0.0, 1.0), 2.0, LineStyle::Dashed)
2451            .with_label("Linear");
2452        let index2 = figure.add_line_plot(line2);
2453
2454        assert_eq!(figure.len(), 2);
2455        assert_eq!(index1, 0);
2456        assert_eq!(index2, 1);
2457
2458        // Test legend entries
2459        let legend = figure.legend_entries();
2460        assert_eq!(legend.len(), 2);
2461        assert_eq!(legend[0].label, "Quadratic");
2462        assert_eq!(legend[1].label, "Linear");
2463    }
2464
2465    #[test]
2466    fn test_mixed_plot_types() {
2467        let mut figure = Figure::new();
2468
2469        // Add different plot types
2470        let line = LinePlot::new(vec![0.0, 1.0, 2.0], vec![1.0, 2.0, 3.0])
2471            .unwrap()
2472            .with_label("Line");
2473        figure.add_line_plot(line);
2474
2475        let scatter = ScatterPlot::new(vec![0.5, 1.5, 2.5], vec![1.5, 2.5, 3.5])
2476            .unwrap()
2477            .with_label("Scatter");
2478        figure.add_scatter_plot(scatter);
2479
2480        let bar = BarChart::new(vec!["A".to_string(), "B".to_string()], vec![2.0, 4.0])
2481            .unwrap()
2482            .with_label("Bar");
2483        figure.add_bar_chart(bar);
2484
2485        assert_eq!(figure.len(), 3);
2486
2487        // Test render data generation
2488        let render_data = figure.render_data();
2489        assert_eq!(render_data.len(), 3);
2490
2491        // Test statistics
2492        let stats = figure.statistics();
2493        assert_eq!(stats.total_plots, 3);
2494        assert_eq!(stats.visible_plots, 3);
2495        assert!(stats.has_legend);
2496    }
2497
2498    #[test]
2499    fn test_plot_visibility() {
2500        let mut figure = Figure::new();
2501
2502        let mut line = LinePlot::new(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
2503        line.set_visible(false); // Hide this plot
2504        figure.add_line_plot(line);
2505
2506        let scatter = ScatterPlot::new(vec![0.0, 1.0], vec![1.0, 2.0]).unwrap();
2507        figure.add_scatter_plot(scatter);
2508
2509        // Only one plot should be visible
2510        let render_data = figure.render_data();
2511        assert_eq!(render_data.len(), 1);
2512
2513        let stats = figure.statistics();
2514        assert_eq!(stats.total_plots, 2);
2515        assert_eq!(stats.visible_plots, 1);
2516    }
2517
2518    #[test]
2519    fn test_bounds_computation() {
2520        let mut figure = Figure::new();
2521
2522        // Add plots with different ranges
2523        let line = LinePlot::new(vec![-1.0, 0.0, 1.0], vec![-2.0, 0.0, 2.0]).unwrap();
2524        figure.add_line_plot(line);
2525
2526        let scatter = ScatterPlot::new(vec![2.0, 3.0, 4.0], vec![1.0, 3.0, 5.0]).unwrap();
2527        figure.add_scatter_plot(scatter);
2528
2529        let bounds = figure.bounds();
2530
2531        // Bounds should encompass all plots
2532        assert!(bounds.min.x <= -1.0);
2533        assert!(bounds.max.x >= 4.0);
2534        assert!(bounds.min.y <= -2.0);
2535        assert!(bounds.max.y >= 5.0);
2536    }
2537
2538    #[test]
2539    fn test_reference_line_only_bounds_use_default_span() {
2540        let mut vertical_figure = Figure::new();
2541        vertical_figure.add_reference_line_on_axes(
2542            ReferenceLine::new(ReferenceLineOrientation::Vertical, 2.0).unwrap(),
2543            0,
2544        );
2545        let vertical_bounds = vertical_figure.bounds();
2546        assert_eq!(vertical_bounds.min.x, 1.5);
2547        assert_eq!(vertical_bounds.max.x, 2.5);
2548        assert_eq!(vertical_bounds.min.y, 0.0);
2549        assert_eq!(vertical_bounds.max.y, 1.0);
2550
2551        let mut horizontal_figure = Figure::new();
2552        horizontal_figure.add_reference_line_on_axes(
2553            ReferenceLine::new(ReferenceLineOrientation::Horizontal, 3.0).unwrap(),
2554            0,
2555        );
2556        let horizontal_bounds = horizontal_figure.bounds();
2557        assert_eq!(horizontal_bounds.min.x, 0.0);
2558        assert_eq!(horizontal_bounds.max.x, 1.0);
2559        assert_eq!(horizontal_bounds.min.y, 2.5);
2560        assert_eq!(horizontal_bounds.max.y, 3.5);
2561    }
2562
2563    #[test]
2564    fn test_reference_line_render_data_prefers_figure_limits() {
2565        let mut horizontal_figure = Figure::new().with_limits((-2.0, 8.0), (-10.0, 10.0));
2566        horizontal_figure.axes_metadata[0].x_limits = Some((0.0, 1.0));
2567        horizontal_figure.add_reference_line_on_axes(
2568            ReferenceLine::new(ReferenceLineOrientation::Horizontal, 3.0).unwrap(),
2569            0,
2570        );
2571        let horizontal_bounds = horizontal_figure.render_data()[0].bounds.unwrap();
2572        assert_eq!(horizontal_bounds.min.x, -2.0);
2573        assert_eq!(horizontal_bounds.max.x, 8.0);
2574        assert_eq!(horizontal_bounds.min.y, 3.0);
2575        assert_eq!(horizontal_bounds.max.y, 3.0);
2576
2577        let mut vertical_figure = Figure::new().with_limits((-2.0, 8.0), (-10.0, 10.0));
2578        vertical_figure.axes_metadata[0].y_limits = Some((0.0, 1.0));
2579        vertical_figure.add_reference_line_on_axes(
2580            ReferenceLine::new(ReferenceLineOrientation::Vertical, 4.0).unwrap(),
2581            0,
2582        );
2583        let vertical_bounds = vertical_figure.render_data()[0].bounds.unwrap();
2584        assert_eq!(vertical_bounds.min.x, 4.0);
2585        assert_eq!(vertical_bounds.max.x, 4.0);
2586        assert_eq!(vertical_bounds.min.y, -10.0);
2587        assert_eq!(vertical_bounds.max.y, 10.0);
2588    }
2589
2590    #[test]
2591    fn test_matlab_compat_multiple_lines() {
2592        use super::matlab_compat::*;
2593
2594        let mut figure = figure_with_title("Multiple Lines Test");
2595
2596        let data_sets = vec![
2597            (
2598                vec![0.0, 1.0, 2.0],
2599                vec![0.0, 1.0, 4.0],
2600                Some("Quadratic".to_string()),
2601            ),
2602            (
2603                vec![0.0, 1.0, 2.0],
2604                vec![0.0, 1.0, 2.0],
2605                Some("Linear".to_string()),
2606            ),
2607            (
2608                vec![0.0, 1.0, 2.0],
2609                vec![1.0, 1.0, 1.0],
2610                Some("Constant".to_string()),
2611            ),
2612        ];
2613
2614        let indices = plot_multiple_lines(&mut figure, data_sets).unwrap();
2615
2616        assert_eq!(indices.len(), 3);
2617        assert_eq!(figure.len(), 3);
2618
2619        // Each plot should have different colors
2620        let legend = figure.legend_entries();
2621        assert_eq!(legend.len(), 3);
2622        assert_ne!(legend[0].color, legend[1].color);
2623        assert_ne!(legend[1].color, legend[2].color);
2624    }
2625
2626    #[test]
2627    fn axes_metadata_and_labels_are_isolated_per_subplot() {
2628        let mut figure = Figure::new();
2629        figure.set_subplot_grid(1, 2);
2630        figure.set_axes_title(0, "Left Title");
2631        figure.set_axes_xlabel(0, "Left X");
2632        figure.set_axes_ylabel(0, "Left Y");
2633        figure.set_axes_title(1, "Right Title");
2634        figure.set_axes_style(
2635            1,
2636            TextStyle {
2637                font_size: Some(14.0),
2638                ..Default::default()
2639            },
2640        );
2641        figure.set_axes_legend_enabled(0, false);
2642        figure.set_axes_legend_style(
2643            1,
2644            LegendStyle {
2645                location: Some("southwest".into()),
2646                ..Default::default()
2647            },
2648        );
2649
2650        assert_eq!(
2651            figure.axes_metadata(0).and_then(|m| m.title.as_deref()),
2652            Some("Left Title")
2653        );
2654        assert_eq!(
2655            figure.axes_metadata(1).and_then(|m| m.title.as_deref()),
2656            Some("Right Title")
2657        );
2658        assert_eq!(
2659            figure.axes_metadata(0).and_then(|m| m.x_label.as_deref()),
2660            Some("Left X")
2661        );
2662        assert_eq!(
2663            figure.axes_metadata(0).and_then(|m| m.y_label.as_deref()),
2664            Some("Left Y")
2665        );
2666        assert!(!figure.axes_metadata(0).unwrap().legend_enabled);
2667        assert_eq!(
2668            figure
2669                .axes_metadata(1)
2670                .unwrap()
2671                .legend_style
2672                .location
2673                .as_deref(),
2674            Some("southwest")
2675        );
2676        assert_eq!(figure.axes_metadata(0).unwrap().axes_style.font_size, None);
2677        assert_eq!(
2678            figure.axes_metadata(1).unwrap().axes_style.font_size,
2679            Some(14.0)
2680        );
2681    }
2682
2683    #[test]
2684    fn set_labels_for_axes_only_updates_target_subplot() {
2685        let mut figure = Figure::new();
2686        figure.set_subplot_grid(1, 2);
2687        figure.add_line_plot_on_axes(
2688            LinePlot::new(vec![0.0, 1.0], vec![1.0, 2.0])
2689                .unwrap()
2690                .with_label("L0"),
2691            0,
2692        );
2693        figure.add_line_plot_on_axes(
2694            LinePlot::new(vec![0.0, 1.0], vec![2.0, 3.0])
2695                .unwrap()
2696                .with_label("R0"),
2697            1,
2698        );
2699        figure.set_labels_for_axes(1, &["Right Only".into()]);
2700
2701        let left_entries = figure.legend_entries_for_axes(0);
2702        let right_entries = figure.legend_entries_for_axes(1);
2703        assert_eq!(left_entries[0].label, "L0");
2704        assert_eq!(right_entries[0].label, "Right Only");
2705    }
2706
2707    #[test]
2708    fn axes_log_modes_are_isolated_per_subplot() {
2709        let mut figure = Figure::new();
2710        figure.set_subplot_grid(1, 2);
2711        figure.set_axes_log_modes(1, true, false);
2712
2713        assert!(!figure.axes_metadata(0).unwrap().x_log);
2714        assert!(!figure.axes_metadata(0).unwrap().y_log);
2715        assert!(figure.axes_metadata(1).unwrap().x_log);
2716        assert!(!figure.axes_metadata(1).unwrap().y_log);
2717
2718        figure.set_active_axes_index(1);
2719        assert!(figure.x_log);
2720        assert!(!figure.y_log);
2721    }
2722
2723    #[test]
2724    fn z_label_and_view_state_are_isolated_per_subplot() {
2725        let mut figure = Figure::new();
2726        figure.set_subplot_grid(1, 2);
2727        figure.set_axes_zlabel(1, "Height");
2728        figure.set_axes_view(1, 45.0, 20.0);
2729
2730        assert_eq!(figure.axes_metadata(0).unwrap().z_label, None);
2731        assert_eq!(
2732            figure.axes_metadata(1).unwrap().z_label.as_deref(),
2733            Some("Height")
2734        );
2735        assert_eq!(
2736            figure.axes_metadata(1).unwrap().view_azimuth_deg,
2737            Some(45.0)
2738        );
2739        assert_eq!(
2740            figure.axes_metadata(1).unwrap().view_elevation_deg,
2741            Some(20.0)
2742        );
2743    }
2744
2745    #[test]
2746    fn axes_view_revision_advances_for_each_explicit_view_update() {
2747        let mut figure = Figure::new();
2748
2749        assert_eq!(figure.axes_metadata(0).unwrap().view_revision, 0);
2750
2751        figure.set_axes_view(0, 45.0, 20.0);
2752        assert_eq!(figure.axes_metadata(0).unwrap().view_revision, 1);
2753
2754        figure.set_axes_view(0, 45.0, 20.0);
2755        assert_eq!(figure.axes_metadata(0).unwrap().view_revision, 2);
2756    }
2757
2758    #[test]
2759    fn pie_legend_entries_are_slice_based() {
2760        let mut figure = Figure::new();
2761        let pie = PieChart::new(vec![1.0, 2.0], None)
2762            .unwrap()
2763            .with_slice_labels(vec!["A".into(), "B".into()]);
2764        figure.add_pie_chart(pie);
2765        let entries = figure.legend_entries_for_axes(0);
2766        assert_eq!(entries.len(), 2);
2767        assert_eq!(entries[0].label, "A");
2768        assert_eq!(entries[1].label, "B");
2769    }
2770
2771    #[test]
2772    fn histogram_bars_do_not_use_categorical_axis_labels() {
2773        let mut figure = Figure::new();
2774        let mut bar = BarChart::new(vec!["a".into(), "b".into()], vec![2.0, 3.0]).unwrap();
2775        bar.set_histogram_bin_edges(vec![0.0, 0.5, 1.0]);
2776        figure.add_bar_chart(bar);
2777
2778        assert!(figure.categorical_axis_labels().is_none());
2779        assert_eq!(
2780            figure.histogram_axis_edges_for_axes(0),
2781            Some((true, vec![0.0, 0.5, 1.0]))
2782        );
2783    }
2784
2785    #[test]
2786    fn plain_bar_charts_keep_categorical_axis_labels() {
2787        let mut figure = Figure::new();
2788        let bar = BarChart::new(vec!["A".into(), "B".into()], vec![1.0, 2.0]).unwrap();
2789        figure.add_bar_chart(bar);
2790
2791        assert_eq!(
2792            figure.categorical_axis_labels(),
2793            Some((true, vec!["A".to_string(), "B".to_string()]))
2794        );
2795    }
2796
2797    #[test]
2798    fn line3_contributes_to_3d_bounds_and_metadata() {
2799        let mut figure = Figure::new();
2800        let line3 = Line3Plot::new(vec![0.0, 1.0], vec![1.0, 2.0], vec![2.0, 4.0])
2801            .unwrap()
2802            .with_label("Trajectory");
2803        figure.add_line3_plot(line3);
2804        let bounds = figure.bounds();
2805        assert_eq!(bounds.min.z, 2.0);
2806        assert_eq!(bounds.max.z, 4.0);
2807        let entries = figure.legend_entries_for_axes(0);
2808        assert_eq!(entries[0].plot_type, PlotType::Line3);
2809    }
2810
2811    #[test]
2812    fn hidden_line_removal_off_uses_no_depth_pipeline_for_mesh_edges() {
2813        let mut figure = Figure::new();
2814        let mesh = MeshPlot::new(
2815            vec![
2816                Vec3::new(0.0, 0.0, 0.0),
2817                Vec3::new(1.0, 0.0, 0.0),
2818                Vec3::new(0.0, 1.0, 1.0),
2819            ],
2820            vec![[0, 1, 2]],
2821        )
2822        .unwrap();
2823        figure.add_mesh_plot(mesh);
2824        figure.set_axes_hidden_line_removal(0, false);
2825
2826        let render_data = figure.render_data();
2827        assert_eq!(render_data.len(), 2);
2828        assert_eq!(
2829            render_data[0].pipeline_type,
2830            crate::core::PipelineType::Triangles
2831        );
2832        assert_eq!(
2833            render_data[1].pipeline_type,
2834            crate::core::PipelineType::LinesNoDepth
2835        );
2836    }
2837
2838    #[test]
2839    fn hidden_line_removal_off_uses_no_depth_pipeline_for_wireframe_surfaces() {
2840        let mut figure = Figure::new();
2841        let surface = SurfacePlot::new(
2842            vec![0.0, 1.0],
2843            vec![0.0, 1.0],
2844            vec![vec![0.0, 1.0], vec![1.0, 0.0]],
2845        )
2846        .unwrap()
2847        .with_wireframe(true);
2848        figure.add_surface_plot(surface);
2849        figure.set_axes_hidden_line_removal(0, false);
2850
2851        let render_data = figure.render_data();
2852        assert_eq!(render_data.len(), 1);
2853        assert_eq!(
2854            render_data[0].pipeline_type,
2855            crate::core::PipelineType::LinesNoDepth
2856        );
2857    }
2858
2859    #[test]
2860    fn stem_render_data_includes_marker_pass() {
2861        let mut figure = Figure::new();
2862        figure.add_stem_plot(StemPlot::new(vec![0.0, 1.0], vec![1.0, 2.0]).unwrap());
2863
2864        let render_data = figure.render_data();
2865        assert_eq!(render_data.len(), 2);
2866        assert_eq!(
2867            render_data[0].pipeline_type,
2868            crate::core::PipelineType::Lines
2869        );
2870        assert_eq!(
2871            render_data[1].pipeline_type,
2872            crate::core::PipelineType::Points
2873        );
2874    }
2875
2876    #[test]
2877    fn errorbar_render_data_includes_marker_pass() {
2878        let mut figure = Figure::new();
2879        figure.add_errorbar(
2880            ErrorBar::new_vertical(
2881                vec![0.0, 1.0],
2882                vec![1.0, 2.0],
2883                vec![0.1, 0.2],
2884                vec![0.1, 0.2],
2885            )
2886            .unwrap(),
2887        );
2888
2889        let render_data = figure.render_data();
2890        assert_eq!(render_data.len(), 2);
2891        assert_eq!(
2892            render_data[0].pipeline_type,
2893            crate::core::PipelineType::Lines
2894        );
2895        assert_eq!(
2896            render_data[1].pipeline_type,
2897            crate::core::PipelineType::Points
2898        );
2899    }
2900
2901    #[test]
2902    fn subplot_sensitive_axes_state_is_isolated_per_subplot() {
2903        let mut figure = Figure::new();
2904        figure.set_subplot_grid(1, 2);
2905        figure.set_axes_limits(1, Some((1.0, 2.0)), Some((3.0, 4.0)));
2906        figure.set_axes_z_limits(1, Some((5.0, 6.0)));
2907        figure.set_axes_grid_enabled(1, false);
2908        figure.set_axes_minor_grid_enabled(1, true);
2909        figure.set_axes_box_enabled(1, false);
2910        figure.set_axes_axis_equal(1, true);
2911        figure.set_axes_colorbar_enabled(1, true);
2912        figure.set_axes_colormap(1, ColorMap::Hot);
2913        figure.set_axes_color_limits(1, Some((0.0, 10.0)));
2914
2915        let left = figure.axes_metadata(0).unwrap();
2916        let right = figure.axes_metadata(1).unwrap();
2917        assert_eq!(left.x_limits, None);
2918        assert_eq!(right.x_limits, Some((1.0, 2.0)));
2919        assert!(!left.minor_grid_enabled);
2920        assert!(!left.minor_grid_explicit);
2921        assert!(!right.grid_enabled);
2922        assert!(right.minor_grid_enabled);
2923        assert!(right.minor_grid_explicit);
2924        assert!(!right.box_enabled);
2925        assert!(right.axis_equal);
2926        assert!(right.colorbar_enabled);
2927        assert_eq!(format!("{:?}", right.colormap), "Hot");
2928        assert_eq!(right.color_limits, Some((0.0, 10.0)));
2929    }
2930
2931    #[test]
2932    fn subplot_grid_expansion_reclaims_overlay_axes_slots() {
2933        let mut figure = Figure::new();
2934        let overlay_axes = figure.ensure_overlay_axes(0);
2935        assert_eq!(overlay_axes, 1);
2936        figure.add_line_plot_on_axes(
2937            LinePlot::new(vec![0.0, 1.0], vec![10.0, 20.0]).unwrap(),
2938            overlay_axes,
2939        );
2940        assert_eq!(figure.axes_overlay_parent(overlay_axes), Some(0));
2941        assert_eq!(figure.axes_count(), 2);
2942
2943        figure.set_subplot_grid(2, 1);
2944
2945        let second_subplot = figure.axes_metadata(1).unwrap();
2946        assert_eq!(second_subplot.overlay_parent, None);
2947        assert_eq!(second_subplot.y_axis_location, "left");
2948        assert!(figure.plot_axes_indices().is_empty());
2949        assert_eq!(figure.axes_count(), 2);
2950    }
2951
2952    #[test]
2953    fn active_axes_sync_does_not_clobber_figure_minor_grid_default() {
2954        let mut figure = Figure::new();
2955        figure.set_subplot_grid(1, 2);
2956        figure.minor_grid_enabled = true;
2957
2958        assert!(figure.minor_grid_enabled_for_axes(0));
2959        assert!(figure.minor_grid_enabled_for_axes(1));
2960
2961        figure.set_active_axes_index(1);
2962
2963        assert!(figure.minor_grid_enabled);
2964        assert!(figure.minor_grid_enabled_for_axes(0));
2965        assert!(figure.minor_grid_enabled_for_axes(1));
2966
2967        figure.set_axes_minor_grid_enabled(1, false);
2968
2969        assert!(figure.minor_grid_enabled);
2970        assert!(figure.minor_grid_enabled_for_axes(0));
2971        assert!(!figure.minor_grid_enabled_for_axes(1));
2972    }
2973}