Skip to main content

runmat_plot/
event.rs

1use crate::core::{BoundingBox, Vertex};
2use crate::plots::{
3    AreaPlot, AxesMetadata, BarChart, ColorMap, ContourFillPlot, ContourPlot, ErrorBar, Figure,
4    LegendEntry, LegendStyle, Line3Plot, LinePlot, MarkerStyle, PlotElement, PlotType, QuiverPlot,
5    Scatter3Plot, ScatterPlot, ShadingMode, StairsPlot, StemPlot, SurfacePlot, TextStyle,
6};
7use glam::{Vec3, Vec4};
8use serde::{Deserialize, Serialize};
9
10/// High-level event emitted whenever a figure changes.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct FigureEvent {
14    pub handle: u32,
15    pub kind: FigureEventKind,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub figure: Option<FigureSnapshot>,
18}
19
20/// Event kind for figure lifecycle + updates.
21#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
22#[serde(rename_all = "lowercase")]
23pub enum FigureEventKind {
24    Created,
25    Updated,
26    Cleared,
27    Closed,
28}
29
30/// Snapshot of the figure state describing layout + plots.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct FigureSnapshot {
34    pub layout: FigureLayout,
35    pub metadata: FigureMetadata,
36    pub plots: Vec<PlotDescriptor>,
37}
38
39/// Full replay scene payload capable of reconstructing a figure.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct FigureScene {
43    pub schema_version: u32,
44    pub layout: FigureLayout,
45    pub metadata: FigureMetadata,
46    pub plots: Vec<ScenePlot>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(tag = "kind", rename_all = "snake_case")]
51pub enum ScenePlot {
52    Line {
53        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
54        x: Vec<f64>,
55        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
56        y: Vec<f64>,
57        color_rgba: [f32; 4],
58        line_width: f32,
59        line_style: String,
60        axes_index: u32,
61        label: Option<String>,
62        visible: bool,
63    },
64    Scatter {
65        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
66        x: Vec<f64>,
67        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
68        y: Vec<f64>,
69        color_rgba: [f32; 4],
70        marker_size: f32,
71        marker_style: String,
72        axes_index: u32,
73        label: Option<String>,
74        visible: bool,
75    },
76    Bar {
77        labels: Vec<String>,
78        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
79        values: Vec<f64>,
80        #[serde(default, deserialize_with = "deserialize_option_vec_f64_lossy")]
81        histogram_bin_edges: Option<Vec<f64>>,
82        color_rgba: [f32; 4],
83        #[serde(default)]
84        outline_color_rgba: Option<[f32; 4]>,
85        bar_width: f32,
86        outline_width: f32,
87        orientation: String,
88        group_index: u32,
89        group_count: u32,
90        #[serde(default, deserialize_with = "deserialize_option_vec_f64_lossy")]
91        stack_offsets: Option<Vec<f64>>,
92        axes_index: u32,
93        label: Option<String>,
94        visible: bool,
95    },
96    ErrorBar {
97        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
98        x: Vec<f64>,
99        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
100        y: Vec<f64>,
101        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
102        err_low: Vec<f64>,
103        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
104        err_high: Vec<f64>,
105        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
106        x_err_low: Vec<f64>,
107        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
108        x_err_high: Vec<f64>,
109        orientation: String,
110        color_rgba: [f32; 4],
111        line_width: f32,
112        line_style: String,
113        cap_width: f32,
114        marker_style: Option<String>,
115        marker_size: Option<f32>,
116        marker_face_color: Option<[f32; 4]>,
117        marker_edge_color: Option<[f32; 4]>,
118        marker_filled: Option<bool>,
119        axes_index: u32,
120        label: Option<String>,
121        visible: bool,
122    },
123    Stairs {
124        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
125        x: Vec<f64>,
126        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
127        y: Vec<f64>,
128        color_rgba: [f32; 4],
129        line_width: f32,
130        axes_index: u32,
131        label: Option<String>,
132        visible: bool,
133    },
134    Stem {
135        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
136        x: Vec<f64>,
137        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
138        y: Vec<f64>,
139        #[serde(deserialize_with = "deserialize_f64_lossy")]
140        baseline: f64,
141        color_rgba: [f32; 4],
142        line_width: f32,
143        line_style: String,
144        baseline_color_rgba: [f32; 4],
145        baseline_visible: bool,
146        marker_color_rgba: [f32; 4],
147        marker_size: f32,
148        marker_filled: bool,
149        axes_index: u32,
150        label: Option<String>,
151        visible: bool,
152    },
153    Area {
154        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
155        x: Vec<f64>,
156        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
157        y: Vec<f64>,
158        #[serde(default, deserialize_with = "deserialize_option_vec_f64_lossy")]
159        lower_y: Option<Vec<f64>>,
160        #[serde(deserialize_with = "deserialize_f64_lossy")]
161        baseline: f64,
162        color_rgba: [f32; 4],
163        axes_index: u32,
164        label: Option<String>,
165        visible: bool,
166    },
167    Quiver {
168        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
169        x: Vec<f64>,
170        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
171        y: Vec<f64>,
172        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
173        u: Vec<f64>,
174        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
175        v: Vec<f64>,
176        color_rgba: [f32; 4],
177        line_width: f32,
178        scale: f32,
179        head_size: f32,
180        axes_index: u32,
181        label: Option<String>,
182        visible: bool,
183    },
184    Surface {
185        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
186        x: Vec<f64>,
187        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
188        y: Vec<f64>,
189        #[serde(deserialize_with = "deserialize_matrix_f64_lossy")]
190        z: Vec<Vec<f64>>,
191        colormap: String,
192        shading_mode: String,
193        wireframe: bool,
194        alpha: f32,
195        flatten_z: bool,
196        #[serde(default)]
197        image_mode: bool,
198        #[serde(default)]
199        color_grid_rgba: Option<Vec<Vec<[f32; 4]>>>,
200        #[serde(default, deserialize_with = "deserialize_option_pair_f64_lossy")]
201        color_limits: Option<[f64; 2]>,
202        axes_index: u32,
203        label: Option<String>,
204        visible: bool,
205    },
206    Line3 {
207        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
208        x: Vec<f64>,
209        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
210        y: Vec<f64>,
211        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
212        z: Vec<f64>,
213        color_rgba: [f32; 4],
214        line_width: f32,
215        line_style: String,
216        axes_index: u32,
217        label: Option<String>,
218        visible: bool,
219    },
220    Scatter3 {
221        #[serde(deserialize_with = "deserialize_vec_xyz_f32_lossy")]
222        points: Vec<[f32; 3]>,
223        #[serde(default, deserialize_with = "deserialize_vec_rgba_f32_lossy")]
224        colors_rgba: Vec<[f32; 4]>,
225        point_size: f32,
226        #[serde(default, deserialize_with = "deserialize_option_vec_f32_lossy")]
227        point_sizes: Option<Vec<f32>>,
228        axes_index: u32,
229        label: Option<String>,
230        visible: bool,
231    },
232    Contour {
233        vertices: Vec<SerializedVertex>,
234        bounds_min: [f32; 3],
235        bounds_max: [f32; 3],
236        base_z: f32,
237        line_width: f32,
238        axes_index: u32,
239        label: Option<String>,
240        visible: bool,
241    },
242    ContourFill {
243        vertices: Vec<SerializedVertex>,
244        bounds_min: [f32; 3],
245        bounds_max: [f32; 3],
246        axes_index: u32,
247        label: Option<String>,
248        visible: bool,
249    },
250    Pie {
251        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
252        values: Vec<f64>,
253        colors_rgba: Vec<[f32; 4]>,
254        slice_labels: Vec<String>,
255        label_format: Option<String>,
256        explode: Vec<bool>,
257        axes_index: u32,
258        label: Option<String>,
259        visible: bool,
260    },
261    Unsupported {
262        plot_kind: PlotKind,
263        axes_index: u32,
264        label: Option<String>,
265        visible: bool,
266    },
267}
268
269impl FigureSnapshot {
270    /// Capture a snapshot from a [`Figure`] reference.
271    pub fn capture(figure: &Figure) -> Self {
272        let (rows, cols) = figure.axes_grid();
273        let layout = FigureLayout {
274            axes_rows: rows as u32,
275            axes_cols: cols as u32,
276            axes_indices: figure
277                .plot_axes_indices()
278                .iter()
279                .map(|idx| *idx as u32)
280                .collect(),
281        };
282
283        let metadata = FigureMetadata::from_figure(figure);
284
285        let plots = figure
286            .plots()
287            .enumerate()
288            .map(|(idx, plot)| PlotDescriptor::from_plot(plot, figure_axis_index(figure, idx)))
289            .collect();
290
291        Self {
292            layout,
293            metadata,
294            plots,
295        }
296    }
297}
298
299impl FigureScene {
300    pub const SCHEMA_VERSION: u32 = 1;
301
302    pub fn capture(figure: &Figure) -> Self {
303        let snapshot = FigureSnapshot::capture(figure);
304        let plots = figure
305            .plots()
306            .enumerate()
307            .map(|(idx, plot)| ScenePlot::from_plot(plot, figure_axis_index(figure, idx)))
308            .collect();
309
310        Self {
311            schema_version: Self::SCHEMA_VERSION,
312            layout: snapshot.layout,
313            metadata: snapshot.metadata,
314            plots,
315        }
316    }
317
318    pub fn into_figure(self) -> Result<Figure, String> {
319        if self.schema_version != Self::SCHEMA_VERSION {
320            return Err(format!(
321                "unsupported figure scene schema version {}",
322                self.schema_version
323            ));
324        }
325
326        let mut figure = Figure::new();
327        figure.set_subplot_grid(
328            self.layout.axes_rows as usize,
329            self.layout.axes_cols as usize,
330        );
331        figure.active_axes_index = self.metadata.active_axes_index as usize;
332        if let Some(axes_metadata) = self.metadata.axes_metadata.clone() {
333            figure.axes_metadata = axes_metadata.into_iter().map(AxesMetadata::from).collect();
334            figure.set_active_axes_index(figure.active_axes_index);
335        } else {
336            figure.title = self.metadata.title;
337            figure.x_label = self.metadata.x_label;
338            figure.y_label = self.metadata.y_label;
339            figure.legend_enabled = self.metadata.legend_enabled;
340        }
341        figure.grid_enabled = self.metadata.grid_enabled;
342        figure.z_limits = self.metadata.z_limits.map(|[lo, hi]| (lo, hi));
343        figure.colorbar_enabled = self.metadata.colorbar_enabled;
344        figure.axis_equal = self.metadata.axis_equal;
345        figure.background_color = rgba_to_vec4(self.metadata.background_rgba);
346
347        for plot in self.plots {
348            plot.apply_to_figure(&mut figure)?;
349        }
350
351        Ok(figure)
352    }
353}
354
355fn figure_axis_index(figure: &Figure, plot_index: usize) -> u32 {
356    figure
357        .plot_axes_indices()
358        .get(plot_index)
359        .copied()
360        .unwrap_or(0) as u32
361}
362
363/// Layout metadata describing subplot arrangement.
364#[derive(Debug, Clone, Serialize, Deserialize)]
365#[serde(rename_all = "camelCase")]
366pub struct FigureLayout {
367    pub axes_rows: u32,
368    pub axes_cols: u32,
369    pub axes_indices: Vec<u32>,
370}
371
372/// Figure-level metadata (title, labels, theming).
373#[derive(Debug, Clone, Serialize, Deserialize)]
374#[serde(rename_all = "camelCase")]
375pub struct FigureMetadata {
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub title: Option<String>,
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub x_label: Option<String>,
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub y_label: Option<String>,
382    pub grid_enabled: bool,
383    pub legend_enabled: bool,
384    pub colorbar_enabled: bool,
385    pub axis_equal: bool,
386    pub background_rgba: [f32; 4],
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub colormap: Option<String>,
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub color_limits: Option<[f64; 2]>,
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub z_limits: Option<[f64; 2]>,
393    pub legend_entries: Vec<FigureLegendEntry>,
394    #[serde(default)]
395    pub active_axes_index: u32,
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub axes_metadata: Option<Vec<SerializedAxesMetadata>>,
398}
399
400impl FigureMetadata {
401    fn from_figure(figure: &Figure) -> Self {
402        let legend_entries = figure
403            .legend_entries()
404            .into_iter()
405            .map(FigureLegendEntry::from)
406            .collect();
407
408        Self {
409            title: figure.title.clone(),
410            x_label: figure.x_label.clone(),
411            y_label: figure.y_label.clone(),
412            grid_enabled: figure.grid_enabled,
413            legend_enabled: figure.legend_enabled,
414            colorbar_enabled: figure.colorbar_enabled,
415            axis_equal: figure.axis_equal,
416            background_rgba: vec4_to_rgba(figure.background_color),
417            colormap: Some(format!("{:?}", figure.colormap)),
418            color_limits: figure.color_limits.map(|(lo, hi)| [lo, hi]),
419            z_limits: figure.z_limits.map(|(lo, hi)| [lo, hi]),
420            legend_entries,
421            active_axes_index: figure.active_axes_index as u32,
422            axes_metadata: Some(
423                figure
424                    .axes_metadata
425                    .iter()
426                    .cloned()
427                    .map(SerializedAxesMetadata::from)
428                    .collect(),
429            ),
430        }
431    }
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
435#[serde(rename_all = "camelCase")]
436pub struct SerializedTextStyle {
437    #[serde(skip_serializing_if = "Option::is_none")]
438    pub color_rgba: Option<[f32; 4]>,
439    #[serde(skip_serializing_if = "Option::is_none")]
440    pub font_size: Option<f32>,
441    #[serde(skip_serializing_if = "Option::is_none")]
442    pub font_weight: Option<String>,
443    #[serde(skip_serializing_if = "Option::is_none")]
444    pub font_angle: Option<String>,
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub interpreter: Option<String>,
447    pub visible: bool,
448}
449
450impl From<TextStyle> for SerializedTextStyle {
451    fn from(value: TextStyle) -> Self {
452        Self {
453            color_rgba: value.color.map(vec4_to_rgba),
454            font_size: value.font_size,
455            font_weight: value.font_weight,
456            font_angle: value.font_angle,
457            interpreter: value.interpreter,
458            visible: value.visible,
459        }
460    }
461}
462
463impl From<SerializedTextStyle> for TextStyle {
464    fn from(value: SerializedTextStyle) -> Self {
465        Self {
466            color: value.color_rgba.map(rgba_to_vec4),
467            font_size: value.font_size,
468            font_weight: value.font_weight,
469            font_angle: value.font_angle,
470            interpreter: value.interpreter,
471            visible: value.visible,
472        }
473    }
474}
475
476#[derive(Debug, Clone, Serialize, Deserialize)]
477#[serde(rename_all = "camelCase")]
478pub struct SerializedLegendStyle {
479    #[serde(skip_serializing_if = "Option::is_none")]
480    pub location: Option<String>,
481    pub visible: bool,
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub font_size: Option<f32>,
484    #[serde(skip_serializing_if = "Option::is_none")]
485    pub font_weight: Option<String>,
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub font_angle: Option<String>,
488    #[serde(skip_serializing_if = "Option::is_none")]
489    pub interpreter: Option<String>,
490    #[serde(skip_serializing_if = "Option::is_none")]
491    pub box_visible: Option<bool>,
492    #[serde(skip_serializing_if = "Option::is_none")]
493    pub orientation: Option<String>,
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub text_color_rgba: Option<[f32; 4]>,
496}
497
498impl From<LegendStyle> for SerializedLegendStyle {
499    fn from(value: LegendStyle) -> Self {
500        Self {
501            location: value.location,
502            visible: value.visible,
503            font_size: value.font_size,
504            font_weight: value.font_weight,
505            font_angle: value.font_angle,
506            interpreter: value.interpreter,
507            box_visible: value.box_visible,
508            orientation: value.orientation,
509            text_color_rgba: value.text_color.map(vec4_to_rgba),
510        }
511    }
512}
513
514impl From<SerializedLegendStyle> for LegendStyle {
515    fn from(value: SerializedLegendStyle) -> Self {
516        Self {
517            location: value.location,
518            visible: value.visible,
519            font_size: value.font_size,
520            font_weight: value.font_weight,
521            font_angle: value.font_angle,
522            interpreter: value.interpreter,
523            box_visible: value.box_visible,
524            orientation: value.orientation,
525            text_color: value.text_color_rgba.map(rgba_to_vec4),
526        }
527    }
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize)]
531#[serde(rename_all = "camelCase")]
532pub struct SerializedAxesMetadata {
533    #[serde(skip_serializing_if = "Option::is_none")]
534    pub title: Option<String>,
535    #[serde(skip_serializing_if = "Option::is_none")]
536    pub x_label: Option<String>,
537    #[serde(skip_serializing_if = "Option::is_none")]
538    pub y_label: Option<String>,
539    #[serde(skip_serializing_if = "Option::is_none")]
540    pub z_label: Option<String>,
541    #[serde(skip_serializing_if = "Option::is_none")]
542    pub x_limits: Option<[f64; 2]>,
543    #[serde(skip_serializing_if = "Option::is_none")]
544    pub y_limits: Option<[f64; 2]>,
545    #[serde(skip_serializing_if = "Option::is_none")]
546    pub z_limits: Option<[f64; 2]>,
547    #[serde(default)]
548    pub x_log: bool,
549    #[serde(default)]
550    pub y_log: bool,
551    #[serde(skip_serializing_if = "Option::is_none")]
552    pub view_azimuth_deg: Option<f32>,
553    #[serde(skip_serializing_if = "Option::is_none")]
554    pub view_elevation_deg: Option<f32>,
555    #[serde(default)]
556    pub grid_enabled: bool,
557    #[serde(default)]
558    pub box_enabled: bool,
559    #[serde(default)]
560    pub axis_equal: bool,
561    pub legend_enabled: bool,
562    #[serde(default)]
563    pub colorbar_enabled: bool,
564    pub colormap: String,
565    #[serde(skip_serializing_if = "Option::is_none")]
566    pub color_limits: Option<[f64; 2]>,
567    pub title_style: SerializedTextStyle,
568    pub x_label_style: SerializedTextStyle,
569    pub y_label_style: SerializedTextStyle,
570    pub z_label_style: SerializedTextStyle,
571    pub legend_style: SerializedLegendStyle,
572    #[serde(default, skip_serializing_if = "Vec::is_empty")]
573    pub world_text_annotations: Vec<SerializedTextAnnotation>,
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize)]
577#[serde(rename_all = "camelCase")]
578pub struct SerializedTextAnnotation {
579    pub position: [f32; 3],
580    pub text: String,
581    pub style: SerializedTextStyle,
582}
583
584impl From<AxesMetadata> for SerializedAxesMetadata {
585    fn from(value: AxesMetadata) -> Self {
586        Self {
587            title: value.title,
588            x_label: value.x_label,
589            y_label: value.y_label,
590            z_label: value.z_label,
591            x_limits: value.x_limits.map(|(a, b)| [a, b]),
592            y_limits: value.y_limits.map(|(a, b)| [a, b]),
593            z_limits: value.z_limits.map(|(a, b)| [a, b]),
594            x_log: value.x_log,
595            y_log: value.y_log,
596            view_azimuth_deg: value.view_azimuth_deg,
597            view_elevation_deg: value.view_elevation_deg,
598            grid_enabled: value.grid_enabled,
599            box_enabled: value.box_enabled,
600            axis_equal: value.axis_equal,
601            legend_enabled: value.legend_enabled,
602            colorbar_enabled: value.colorbar_enabled,
603            colormap: format!("{:?}", value.colormap),
604            color_limits: value.color_limits.map(|(a, b)| [a, b]),
605            title_style: value.title_style.into(),
606            x_label_style: value.x_label_style.into(),
607            y_label_style: value.y_label_style.into(),
608            z_label_style: value.z_label_style.into(),
609            legend_style: value.legend_style.into(),
610            world_text_annotations: value
611                .world_text_annotations
612                .into_iter()
613                .map(Into::into)
614                .collect(),
615        }
616    }
617}
618
619impl From<SerializedAxesMetadata> for AxesMetadata {
620    fn from(value: SerializedAxesMetadata) -> Self {
621        Self {
622            title: value.title,
623            x_label: value.x_label,
624            y_label: value.y_label,
625            z_label: value.z_label,
626            x_limits: value.x_limits.map(|[a, b]| (a, b)),
627            y_limits: value.y_limits.map(|[a, b]| (a, b)),
628            z_limits: value.z_limits.map(|[a, b]| (a, b)),
629            x_log: value.x_log,
630            y_log: value.y_log,
631            view_azimuth_deg: value.view_azimuth_deg,
632            view_elevation_deg: value.view_elevation_deg,
633            grid_enabled: value.grid_enabled,
634            box_enabled: value.box_enabled,
635            axis_equal: value.axis_equal,
636            legend_enabled: value.legend_enabled,
637            colorbar_enabled: value.colorbar_enabled,
638            colormap: parse_colormap_name(&value.colormap),
639            color_limits: value.color_limits.map(|[a, b]| (a, b)),
640            title_style: value.title_style.into(),
641            x_label_style: value.x_label_style.into(),
642            y_label_style: value.y_label_style.into(),
643            z_label_style: value.z_label_style.into(),
644            legend_style: value.legend_style.into(),
645            world_text_annotations: value
646                .world_text_annotations
647                .into_iter()
648                .map(Into::into)
649                .collect(),
650        }
651    }
652}
653
654impl From<crate::plots::figure::TextAnnotation> for SerializedTextAnnotation {
655    fn from(value: crate::plots::figure::TextAnnotation) -> Self {
656        Self {
657            position: value.position.to_array(),
658            text: value.text,
659            style: value.style.into(),
660        }
661    }
662}
663
664impl From<SerializedTextAnnotation> for crate::plots::figure::TextAnnotation {
665    fn from(value: SerializedTextAnnotation) -> Self {
666        Self {
667            position: glam::Vec3::from_array(value.position),
668            text: value.text,
669            style: value.style.into(),
670        }
671    }
672}
673
674/// Descriptor for a single plot element within the figure.
675#[derive(Debug, Clone, Serialize, Deserialize)]
676#[serde(rename_all = "camelCase")]
677pub struct PlotDescriptor {
678    pub kind: PlotKind,
679    #[serde(skip_serializing_if = "Option::is_none")]
680    pub label: Option<String>,
681    pub axes_index: u32,
682    pub color_rgba: [f32; 4],
683    pub visible: bool,
684}
685
686impl PlotDescriptor {
687    fn from_plot(plot: &PlotElement, axes_index: u32) -> Self {
688        Self {
689            kind: PlotKind::from(plot.plot_type()),
690            label: plot.label(),
691            axes_index,
692            color_rgba: vec4_to_rgba(plot.color()),
693            visible: plot.is_visible(),
694        }
695    }
696}
697
698impl ScenePlot {
699    fn from_plot(plot: &PlotElement, axes_index: u32) -> Self {
700        match plot {
701            PlotElement::Line(line) => Self::Line {
702                x: line.x_data.clone(),
703                y: line.y_data.clone(),
704                color_rgba: vec4_to_rgba(line.color),
705                line_width: line.line_width,
706                line_style: format!("{:?}", line.line_style),
707                axes_index,
708                label: line.label.clone(),
709                visible: line.visible,
710            },
711            PlotElement::Scatter(scatter) => Self::Scatter {
712                x: scatter.x_data.clone(),
713                y: scatter.y_data.clone(),
714                color_rgba: vec4_to_rgba(scatter.color),
715                marker_size: scatter.marker_size,
716                marker_style: format!("{:?}", scatter.marker_style),
717                axes_index,
718                label: scatter.label.clone(),
719                visible: scatter.visible,
720            },
721            PlotElement::Bar(bar) => Self::Bar {
722                labels: bar.labels.clone(),
723                values: bar.values().unwrap_or(&[]).to_vec(),
724                histogram_bin_edges: bar.histogram_bin_edges().map(|edges| edges.to_vec()),
725                color_rgba: vec4_to_rgba(bar.color),
726                outline_color_rgba: bar.outline_color.map(vec4_to_rgba),
727                bar_width: bar.bar_width,
728                outline_width: bar.outline_width,
729                orientation: format!("{:?}", bar.orientation),
730                group_index: bar.group_index as u32,
731                group_count: bar.group_count as u32,
732                stack_offsets: bar.stack_offsets().map(|offsets| offsets.to_vec()),
733                axes_index,
734                label: bar.label.clone(),
735                visible: bar.visible,
736            },
737            PlotElement::ErrorBar(error) => Self::ErrorBar {
738                x: error.x.clone(),
739                y: error.y.clone(),
740                err_low: error.y_neg.clone(),
741                err_high: error.y_pos.clone(),
742                x_err_low: error.x_neg.clone(),
743                x_err_high: error.x_pos.clone(),
744                orientation: format!("{:?}", error.orientation),
745                color_rgba: vec4_to_rgba(error.color),
746                line_width: error.line_width,
747                line_style: format!("{:?}", error.line_style),
748                cap_width: error.cap_size,
749                marker_style: error.marker.as_ref().map(|m| format!("{:?}", m.kind)),
750                marker_size: error.marker.as_ref().map(|m| m.size),
751                marker_face_color: error.marker.as_ref().map(|m| vec4_to_rgba(m.face_color)),
752                marker_edge_color: error.marker.as_ref().map(|m| vec4_to_rgba(m.edge_color)),
753                marker_filled: error.marker.as_ref().map(|m| m.filled),
754                axes_index,
755                label: error.label.clone(),
756                visible: error.visible,
757            },
758            PlotElement::Stairs(stairs) => Self::Stairs {
759                x: stairs.x.clone(),
760                y: stairs.y.clone(),
761                color_rgba: vec4_to_rgba(stairs.color),
762                line_width: stairs.line_width,
763                axes_index,
764                label: stairs.label.clone(),
765                visible: stairs.visible,
766            },
767            PlotElement::Stem(stem) => Self::Stem {
768                x: stem.x.clone(),
769                y: stem.y.clone(),
770                baseline: stem.baseline,
771                color_rgba: vec4_to_rgba(stem.color),
772                line_width: stem.line_width,
773                line_style: format!("{:?}", stem.line_style),
774                baseline_color_rgba: vec4_to_rgba(stem.baseline_color),
775                baseline_visible: stem.baseline_visible,
776                marker_color_rgba: vec4_to_rgba(
777                    stem.marker
778                        .as_ref()
779                        .map(|m| m.face_color)
780                        .unwrap_or(stem.color),
781                ),
782                marker_size: stem.marker.as_ref().map(|m| m.size).unwrap_or(0.0),
783                marker_filled: stem.marker.as_ref().map(|m| m.filled).unwrap_or(false),
784                axes_index,
785                label: stem.label.clone(),
786                visible: stem.visible,
787            },
788            PlotElement::Area(area) => Self::Area {
789                x: area.x.clone(),
790                y: area.y.clone(),
791                lower_y: area.lower_y.clone(),
792                baseline: area.baseline,
793                color_rgba: vec4_to_rgba(area.color),
794                axes_index,
795                label: area.label.clone(),
796                visible: area.visible,
797            },
798            PlotElement::Quiver(quiver) => Self::Quiver {
799                x: quiver.x.clone(),
800                y: quiver.y.clone(),
801                u: quiver.u.clone(),
802                v: quiver.v.clone(),
803                color_rgba: vec4_to_rgba(quiver.color),
804                line_width: quiver.line_width,
805                scale: quiver.scale,
806                head_size: quiver.head_size,
807                axes_index,
808                label: quiver.label.clone(),
809                visible: quiver.visible,
810            },
811            PlotElement::Surface(surface) => Self::Surface {
812                x: surface.x_data.clone(),
813                y: surface.y_data.clone(),
814                z: surface.z_data.clone().unwrap_or_default(),
815                colormap: format!("{:?}", surface.colormap),
816                shading_mode: format!("{:?}", surface.shading_mode),
817                wireframe: surface.wireframe,
818                alpha: surface.alpha,
819                flatten_z: surface.flatten_z,
820                image_mode: surface.image_mode,
821                color_grid_rgba: surface.color_grid.as_ref().map(|grid| {
822                    grid.iter()
823                        .map(|row| row.iter().map(|color| vec4_to_rgba(*color)).collect())
824                        .collect()
825                }),
826                color_limits: surface.color_limits.map(|(lo, hi)| [lo, hi]),
827                axes_index,
828                label: surface.label.clone(),
829                visible: surface.visible,
830            },
831            PlotElement::Line3(line) => Self::Line3 {
832                x: line.x_data.clone(),
833                y: line.y_data.clone(),
834                z: line.z_data.clone(),
835                color_rgba: vec4_to_rgba(line.color),
836                line_width: line.line_width,
837                line_style: format!("{:?}", line.line_style),
838                axes_index,
839                label: line.label.clone(),
840                visible: line.visible,
841            },
842            PlotElement::Scatter3(scatter3) => Self::Scatter3 {
843                points: scatter3
844                    .points
845                    .iter()
846                    .map(|point| vec3_to_xyz(*point))
847                    .collect(),
848                colors_rgba: scatter3
849                    .colors
850                    .iter()
851                    .map(|color| vec4_to_rgba(*color))
852                    .collect(),
853                point_size: scatter3.point_size,
854                point_sizes: scatter3.point_sizes.clone(),
855                axes_index,
856                label: scatter3.label.clone(),
857                visible: scatter3.visible,
858            },
859            PlotElement::Contour(contour) => Self::Contour {
860                vertices: contour
861                    .cpu_vertices()
862                    .unwrap_or(&[])
863                    .iter()
864                    .cloned()
865                    .map(Into::into)
866                    .collect(),
867                bounds_min: vec3_to_xyz(contour.bounds().min),
868                bounds_max: vec3_to_xyz(contour.bounds().max),
869                base_z: contour.base_z,
870                line_width: contour.line_width,
871                axes_index,
872                label: contour.label.clone(),
873                visible: contour.visible,
874            },
875            PlotElement::ContourFill(fill) => Self::ContourFill {
876                vertices: fill
877                    .cpu_vertices()
878                    .unwrap_or(&[])
879                    .iter()
880                    .cloned()
881                    .map(Into::into)
882                    .collect(),
883                bounds_min: vec3_to_xyz(fill.bounds().min),
884                bounds_max: vec3_to_xyz(fill.bounds().max),
885                axes_index,
886                label: fill.label.clone(),
887                visible: fill.visible,
888            },
889            PlotElement::Pie(pie) => Self::Pie {
890                values: pie.values.clone(),
891                colors_rgba: pie.colors.iter().map(|c| vec4_to_rgba(*c)).collect(),
892                slice_labels: pie.slice_labels.clone(),
893                label_format: pie.label_format.clone(),
894                explode: pie.explode.clone(),
895                axes_index,
896                label: pie.label.clone(),
897                visible: pie.visible,
898            },
899        }
900    }
901
902    fn apply_to_figure(self, figure: &mut Figure) -> Result<(), String> {
903        match self {
904            ScenePlot::Line {
905                x,
906                y,
907                color_rgba,
908                line_width,
909                line_style,
910                axes_index,
911                label,
912                visible,
913            } => {
914                let mut line = LinePlot::new(x, y)?;
915                line.set_color(rgba_to_vec4(color_rgba));
916                line.set_line_width(line_width);
917                line.set_line_style(parse_line_style(&line_style));
918                line.label = label;
919                line.set_visible(visible);
920                figure.add_line_plot_on_axes(line, axes_index as usize);
921            }
922            ScenePlot::Scatter {
923                x,
924                y,
925                color_rgba,
926                marker_size,
927                marker_style,
928                axes_index,
929                label,
930                visible,
931            } => {
932                let mut scatter = ScatterPlot::new(x, y)?;
933                scatter.set_color(rgba_to_vec4(color_rgba));
934                scatter.set_marker_size(marker_size);
935                scatter.set_marker_style(parse_marker_style(&marker_style));
936                scatter.label = label;
937                scatter.set_visible(visible);
938                figure.add_scatter_plot_on_axes(scatter, axes_index as usize);
939            }
940            ScenePlot::Bar {
941                labels,
942                values,
943                histogram_bin_edges,
944                color_rgba,
945                outline_color_rgba,
946                bar_width,
947                outline_width,
948                orientation,
949                group_index,
950                group_count,
951                stack_offsets,
952                axes_index,
953                label,
954                visible,
955            } => {
956                let mut bar = BarChart::new(labels, values)?
957                    .with_style(rgba_to_vec4(color_rgba), bar_width)
958                    .with_orientation(parse_bar_orientation(&orientation))
959                    .with_group(group_index as usize, group_count as usize);
960                if let Some(edges) = histogram_bin_edges {
961                    bar.set_histogram_bin_edges(edges);
962                }
963                if let Some(offsets) = stack_offsets {
964                    bar = bar.with_stack_offsets(offsets);
965                }
966                if let Some(outline) = outline_color_rgba {
967                    bar = bar.with_outline(rgba_to_vec4(outline), outline_width);
968                }
969                bar.label = label;
970                bar.set_visible(visible);
971                figure.add_bar_chart_on_axes(bar, axes_index as usize);
972            }
973            ScenePlot::ErrorBar {
974                x,
975                y,
976                err_low,
977                err_high,
978                x_err_low,
979                x_err_high,
980                orientation,
981                color_rgba,
982                line_width,
983                line_style,
984                cap_width,
985                marker_style,
986                marker_size,
987                marker_face_color,
988                marker_edge_color,
989                marker_filled,
990                axes_index,
991                label,
992                visible,
993            } => {
994                let mut error = if orientation.eq_ignore_ascii_case("Both") {
995                    ErrorBar::new_both(x, y, x_err_low, x_err_high, err_low, err_high)?
996                } else {
997                    ErrorBar::new_vertical(x, y, err_low, err_high)?
998                }
999                .with_style(
1000                    rgba_to_vec4(color_rgba),
1001                    line_width,
1002                    parse_line_style_name(&line_style),
1003                    cap_width,
1004                );
1005                if let Some(size) = marker_size {
1006                    error.set_marker(Some(crate::plots::line::LineMarkerAppearance {
1007                        kind: parse_marker_style(marker_style.as_deref().unwrap_or("Circle")),
1008                        size,
1009                        edge_color: marker_edge_color
1010                            .map(rgba_to_vec4)
1011                            .unwrap_or(rgba_to_vec4(color_rgba)),
1012                        face_color: marker_face_color
1013                            .map(rgba_to_vec4)
1014                            .unwrap_or(rgba_to_vec4(color_rgba)),
1015                        filled: marker_filled.unwrap_or(false),
1016                    }));
1017                }
1018                error.label = label;
1019                error.set_visible(visible);
1020                figure.add_errorbar_on_axes(error, axes_index as usize);
1021            }
1022            ScenePlot::Stairs {
1023                x,
1024                y,
1025                color_rgba,
1026                line_width,
1027                axes_index,
1028                label,
1029                visible,
1030            } => {
1031                let mut stairs = StairsPlot::new(x, y)?;
1032                stairs.color = rgba_to_vec4(color_rgba);
1033                stairs.line_width = line_width;
1034                stairs.label = label;
1035                stairs.set_visible(visible);
1036                figure.add_stairs_plot_on_axes(stairs, axes_index as usize);
1037            }
1038            ScenePlot::Stem {
1039                x,
1040                y,
1041                baseline,
1042                color_rgba,
1043                line_width,
1044                line_style,
1045                baseline_color_rgba,
1046                baseline_visible,
1047                marker_color_rgba,
1048                marker_size,
1049                marker_filled,
1050                axes_index,
1051                label,
1052                visible,
1053            } => {
1054                let mut stem = StemPlot::new(x, y)?;
1055                stem = stem
1056                    .with_style(
1057                        rgba_to_vec4(color_rgba),
1058                        line_width,
1059                        parse_line_style_name(&line_style),
1060                        baseline,
1061                    )
1062                    .with_baseline_style(rgba_to_vec4(baseline_color_rgba), baseline_visible);
1063                if marker_size > 0.0 {
1064                    stem.set_marker(Some(crate::plots::line::LineMarkerAppearance {
1065                        kind: crate::plots::scatter::MarkerStyle::Circle,
1066                        size: marker_size,
1067                        edge_color: rgba_to_vec4(marker_color_rgba),
1068                        face_color: rgba_to_vec4(marker_color_rgba),
1069                        filled: marker_filled,
1070                    }));
1071                }
1072                stem.label = label;
1073                stem.set_visible(visible);
1074                figure.add_stem_plot_on_axes(stem, axes_index as usize);
1075            }
1076            ScenePlot::Area {
1077                x,
1078                y,
1079                lower_y,
1080                baseline,
1081                color_rgba,
1082                axes_index,
1083                label,
1084                visible,
1085            } => {
1086                let mut area = AreaPlot::new(x, y)?;
1087                if let Some(lower_y) = lower_y {
1088                    area = area.with_lower_curve(lower_y);
1089                }
1090                area.baseline = baseline;
1091                area.color = rgba_to_vec4(color_rgba);
1092                area.label = label;
1093                area.set_visible(visible);
1094                figure.add_area_plot_on_axes(area, axes_index as usize);
1095            }
1096            ScenePlot::Quiver {
1097                x,
1098                y,
1099                u,
1100                v,
1101                color_rgba,
1102                line_width,
1103                scale,
1104                head_size,
1105                axes_index,
1106                label,
1107                visible,
1108            } => {
1109                let mut quiver = QuiverPlot::new(x, y, u, v)?
1110                    .with_style(rgba_to_vec4(color_rgba), line_width, scale, head_size)
1111                    .with_label(label.unwrap_or_else(|| "Data".to_string()));
1112                quiver.set_visible(visible);
1113                figure.add_quiver_plot_on_axes(quiver, axes_index as usize);
1114            }
1115            ScenePlot::Surface {
1116                x,
1117                y,
1118                z,
1119                colormap,
1120                shading_mode,
1121                wireframe,
1122                alpha,
1123                flatten_z,
1124                image_mode,
1125                color_grid_rgba,
1126                color_limits,
1127                axes_index,
1128                label,
1129                visible,
1130            } => {
1131                let mut surface = SurfacePlot::new(x, y, z)?;
1132                surface.colormap = parse_colormap(&colormap);
1133                surface.shading_mode = parse_shading_mode(&shading_mode);
1134                surface.wireframe = wireframe;
1135                surface.alpha = alpha.clamp(0.0, 1.0);
1136                surface.flatten_z = flatten_z;
1137                surface.image_mode = image_mode;
1138                surface.color_grid = color_grid_rgba.map(|grid| {
1139                    grid.into_iter()
1140                        .map(|row| row.into_iter().map(rgba_to_vec4).collect())
1141                        .collect()
1142                });
1143                surface.color_limits = color_limits.map(|[lo, hi]| (lo, hi));
1144                surface.label = label;
1145                surface.visible = visible;
1146                figure.add_surface_plot_on_axes(surface, axes_index as usize);
1147            }
1148            ScenePlot::Line3 {
1149                x,
1150                y,
1151                z,
1152                color_rgba,
1153                line_width,
1154                line_style,
1155                axes_index,
1156                label,
1157                visible,
1158            } => {
1159                let mut plot = Line3Plot::new(x, y, z)?
1160                    .with_style(
1161                        rgba_to_vec4(color_rgba),
1162                        line_width,
1163                        parse_line_style_name(&line_style),
1164                    )
1165                    .with_label(label.unwrap_or_else(|| "Data".to_string()));
1166                plot.set_visible(visible);
1167                figure.add_line3_plot_on_axes(plot, axes_index as usize);
1168            }
1169            ScenePlot::Scatter3 {
1170                points,
1171                colors_rgba,
1172                point_size,
1173                point_sizes,
1174                axes_index,
1175                label,
1176                visible,
1177            } => {
1178                let points: Vec<Vec3> = points.into_iter().map(xyz_to_vec3).collect();
1179                let colors: Vec<Vec4> = colors_rgba.into_iter().map(rgba_to_vec4).collect();
1180                let mut scatter3 = Scatter3Plot::new(points)?;
1181                if !colors.is_empty() {
1182                    scatter3 = scatter3.with_colors(colors)?;
1183                }
1184                scatter3.point_size = point_size.max(1.0);
1185                scatter3.point_sizes = point_sizes;
1186                scatter3.label = label;
1187                scatter3.visible = visible;
1188                figure.add_scatter3_plot_on_axes(scatter3, axes_index as usize);
1189            }
1190            ScenePlot::Contour {
1191                vertices,
1192                bounds_min,
1193                bounds_max,
1194                base_z,
1195                line_width,
1196                axes_index,
1197                label,
1198                visible,
1199            } => {
1200                let mut contour = ContourPlot::from_vertices(
1201                    vertices.into_iter().map(Into::into).collect(),
1202                    base_z,
1203                    serialized_bounds(bounds_min, bounds_max),
1204                )
1205                .with_line_width(line_width);
1206                contour.label = label;
1207                contour.set_visible(visible);
1208                figure.add_contour_plot_on_axes(contour, axes_index as usize);
1209            }
1210            ScenePlot::ContourFill {
1211                vertices,
1212                bounds_min,
1213                bounds_max,
1214                axes_index,
1215                label,
1216                visible,
1217            } => {
1218                let mut fill = ContourFillPlot::from_vertices(
1219                    vertices.into_iter().map(Into::into).collect(),
1220                    serialized_bounds(bounds_min, bounds_max),
1221                );
1222                fill.label = label;
1223                fill.set_visible(visible);
1224                figure.add_contour_fill_plot_on_axes(fill, axes_index as usize);
1225            }
1226            ScenePlot::Pie {
1227                values,
1228                colors_rgba,
1229                slice_labels,
1230                label_format,
1231                explode,
1232                axes_index,
1233                label,
1234                visible,
1235            } => {
1236                let mut pie = crate::plots::PieChart::new(
1237                    values,
1238                    Some(colors_rgba.into_iter().map(rgba_to_vec4).collect()),
1239                )?
1240                .with_slice_labels(slice_labels)
1241                .with_explode(explode);
1242                if let Some(fmt) = label_format {
1243                    pie = pie.with_label_format(fmt);
1244                }
1245                pie.label = label;
1246                pie.set_visible(visible);
1247                figure.add_pie_chart_on_axes(pie, axes_index as usize);
1248            }
1249            ScenePlot::Unsupported { .. } => {}
1250        }
1251        Ok(())
1252    }
1253}
1254
1255fn parse_line_style(value: &str) -> crate::plots::LineStyle {
1256    match value {
1257        "Dashed" => crate::plots::LineStyle::Dashed,
1258        "Dotted" => crate::plots::LineStyle::Dotted,
1259        "DashDot" => crate::plots::LineStyle::DashDot,
1260        _ => crate::plots::LineStyle::Solid,
1261    }
1262}
1263
1264fn parse_bar_orientation(value: &str) -> crate::plots::bar::Orientation {
1265    match value {
1266        "Horizontal" => crate::plots::bar::Orientation::Horizontal,
1267        _ => crate::plots::bar::Orientation::Vertical,
1268    }
1269}
1270
1271fn parse_marker_style(value: &str) -> MarkerStyle {
1272    match value {
1273        "Square" => MarkerStyle::Square,
1274        "Triangle" => MarkerStyle::Triangle,
1275        "Diamond" => MarkerStyle::Diamond,
1276        "Plus" => MarkerStyle::Plus,
1277        "Cross" => MarkerStyle::Cross,
1278        "Star" => MarkerStyle::Star,
1279        "Hexagon" => MarkerStyle::Hexagon,
1280        _ => MarkerStyle::Circle,
1281    }
1282}
1283
1284fn parse_colormap(value: &str) -> ColorMap {
1285    match value {
1286        "Jet" => ColorMap::Jet,
1287        "Hot" => ColorMap::Hot,
1288        "Cool" => ColorMap::Cool,
1289        "Spring" => ColorMap::Spring,
1290        "Summer" => ColorMap::Summer,
1291        "Autumn" => ColorMap::Autumn,
1292        "Winter" => ColorMap::Winter,
1293        "Gray" => ColorMap::Gray,
1294        "Bone" => ColorMap::Bone,
1295        "Copper" => ColorMap::Copper,
1296        "Pink" => ColorMap::Pink,
1297        "Lines" => ColorMap::Lines,
1298        "Viridis" => ColorMap::Viridis,
1299        "Plasma" => ColorMap::Plasma,
1300        "Inferno" => ColorMap::Inferno,
1301        "Magma" => ColorMap::Magma,
1302        "Turbo" => ColorMap::Turbo,
1303        "Parula" => ColorMap::Parula,
1304        _ => ColorMap::Parula,
1305    }
1306}
1307
1308fn parse_shading_mode(value: &str) -> ShadingMode {
1309    match value {
1310        "Flat" => ShadingMode::Flat,
1311        "Smooth" => ShadingMode::Smooth,
1312        "Faceted" => ShadingMode::Faceted,
1313        "None" => ShadingMode::None,
1314        _ => ShadingMode::Smooth,
1315    }
1316}
1317
1318fn xyz_to_vec3(value: [f32; 3]) -> Vec3 {
1319    Vec3::new(value[0], value[1], value[2])
1320}
1321
1322fn serialized_bounds(min: [f32; 3], max: [f32; 3]) -> BoundingBox {
1323    BoundingBox::new(xyz_to_vec3(min), xyz_to_vec3(max))
1324}
1325
1326fn vec3_to_xyz(value: Vec3) -> [f32; 3] {
1327    [value.x, value.y, value.z]
1328}
1329
1330fn rgba_to_vec4(value: [f32; 4]) -> Vec4 {
1331    Vec4::new(value[0], value[1], value[2], value[3])
1332}
1333
1334#[derive(Debug, Clone, Serialize, Deserialize)]
1335#[serde(rename_all = "camelCase")]
1336pub struct SerializedVertex {
1337    position: [f32; 3],
1338    color_rgba: [f32; 4],
1339    normal: [f32; 3],
1340    tex_coords: [f32; 2],
1341}
1342
1343impl From<Vertex> for SerializedVertex {
1344    fn from(value: Vertex) -> Self {
1345        Self {
1346            position: value.position,
1347            color_rgba: value.color,
1348            normal: value.normal,
1349            tex_coords: value.tex_coords,
1350        }
1351    }
1352}
1353
1354impl From<SerializedVertex> for Vertex {
1355    fn from(value: SerializedVertex) -> Self {
1356        Self {
1357            position: value.position,
1358            color: value.color_rgba,
1359            normal: value.normal,
1360            tex_coords: value.tex_coords,
1361        }
1362    }
1363}
1364
1365/// Serialized legend entry for frontend rendering.
1366#[derive(Debug, Clone, Serialize, Deserialize)]
1367#[serde(rename_all = "camelCase")]
1368pub struct FigureLegendEntry {
1369    pub label: String,
1370    pub plot_type: PlotKind,
1371    pub color_rgba: [f32; 4],
1372}
1373
1374impl From<LegendEntry> for FigureLegendEntry {
1375    fn from(entry: LegendEntry) -> Self {
1376        Self {
1377            label: entry.label,
1378            plot_type: PlotKind::from(entry.plot_type),
1379            color_rgba: vec4_to_rgba(entry.color),
1380        }
1381    }
1382}
1383
1384/// Serializable plot kind values consumed by UI + transports.
1385#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1386#[serde(rename_all = "snake_case")]
1387pub enum PlotKind {
1388    Line,
1389    Line3,
1390    Scatter,
1391    Bar,
1392    ErrorBar,
1393    Stairs,
1394    Stem,
1395    Area,
1396    Quiver,
1397    Pie,
1398    Image,
1399    Surface,
1400    Scatter3,
1401    Contour,
1402    ContourFill,
1403}
1404
1405impl From<PlotType> for PlotKind {
1406    fn from(value: PlotType) -> Self {
1407        match value {
1408            PlotType::Line => Self::Line,
1409            PlotType::Line3 => Self::Line3,
1410            PlotType::Scatter => Self::Scatter,
1411            PlotType::Bar => Self::Bar,
1412            PlotType::ErrorBar => Self::ErrorBar,
1413            PlotType::Stairs => Self::Stairs,
1414            PlotType::Stem => Self::Stem,
1415            PlotType::Area => Self::Area,
1416            PlotType::Quiver => Self::Quiver,
1417            PlotType::Pie => Self::Pie,
1418            PlotType::Surface => Self::Surface,
1419            PlotType::Scatter3 => Self::Scatter3,
1420            PlotType::Contour => Self::Contour,
1421            PlotType::ContourFill => Self::ContourFill,
1422        }
1423    }
1424}
1425
1426fn parse_line_style_name(name: &str) -> crate::plots::line::LineStyle {
1427    match name.to_ascii_lowercase().as_str() {
1428        "dashed" => crate::plots::line::LineStyle::Dashed,
1429        "dotted" => crate::plots::line::LineStyle::Dotted,
1430        "dashdot" => crate::plots::line::LineStyle::DashDot,
1431        _ => crate::plots::line::LineStyle::Solid,
1432    }
1433}
1434
1435fn parse_colormap_name(name: &str) -> crate::plots::surface::ColorMap {
1436    match name.trim().to_ascii_lowercase().as_str() {
1437        "viridis" => crate::plots::surface::ColorMap::Viridis,
1438        "plasma" => crate::plots::surface::ColorMap::Plasma,
1439        "inferno" => crate::plots::surface::ColorMap::Inferno,
1440        "magma" => crate::plots::surface::ColorMap::Magma,
1441        "turbo" => crate::plots::surface::ColorMap::Turbo,
1442        "jet" => crate::plots::surface::ColorMap::Jet,
1443        "hot" => crate::plots::surface::ColorMap::Hot,
1444        "cool" => crate::plots::surface::ColorMap::Cool,
1445        "spring" => crate::plots::surface::ColorMap::Spring,
1446        "summer" => crate::plots::surface::ColorMap::Summer,
1447        "autumn" => crate::plots::surface::ColorMap::Autumn,
1448        "winter" => crate::plots::surface::ColorMap::Winter,
1449        "gray" | "grey" => crate::plots::surface::ColorMap::Gray,
1450        "bone" => crate::plots::surface::ColorMap::Bone,
1451        "copper" => crate::plots::surface::ColorMap::Copper,
1452        "pink" => crate::plots::surface::ColorMap::Pink,
1453        "lines" => crate::plots::surface::ColorMap::Lines,
1454        _ => crate::plots::surface::ColorMap::Parula,
1455    }
1456}
1457
1458fn vec4_to_rgba(value: Vec4) -> [f32; 4] {
1459    [value.x, value.y, value.z, value.w]
1460}
1461
1462fn deserialize_f64_lossy<'de, D>(deserializer: D) -> Result<f64, D::Error>
1463where
1464    D: serde::Deserializer<'de>,
1465{
1466    let value = Option::<f64>::deserialize(deserializer)?;
1467    Ok(value.unwrap_or(f64::NAN))
1468}
1469
1470fn deserialize_vec_f64_lossy<'de, D>(deserializer: D) -> Result<Vec<f64>, D::Error>
1471where
1472    D: serde::Deserializer<'de>,
1473{
1474    let values = Vec::<Option<f64>>::deserialize(deserializer)?;
1475    Ok(values
1476        .into_iter()
1477        .map(|value| value.unwrap_or(f64::NAN))
1478        .collect())
1479}
1480
1481fn deserialize_option_vec_f64_lossy<'de, D>(deserializer: D) -> Result<Option<Vec<f64>>, D::Error>
1482where
1483    D: serde::Deserializer<'de>,
1484{
1485    let values = Option::<Vec<Option<f64>>>::deserialize(deserializer)?;
1486    Ok(values.map(|items| {
1487        items
1488            .into_iter()
1489            .map(|value| value.unwrap_or(f64::NAN))
1490            .collect()
1491    }))
1492}
1493
1494fn deserialize_matrix_f64_lossy<'de, D>(deserializer: D) -> Result<Vec<Vec<f64>>, D::Error>
1495where
1496    D: serde::Deserializer<'de>,
1497{
1498    let rows = Vec::<Vec<Option<f64>>>::deserialize(deserializer)?;
1499    Ok(rows
1500        .into_iter()
1501        .map(|row| {
1502            row.into_iter()
1503                .map(|value| value.unwrap_or(f64::NAN))
1504                .collect()
1505        })
1506        .collect())
1507}
1508
1509fn deserialize_option_pair_f64_lossy<'de, D>(deserializer: D) -> Result<Option<[f64; 2]>, D::Error>
1510where
1511    D: serde::Deserializer<'de>,
1512{
1513    let value = Option::<[Option<f64>; 2]>::deserialize(deserializer)?;
1514    Ok(value.map(|pair| [pair[0].unwrap_or(f64::NAN), pair[1].unwrap_or(f64::NAN)]))
1515}
1516
1517fn deserialize_option_vec_f32_lossy<'de, D>(deserializer: D) -> Result<Option<Vec<f32>>, D::Error>
1518where
1519    D: serde::Deserializer<'de>,
1520{
1521    let values = Option::<Vec<Option<f32>>>::deserialize(deserializer)?;
1522    Ok(values.map(|items| {
1523        items
1524            .into_iter()
1525            .map(|value| value.unwrap_or(f32::NAN))
1526            .collect()
1527    }))
1528}
1529
1530fn deserialize_vec_xyz_f32_lossy<'de, D>(deserializer: D) -> Result<Vec<[f32; 3]>, D::Error>
1531where
1532    D: serde::Deserializer<'de>,
1533{
1534    let values = Vec::<[Option<f32>; 3]>::deserialize(deserializer)?;
1535    Ok(values
1536        .into_iter()
1537        .map(|xyz| {
1538            [
1539                xyz[0].unwrap_or(f32::NAN),
1540                xyz[1].unwrap_or(f32::NAN),
1541                xyz[2].unwrap_or(f32::NAN),
1542            ]
1543        })
1544        .collect())
1545}
1546
1547fn deserialize_vec_rgba_f32_lossy<'de, D>(deserializer: D) -> Result<Vec<[f32; 4]>, D::Error>
1548where
1549    D: serde::Deserializer<'de>,
1550{
1551    let values = Vec::<[Option<f32>; 4]>::deserialize(deserializer)?;
1552    Ok(values
1553        .into_iter()
1554        .map(|rgba| {
1555            [
1556                rgba[0].unwrap_or(f32::NAN),
1557                rgba[1].unwrap_or(f32::NAN),
1558                rgba[2].unwrap_or(f32::NAN),
1559                rgba[3].unwrap_or(f32::NAN),
1560            ]
1561        })
1562        .collect())
1563}
1564
1565#[cfg(test)]
1566mod tests {
1567    use super::*;
1568    use crate::plots::{Figure, Line3Plot, LinePlot, Scatter3Plot, ScatterPlot, SurfacePlot};
1569    use glam::Vec3;
1570
1571    #[test]
1572    fn capture_snapshot_reflects_layout_and_metadata() {
1573        let mut figure = Figure::new()
1574            .with_title("Demo")
1575            .with_labels("X", "Y")
1576            .with_grid(false)
1577            .with_subplot_grid(1, 2);
1578        let line = LinePlot::new(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
1579        figure.add_line_plot_on_axes(line, 1);
1580
1581        let snapshot = FigureSnapshot::capture(&figure);
1582        assert_eq!(snapshot.layout.axes_rows, 1);
1583        assert_eq!(snapshot.layout.axes_cols, 2);
1584        assert_eq!(snapshot.metadata.title.as_deref(), Some("Demo"));
1585        assert_eq!(snapshot.metadata.legend_entries.len(), 0);
1586        assert_eq!(snapshot.plots.len(), 1);
1587        assert_eq!(snapshot.plots[0].axes_index, 1);
1588        assert!(!snapshot.metadata.grid_enabled);
1589    }
1590
1591    #[test]
1592    fn figure_scene_roundtrip_reconstructs_supported_plots() {
1593        let mut figure = Figure::new().with_title("Replay").with_subplot_grid(1, 2);
1594        let mut line = LinePlot::new(vec![0.0, 1.0], vec![1.0, 2.0]).unwrap();
1595        line.label = Some("line".to_string());
1596        figure.add_line_plot_on_axes(line, 0);
1597        let mut scatter = ScatterPlot::new(vec![0.0, 1.0, 2.0], vec![2.0, 3.0, 4.0]).unwrap();
1598        scatter.label = Some("scatter".to_string());
1599        figure.add_scatter_plot_on_axes(scatter, 1);
1600
1601        let scene = FigureScene::capture(&figure);
1602        let rebuilt = scene.into_figure().expect("scene restore should succeed");
1603        assert_eq!(rebuilt.axes_grid(), (1, 2));
1604        assert_eq!(rebuilt.plots().count(), 2);
1605        assert_eq!(rebuilt.title.as_deref(), Some("Replay"));
1606    }
1607
1608    #[test]
1609    fn figure_scene_roundtrip_reconstructs_surface_and_scatter3() {
1610        let mut figure = Figure::new().with_title("Replay3D").with_subplot_grid(1, 2);
1611        let mut surface = SurfacePlot::new(
1612            vec![0.0, 1.0],
1613            vec![0.0, 1.0],
1614            vec![vec![0.0, 1.0], vec![1.0, 2.0]],
1615        )
1616        .expect("surface data should be valid");
1617        surface.label = Some("surface".to_string());
1618        figure.add_surface_plot_on_axes(surface, 0);
1619
1620        let mut scatter3 = Scatter3Plot::new(vec![
1621            Vec3::new(0.0, 0.0, 0.0),
1622            Vec3::new(1.0, 2.0, 3.0),
1623            Vec3::new(2.0, 3.0, 4.0),
1624        ])
1625        .expect("scatter3 data should be valid");
1626        scatter3.label = Some("scatter3".to_string());
1627        figure.add_scatter3_plot_on_axes(scatter3, 1);
1628
1629        let scene = FigureScene::capture(&figure);
1630        let rebuilt = scene.into_figure().expect("scene restore should succeed");
1631        assert_eq!(rebuilt.axes_grid(), (1, 2));
1632        assert_eq!(rebuilt.plots().count(), 2);
1633        assert_eq!(rebuilt.title.as_deref(), Some("Replay3D"));
1634        assert!(matches!(
1635            rebuilt.plots().next(),
1636            Some(PlotElement::Surface(_))
1637        ));
1638        assert!(matches!(
1639            rebuilt.plots().nth(1),
1640            Some(PlotElement::Scatter3(_))
1641        ));
1642    }
1643
1644    #[test]
1645    fn figure_scene_roundtrip_preserves_line3_plot() {
1646        let mut figure = Figure::new();
1647        let line3 = Line3Plot::new(vec![0.0, 1.0], vec![1.0, 2.0], vec![2.0, 3.0])
1648            .unwrap()
1649            .with_label("Trajectory");
1650        figure.add_line3_plot(line3);
1651
1652        let rebuilt = FigureScene::capture(&figure)
1653            .into_figure()
1654            .expect("scene restore should succeed");
1655
1656        let PlotElement::Line3(line3) = rebuilt.plots().next().unwrap() else {
1657            panic!("expected line3")
1658        };
1659        assert_eq!(line3.x_data, vec![0.0, 1.0]);
1660        assert_eq!(line3.z_data, vec![2.0, 3.0]);
1661        assert_eq!(line3.label.as_deref(), Some("Trajectory"));
1662    }
1663
1664    #[test]
1665    fn figure_scene_roundtrip_preserves_contour_and_fill_plots() {
1666        let mut figure = Figure::new();
1667        let bounds = BoundingBox::new(Vec3::new(-1.0, -2.0, 0.0), Vec3::new(3.0, 4.0, 0.0));
1668        let vertices = vec![Vertex {
1669            position: [0.0, 0.0, 0.0],
1670            color: [1.0, 0.0, 0.0, 1.0],
1671            normal: [0.0, 0.0, 1.0],
1672            tex_coords: [0.0, 0.0],
1673        }];
1674        let fill = ContourFillPlot::from_vertices(vertices.clone(), bounds).with_label("fill");
1675        let contour = ContourPlot::from_vertices(vertices, 0.0, bounds)
1676            .with_label("lines")
1677            .with_line_width(2.0);
1678        figure.add_contour_fill_plot(fill);
1679        figure.add_contour_plot(contour);
1680
1681        let rebuilt = FigureScene::capture(&figure)
1682            .into_figure()
1683            .expect("scene restore should succeed");
1684        assert!(matches!(
1685            rebuilt.plots().next(),
1686            Some(PlotElement::ContourFill(_))
1687        ));
1688        let Some(PlotElement::Contour(contour)) = rebuilt.plots().nth(1) else {
1689            panic!("expected contour")
1690        };
1691        assert_eq!(contour.line_width, 2.0);
1692    }
1693
1694    #[test]
1695    fn figure_scene_roundtrip_preserves_stem_style_surface() {
1696        let mut figure = Figure::new();
1697        let mut stem = StemPlot::new(vec![0.0, 1.0], vec![1.0, 2.0])
1698            .unwrap()
1699            .with_style(
1700                Vec4::new(1.0, 0.0, 0.0, 1.0),
1701                2.0,
1702                crate::plots::line::LineStyle::Dashed,
1703                -1.0,
1704            )
1705            .with_baseline_style(Vec4::new(0.0, 0.0, 0.0, 1.0), false)
1706            .with_label("Impulse");
1707        stem.set_marker(Some(crate::plots::line::LineMarkerAppearance {
1708            kind: crate::plots::scatter::MarkerStyle::Square,
1709            size: 8.0,
1710            edge_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
1711            face_color: Vec4::new(1.0, 0.0, 0.0, 1.0),
1712            filled: true,
1713        }));
1714        figure.add_stem_plot(stem);
1715
1716        let rebuilt = FigureScene::capture(&figure)
1717            .into_figure()
1718            .expect("scene restore should succeed");
1719        let PlotElement::Stem(stem) = rebuilt.plots().next().unwrap() else {
1720            panic!("expected stem")
1721        };
1722        assert_eq!(stem.baseline, -1.0);
1723        assert_eq!(stem.line_width, 2.0);
1724        assert_eq!(stem.label.as_deref(), Some("Impulse"));
1725        assert!(!stem.baseline_visible);
1726        assert!(stem.marker.as_ref().map(|m| m.filled).unwrap_or(false));
1727        assert_eq!(stem.marker.as_ref().map(|m| m.size), Some(8.0));
1728    }
1729
1730    #[test]
1731    fn figure_scene_roundtrip_preserves_bar_plot() {
1732        let mut figure = Figure::new();
1733        let bar = BarChart::new(vec!["A".into(), "B".into()], vec![2.0, 3.5])
1734            .unwrap()
1735            .with_style(Vec4::new(0.2, 0.4, 0.8, 1.0), 0.95)
1736            .with_outline(Vec4::new(0.1, 0.1, 0.1, 1.0), 1.5)
1737            .with_label("Histogram")
1738            .with_stack_offsets(vec![1.0, 0.5]);
1739        figure.add_bar_chart(bar);
1740
1741        let rebuilt = FigureScene::capture(&figure)
1742            .into_figure()
1743            .expect("scene restore should succeed");
1744        let PlotElement::Bar(bar) = rebuilt.plots().next().unwrap() else {
1745            panic!("expected bar")
1746        };
1747        assert_eq!(bar.labels, vec!["A", "B"]);
1748        assert_eq!(bar.values().unwrap_or(&[]), &[2.0, 3.5]);
1749        assert_eq!(bar.bar_width, 0.95);
1750        assert_eq!(bar.outline_width, 1.5);
1751        assert_eq!(bar.label.as_deref(), Some("Histogram"));
1752        assert_eq!(bar.stack_offsets().unwrap_or(&[]), &[1.0, 0.5]);
1753        assert!(bar.histogram_bin_edges().is_none());
1754    }
1755
1756    #[test]
1757    fn figure_scene_roundtrip_preserves_histogram_bin_edges() {
1758        let mut figure = Figure::new();
1759        let mut bar = BarChart::new(vec!["bin1".into(), "bin2".into()], vec![4.0, 5.0]).unwrap();
1760        bar.set_histogram_bin_edges(vec![0.0, 0.5, 1.0]);
1761        figure.add_bar_chart(bar);
1762
1763        let rebuilt = FigureScene::capture(&figure)
1764            .into_figure()
1765            .expect("scene restore should succeed");
1766        let PlotElement::Bar(bar) = rebuilt.plots().next().unwrap() else {
1767            panic!("expected bar")
1768        };
1769        assert_eq!(bar.histogram_bin_edges().unwrap_or(&[]), &[0.0, 0.5, 1.0]);
1770    }
1771
1772    #[test]
1773    fn figure_scene_roundtrip_preserves_errorbar_style_surface() {
1774        let mut figure = Figure::new();
1775        let mut error = ErrorBar::new_vertical(
1776            vec![0.0, 1.0],
1777            vec![1.0, 2.0],
1778            vec![0.1, 0.2],
1779            vec![0.2, 0.3],
1780        )
1781        .unwrap()
1782        .with_style(
1783            Vec4::new(1.0, 0.0, 0.0, 1.0),
1784            2.0,
1785            crate::plots::line::LineStyle::Dashed,
1786            10.0,
1787        )
1788        .with_label("Err");
1789        error.set_marker(Some(crate::plots::line::LineMarkerAppearance {
1790            kind: crate::plots::scatter::MarkerStyle::Triangle,
1791            size: 8.0,
1792            edge_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
1793            face_color: Vec4::new(1.0, 0.0, 0.0, 1.0),
1794            filled: true,
1795        }));
1796        figure.add_errorbar(error);
1797
1798        let rebuilt = FigureScene::capture(&figure)
1799            .into_figure()
1800            .expect("scene restore should succeed");
1801        let PlotElement::ErrorBar(error) = rebuilt.plots().next().unwrap() else {
1802            panic!("expected errorbar")
1803        };
1804        assert_eq!(error.line_width, 2.0);
1805        assert_eq!(error.cap_size, 10.0);
1806        assert_eq!(error.label.as_deref(), Some("Err"));
1807        assert_eq!(error.line_style, crate::plots::line::LineStyle::Dashed);
1808        assert!(error.marker.as_ref().map(|m| m.filled).unwrap_or(false));
1809    }
1810
1811    #[test]
1812    fn figure_scene_roundtrip_preserves_errorbar_both_direction() {
1813        let mut figure = Figure::new();
1814        let error = ErrorBar::new_both(
1815            vec![1.0, 2.0],
1816            vec![3.0, 4.0],
1817            vec![0.1, 0.2],
1818            vec![0.2, 0.3],
1819            vec![0.3, 0.4],
1820            vec![0.4, 0.5],
1821        )
1822        .unwrap();
1823        figure.add_errorbar(error);
1824        let rebuilt = FigureScene::capture(&figure)
1825            .into_figure()
1826            .expect("scene restore should succeed");
1827        let PlotElement::ErrorBar(error) = rebuilt.plots().next().unwrap() else {
1828            panic!("expected errorbar")
1829        };
1830        assert_eq!(
1831            error.orientation,
1832            crate::plots::errorbar::ErrorBarOrientation::Both
1833        );
1834        assert_eq!(error.x_neg, vec![0.1, 0.2]);
1835        assert_eq!(error.x_pos, vec![0.2, 0.3]);
1836    }
1837
1838    #[test]
1839    fn figure_scene_roundtrip_preserves_quiver_plot() {
1840        let mut figure = Figure::new();
1841        let quiver = QuiverPlot::new(
1842            vec![0.0, 1.0],
1843            vec![1.0, 2.0],
1844            vec![0.5, -0.5],
1845            vec![1.0, 0.25],
1846        )
1847        .unwrap()
1848        .with_style(Vec4::new(0.2, 0.3, 0.4, 1.0), 2.0, 1.5, 0.2)
1849        .with_label("Field");
1850        figure.add_quiver_plot(quiver);
1851
1852        let rebuilt = FigureScene::capture(&figure)
1853            .into_figure()
1854            .expect("scene restore should succeed");
1855        let PlotElement::Quiver(quiver) = rebuilt.plots().next().unwrap() else {
1856            panic!("expected quiver")
1857        };
1858        assert_eq!(quiver.u, vec![0.5, -0.5]);
1859        assert_eq!(quiver.v, vec![1.0, 0.25]);
1860        assert_eq!(quiver.line_width, 2.0);
1861        assert_eq!(quiver.scale, 1.5);
1862        assert_eq!(quiver.head_size, 0.2);
1863        assert_eq!(quiver.label.as_deref(), Some("Field"));
1864    }
1865
1866    #[test]
1867    fn figure_scene_roundtrip_preserves_image_surface_mode_and_color_grid() {
1868        let mut figure = Figure::new();
1869        let surface = SurfacePlot::new(
1870            vec![0.0, 1.0],
1871            vec![0.0, 1.0],
1872            vec![vec![0.0, 0.0], vec![0.0, 0.0]],
1873        )
1874        .unwrap()
1875        .with_flatten_z(true)
1876        .with_image_mode(true)
1877        .with_color_grid(vec![
1878            vec![Vec4::new(1.0, 0.0, 0.0, 1.0), Vec4::new(0.0, 1.0, 0.0, 1.0)],
1879            vec![Vec4::new(0.0, 0.0, 1.0, 1.0), Vec4::new(1.0, 1.0, 1.0, 1.0)],
1880        ]);
1881        figure.add_surface_plot(surface);
1882
1883        let rebuilt = FigureScene::capture(&figure)
1884            .into_figure()
1885            .expect("scene restore should succeed");
1886        let PlotElement::Surface(surface) = rebuilt.plots().next().unwrap() else {
1887            panic!("expected surface")
1888        };
1889        assert!(surface.flatten_z);
1890        assert!(surface.image_mode);
1891        assert!(surface.color_grid.is_some());
1892        assert_eq!(
1893            surface.color_grid.as_ref().unwrap()[0][0],
1894            Vec4::new(1.0, 0.0, 0.0, 1.0)
1895        );
1896    }
1897
1898    #[test]
1899    fn figure_scene_roundtrip_preserves_area_lower_curve() {
1900        let mut figure = Figure::new();
1901        let area = AreaPlot::new(vec![1.0, 2.0], vec![2.0, 3.0])
1902            .unwrap()
1903            .with_lower_curve(vec![0.5, 1.0])
1904            .with_label("Stacked");
1905        figure.add_area_plot(area);
1906
1907        let rebuilt = FigureScene::capture(&figure)
1908            .into_figure()
1909            .expect("scene restore should succeed");
1910        let PlotElement::Area(area) = rebuilt.plots().next().unwrap() else {
1911            panic!("expected area")
1912        };
1913        assert_eq!(area.lower_y, Some(vec![0.5, 1.0]));
1914        assert_eq!(area.label.as_deref(), Some("Stacked"));
1915    }
1916
1917    #[test]
1918    fn figure_scene_roundtrip_preserves_axes_local_limits_and_colormap_state() {
1919        let mut figure = Figure::new().with_subplot_grid(1, 2);
1920        figure.set_axes_limits(1, Some((1.0, 2.0)), Some((3.0, 4.0)));
1921        figure.set_axes_z_limits(1, Some((5.0, 6.0)));
1922        figure.set_axes_grid_enabled(1, false);
1923        figure.set_axes_box_enabled(1, false);
1924        figure.set_axes_axis_equal(1, true);
1925        figure.set_axes_colorbar_enabled(1, true);
1926        figure.set_axes_colormap(1, ColorMap::Hot);
1927        figure.set_axes_color_limits(1, Some((0.0, 10.0)));
1928        figure.set_active_axes_index(1);
1929
1930        let rebuilt = FigureScene::capture(&figure)
1931            .into_figure()
1932            .expect("scene restore should succeed");
1933        let meta = rebuilt.axes_metadata(1).unwrap();
1934        assert_eq!(meta.x_limits, Some((1.0, 2.0)));
1935        assert_eq!(meta.y_limits, Some((3.0, 4.0)));
1936        assert_eq!(meta.z_limits, Some((5.0, 6.0)));
1937        assert!(!meta.grid_enabled);
1938        assert!(!meta.box_enabled);
1939        assert!(meta.axis_equal);
1940        assert!(meta.colorbar_enabled);
1941        assert_eq!(format!("{:?}", meta.colormap), "Hot");
1942        assert_eq!(meta.color_limits, Some((0.0, 10.0)));
1943    }
1944
1945    #[test]
1946    fn figure_scene_roundtrip_preserves_axes_local_annotation_metadata() {
1947        let mut figure = Figure::new().with_subplot_grid(1, 2);
1948        figure.set_active_axes_index(0);
1949        figure.set_axes_title(0, "Left");
1950        figure.set_axes_xlabel(0, "LX");
1951        figure.set_axes_ylabel(0, "LY");
1952        figure.set_axes_legend_enabled(0, false);
1953        figure.set_axes_title(1, "Right");
1954        figure.set_axes_xlabel(1, "RX");
1955        figure.set_axes_ylabel(1, "RY");
1956        figure.set_axes_legend_enabled(1, true);
1957        figure.set_axes_legend_style(
1958            1,
1959            LegendStyle {
1960                location: Some("northeast".into()),
1961                font_weight: Some("bold".into()),
1962                orientation: Some("horizontal".into()),
1963                ..Default::default()
1964            },
1965        );
1966        if let Some(meta) = figure.axes_metadata.get_mut(0) {
1967            meta.title_style.font_weight = Some("bold".into());
1968            meta.title_style.font_angle = Some("italic".into());
1969        }
1970        figure.set_active_axes_index(1);
1971
1972        let rebuilt = FigureScene::capture(&figure)
1973            .into_figure()
1974            .expect("scene restore should succeed");
1975
1976        assert_eq!(rebuilt.active_axes_index, 1);
1977        assert_eq!(
1978            rebuilt.axes_metadata(0).and_then(|m| m.title.as_deref()),
1979            Some("Left")
1980        );
1981        assert_eq!(
1982            rebuilt.axes_metadata(0).and_then(|m| m.x_label.as_deref()),
1983            Some("LX")
1984        );
1985        assert_eq!(
1986            rebuilt.axes_metadata(0).and_then(|m| m.y_label.as_deref()),
1987            Some("LY")
1988        );
1989        assert!(!rebuilt.axes_metadata(0).unwrap().legend_enabled);
1990        assert_eq!(
1991            rebuilt
1992                .axes_metadata(0)
1993                .unwrap()
1994                .title_style
1995                .font_weight
1996                .as_deref(),
1997            Some("bold")
1998        );
1999        assert_eq!(
2000            rebuilt
2001                .axes_metadata(0)
2002                .unwrap()
2003                .title_style
2004                .font_angle
2005                .as_deref(),
2006            Some("italic")
2007        );
2008        assert_eq!(
2009            rebuilt.axes_metadata(1).and_then(|m| m.title.as_deref()),
2010            Some("Right")
2011        );
2012        assert_eq!(
2013            rebuilt.axes_metadata(1).and_then(|m| m.x_label.as_deref()),
2014            Some("RX")
2015        );
2016        assert_eq!(
2017            rebuilt.axes_metadata(1).and_then(|m| m.y_label.as_deref()),
2018            Some("RY")
2019        );
2020        assert_eq!(
2021            rebuilt
2022                .axes_metadata(1)
2023                .unwrap()
2024                .legend_style
2025                .location
2026                .as_deref(),
2027            Some("northeast")
2028        );
2029        assert_eq!(
2030            rebuilt
2031                .axes_metadata(1)
2032                .unwrap()
2033                .legend_style
2034                .font_weight
2035                .as_deref(),
2036            Some("bold")
2037        );
2038        assert_eq!(
2039            rebuilt
2040                .axes_metadata(1)
2041                .unwrap()
2042                .legend_style
2043                .orientation
2044                .as_deref(),
2045            Some("horizontal")
2046        );
2047    }
2048
2049    #[test]
2050    fn figure_scene_roundtrip_preserves_axes_local_log_modes() {
2051        let mut figure = Figure::new().with_subplot_grid(1, 2);
2052        figure.set_axes_log_modes(0, true, false);
2053        figure.set_axes_log_modes(1, false, true);
2054        figure.set_active_axes_index(1);
2055
2056        let rebuilt = FigureScene::capture(&figure)
2057            .into_figure()
2058            .expect("scene restore should succeed");
2059
2060        assert!(rebuilt.axes_metadata(0).unwrap().x_log);
2061        assert!(!rebuilt.axes_metadata(0).unwrap().y_log);
2062        assert!(!rebuilt.axes_metadata(1).unwrap().x_log);
2063        assert!(rebuilt.axes_metadata(1).unwrap().y_log);
2064        assert!(!rebuilt.x_log);
2065        assert!(rebuilt.y_log);
2066    }
2067
2068    #[test]
2069    fn figure_scene_roundtrip_preserves_zlabel_and_view_state() {
2070        let mut figure = Figure::new().with_subplot_grid(1, 2);
2071        figure.set_axes_zlabel(1, "Height");
2072        figure.set_axes_view(1, 45.0, 20.0);
2073        figure.set_active_axes_index(1);
2074
2075        let rebuilt = FigureScene::capture(&figure)
2076            .into_figure()
2077            .expect("scene restore should succeed");
2078
2079        assert_eq!(
2080            rebuilt.axes_metadata(1).unwrap().z_label.as_deref(),
2081            Some("Height")
2082        );
2083        assert_eq!(
2084            rebuilt.axes_metadata(1).unwrap().view_azimuth_deg,
2085            Some(45.0)
2086        );
2087        assert_eq!(
2088            rebuilt.axes_metadata(1).unwrap().view_elevation_deg,
2089            Some(20.0)
2090        );
2091        assert_eq!(rebuilt.z_label.as_deref(), Some("Height"));
2092    }
2093
2094    #[test]
2095    fn figure_scene_roundtrip_preserves_pie_metadata() {
2096        let mut figure = Figure::new();
2097        let pie = crate::plots::PieChart::new(vec![1.0, 2.0], None)
2098            .unwrap()
2099            .with_slice_labels(vec!["A".into(), "B".into()])
2100            .with_explode(vec![false, true]);
2101        figure.add_pie_chart(pie);
2102
2103        let rebuilt = FigureScene::capture(&figure)
2104            .into_figure()
2105            .expect("scene restore should succeed");
2106        let crate::plots::figure::PlotElement::Pie(pie) = rebuilt.plots().next().unwrap() else {
2107            panic!("expected pie")
2108        };
2109        assert_eq!(pie.slice_labels, vec!["A", "B"]);
2110        assert_eq!(pie.explode, vec![false, true]);
2111    }
2112
2113    #[test]
2114    fn scene_plot_deserialize_maps_null_numeric_values_to_nan() {
2115        let json = r#"{
2116          "schemaVersion": 1,
2117          "layout": { "axesRows": 1, "axesCols": 1, "axesIndices": [0] },
2118          "metadata": {
2119            "gridEnabled": true,
2120            "legendEnabled": false,
2121            "colorbarEnabled": false,
2122            "axisEqual": false,
2123            "backgroundRgba": [1,1,1,1],
2124            "legendEntries": []
2125          },
2126          "plots": [
2127            {
2128              "kind": "surface",
2129              "x": [0.0, null],
2130              "y": [0.0, 1.0],
2131              "z": [[0.0, null], [1.0, 2.0]],
2132              "colormap": "Parula",
2133              "shading_mode": "Smooth",
2134              "wireframe": false,
2135              "alpha": 1.0,
2136              "flatten_z": false,
2137              "color_limits": null,
2138              "axes_index": 0,
2139              "label": null,
2140              "visible": true
2141            }
2142          ]
2143        }"#;
2144        let scene: FigureScene = serde_json::from_str(json).expect("scene should deserialize");
2145        let ScenePlot::Surface { x, z, .. } = &scene.plots[0] else {
2146            panic!("expected surface plot");
2147        };
2148        assert!(x[1].is_nan());
2149        assert!(z[0][1].is_nan());
2150    }
2151
2152    #[test]
2153    fn scene_plot_deserialize_maps_null_scatter3_components_to_nan() {
2154        let json = r#"{
2155          "schemaVersion": 1,
2156          "layout": { "axesRows": 1, "axesCols": 1, "axesIndices": [0] },
2157          "metadata": {
2158            "gridEnabled": true,
2159            "legendEnabled": false,
2160            "colorbarEnabled": false,
2161            "axisEqual": false,
2162            "backgroundRgba": [1,1,1,1],
2163            "legendEntries": []
2164          },
2165          "plots": [
2166            {
2167              "kind": "scatter3",
2168              "points": [[0.0, 1.0, null], [1.0, null, 2.0]],
2169              "colors_rgba": [[0.2, 0.4, 0.6, 1.0], [0.1, 0.2, 0.3, 1.0]],
2170              "point_size": 6.0,
2171              "point_sizes": [3.0, null],
2172              "axes_index": 0,
2173              "label": null,
2174              "visible": true
2175            }
2176          ]
2177        }"#;
2178        let scene: FigureScene = serde_json::from_str(json).expect("scene should deserialize");
2179        let ScenePlot::Scatter3 {
2180            points,
2181            point_sizes,
2182            ..
2183        } = &scene.plots[0]
2184        else {
2185            panic!("expected scatter3 plot");
2186        };
2187        assert!(points[0][2].is_nan());
2188        assert!(points[1][1].is_nan());
2189        assert!(point_sizes.as_ref().unwrap()[1].is_nan());
2190    }
2191}