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