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