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