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