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