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#[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#[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#[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#[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 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#[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#[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#[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#[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#[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}