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