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