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