1use crate::core::{BoundingBox, GpuPackContext, RenderData};
7use crate::plots::surface::ColorMap;
8use crate::plots::{
9 AreaPlot, BarChart, ContourFillPlot, ContourPlot, ErrorBar, Line3Plot, LinePlot, PieChart,
10 QuiverPlot, Scatter3Plot, ScatterPlot, StairsPlot, StemPlot, SurfacePlot,
11};
12use glam::Vec4;
13use log::trace;
14use std::collections::HashMap;
15
16#[derive(Debug, Clone)]
18pub struct Figure {
19 plots: Vec<PlotElement>,
21
22 pub title: Option<String>,
24 pub sg_title: Option<String>,
25 pub x_label: Option<String>,
26 pub y_label: Option<String>,
27 pub z_label: Option<String>,
28 pub legend_enabled: bool,
29 pub grid_enabled: bool,
30 pub box_enabled: bool,
31 pub background_color: Vec4,
32
33 pub x_limits: Option<(f64, f64)>,
35 pub y_limits: Option<(f64, f64)>,
36 pub z_limits: Option<(f64, f64)>,
37
38 pub x_log: bool,
40 pub y_log: bool,
41
42 pub axis_equal: bool,
44
45 pub colormap: ColorMap,
47 pub colorbar_enabled: bool,
48
49 pub color_limits: Option<(f64, f64)>,
51
52 bounds: Option<BoundingBox>,
54 dirty: bool,
55
56 pub axes_rows: usize,
58 pub axes_cols: usize,
59 plot_axes_indices: Vec<usize>,
61
62 pub active_axes_index: usize,
64
65 pub axes_metadata: Vec<AxesMetadata>,
67 pub sg_title_style: TextStyle,
68}
69
70#[derive(Debug, Clone)]
71pub struct TextStyle {
72 pub color: Option<Vec4>,
73 pub font_size: Option<f32>,
74 pub font_weight: Option<String>,
75 pub font_angle: Option<String>,
76 pub interpreter: Option<String>,
77 pub visible: bool,
78}
79
80impl Default for TextStyle {
81 fn default() -> Self {
82 Self {
83 color: None,
84 font_size: None,
85 font_weight: None,
86 font_angle: None,
87 interpreter: None,
88 visible: true,
89 }
90 }
91}
92
93#[derive(Debug, Clone)]
94pub struct LegendStyle {
95 pub location: Option<String>,
96 pub visible: bool,
97 pub font_size: Option<f32>,
98 pub font_weight: Option<String>,
99 pub font_angle: Option<String>,
100 pub interpreter: Option<String>,
101 pub box_visible: Option<bool>,
102 pub orientation: Option<String>,
103 pub text_color: Option<Vec4>,
104}
105
106impl Default for LegendStyle {
107 fn default() -> Self {
108 Self {
109 location: None,
110 visible: true,
111 font_size: None,
112 font_weight: None,
113 font_angle: None,
114 interpreter: None,
115 box_visible: None,
116 orientation: None,
117 text_color: None,
118 }
119 }
120}
121
122#[derive(Debug, Clone, Default)]
123pub struct AxesMetadata {
124 pub title: Option<String>,
125 pub x_label: Option<String>,
126 pub y_label: Option<String>,
127 pub z_label: Option<String>,
128 pub x_limits: Option<(f64, f64)>,
129 pub y_limits: Option<(f64, f64)>,
130 pub z_limits: Option<(f64, f64)>,
131 pub x_log: bool,
132 pub y_log: bool,
133 pub view_azimuth_deg: Option<f32>,
134 pub view_elevation_deg: Option<f32>,
135 pub grid_enabled: bool,
136 pub box_enabled: bool,
137 pub axis_equal: bool,
138 pub legend_enabled: bool,
139 pub colorbar_enabled: bool,
140 pub colormap: ColorMap,
141 pub color_limits: Option<(f64, f64)>,
142 pub title_style: TextStyle,
143 pub x_label_style: TextStyle,
144 pub y_label_style: TextStyle,
145 pub z_label_style: TextStyle,
146 pub legend_style: LegendStyle,
147 pub world_text_annotations: Vec<TextAnnotation>,
148}
149
150#[derive(Debug, Clone)]
151pub struct TextAnnotation {
152 pub position: glam::Vec3,
153 pub text: String,
154 pub style: TextStyle,
155}
156
157#[derive(Debug, Clone)]
159pub enum PlotElement {
160 Line(LinePlot),
161 Scatter(ScatterPlot),
162 Bar(BarChart),
163 ErrorBar(ErrorBar),
164 Stairs(StairsPlot),
165 Stem(StemPlot),
166 Area(AreaPlot),
167 Quiver(QuiverPlot),
168 Pie(PieChart),
169 Surface(SurfacePlot),
170 Line3(Line3Plot),
171 Scatter3(Scatter3Plot),
172 Contour(ContourPlot),
173 ContourFill(ContourFillPlot),
174}
175
176#[derive(Debug, Clone)]
178pub struct LegendEntry {
179 pub label: String,
180 pub color: Vec4,
181 pub plot_type: PlotType,
182}
183
184#[derive(Debug, Clone)]
185pub struct PieLabelEntry {
186 pub label: String,
187 pub position: glam::Vec2,
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
192pub enum PlotType {
193 Line,
194 Scatter,
195 Bar,
196 ErrorBar,
197 Stairs,
198 Stem,
199 Area,
200 Quiver,
201 Pie,
202 Surface,
203 Line3,
204 Scatter3,
205 Contour,
206 ContourFill,
207}
208
209impl Figure {
210 pub fn new() -> Self {
212 Self {
213 plots: Vec::new(),
214 title: None,
215 sg_title: None,
216 x_label: None,
217 y_label: None,
218 z_label: None,
219 legend_enabled: true,
220 grid_enabled: true,
221 box_enabled: true,
222 background_color: Vec4::new(1.0, 1.0, 1.0, 1.0), x_limits: None,
224 y_limits: None,
225 z_limits: None,
226 x_log: false,
227 y_log: false,
228 axis_equal: false,
229 colormap: ColorMap::Parula,
230 colorbar_enabled: false,
231 color_limits: None,
232 bounds: None,
233 dirty: true,
234 axes_rows: 1,
235 axes_cols: 1,
236 plot_axes_indices: Vec::new(),
237 active_axes_index: 0,
238 axes_metadata: vec![AxesMetadata {
239 x_limits: None,
240 y_limits: None,
241 z_limits: None,
242 grid_enabled: true,
243 box_enabled: true,
244 axis_equal: false,
245 legend_enabled: true,
246 colorbar_enabled: false,
247 colormap: ColorMap::Parula,
248 color_limits: None,
249 ..Default::default()
250 }],
251 sg_title_style: TextStyle::default(),
252 }
253 }
254
255 fn ensure_axes_metadata_capacity(&mut self, min_len: usize) {
256 while self.axes_metadata.len() < min_len.max(1) {
257 self.axes_metadata.push(AxesMetadata {
258 x_limits: None,
259 y_limits: None,
260 z_limits: None,
261 grid_enabled: true,
262 box_enabled: true,
263 axis_equal: false,
264 legend_enabled: true,
265 colorbar_enabled: false,
266 colormap: ColorMap::Parula,
267 color_limits: None,
268 ..Default::default()
269 });
270 }
271 }
272
273 fn sync_legacy_fields_from_active_axes(&mut self) {
274 self.ensure_axes_metadata_capacity(self.active_axes_index + 1);
275 if let Some(meta) = self.axes_metadata.get(self.active_axes_index).cloned() {
276 self.title = meta.title;
277 self.x_label = meta.x_label;
278 self.y_label = meta.y_label;
279 self.z_label = meta.z_label;
280 self.x_limits = meta.x_limits;
281 self.y_limits = meta.y_limits;
282 self.z_limits = meta.z_limits;
283 self.x_log = meta.x_log;
284 self.y_log = meta.y_log;
285 self.grid_enabled = meta.grid_enabled;
286 self.box_enabled = meta.box_enabled;
287 self.axis_equal = meta.axis_equal;
288 self.legend_enabled = meta.legend_enabled;
289 self.colorbar_enabled = meta.colorbar_enabled;
290 self.colormap = meta.colormap;
291 self.color_limits = meta.color_limits;
292 }
293 }
294
295 pub fn set_active_axes_index(&mut self, axes_index: usize) {
296 self.ensure_axes_metadata_capacity(axes_index + 1);
297 self.active_axes_index = axes_index;
298 self.sync_legacy_fields_from_active_axes();
299 self.dirty = true;
300 }
301
302 pub fn axes_metadata(&self, axes_index: usize) -> Option<&AxesMetadata> {
303 self.axes_metadata.get(axes_index)
304 }
305
306 pub fn active_axes_metadata(&self) -> Option<&AxesMetadata> {
307 self.axes_metadata(self.active_axes_index)
308 }
309
310 pub fn with_sg_title<S: Into<String>>(mut self, title: S) -> Self {
311 self.set_sg_title(title);
312 self
313 }
314
315 pub fn set_sg_title<S: Into<String>>(&mut self, title: S) {
316 self.sg_title = Some(title.into());
317 self.dirty = true;
318 }
319
320 pub fn clear_sg_title(&mut self) {
321 self.sg_title = None;
322 self.dirty = true;
323 }
324
325 pub fn set_sg_title_style(&mut self, style: TextStyle) {
326 self.sg_title_style = style;
327 self.dirty = true;
328 }
329
330 pub fn has_any_titles(&self) -> bool {
331 let non_empty = |s: Option<&str>| s.map(str::trim).is_some_and(|t| !t.is_empty());
332 non_empty(self.sg_title.as_deref())
333 || non_empty(self.title.as_deref())
334 || self
335 .axes_metadata
336 .iter()
337 .any(|meta| non_empty(meta.title.as_deref()))
338 }
339
340 pub fn with_title<S: Into<String>>(mut self, title: S) -> Self {
342 self.set_title(title);
343 self
344 }
345
346 pub fn set_title<S: Into<String>>(&mut self, title: S) {
348 self.set_axes_title(self.active_axes_index, title);
349 }
350
351 pub fn with_labels<S: Into<String>>(mut self, x_label: S, y_label: S) -> Self {
353 self.set_axis_labels(x_label, y_label);
354 self
355 }
356
357 pub fn set_axis_labels<S: Into<String>>(&mut self, x_label: S, y_label: S) {
359 self.set_axes_labels(self.active_axes_index, x_label, y_label);
360 self.dirty = true;
361 }
362
363 pub fn set_axes_title<S: Into<String>>(&mut self, axes_index: usize, title: S) {
364 self.ensure_axes_metadata_capacity(axes_index + 1);
365 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
366 meta.title = Some(title.into());
367 }
368 if axes_index == self.active_axes_index {
369 self.sync_legacy_fields_from_active_axes();
370 }
371 self.dirty = true;
372 }
373
374 pub fn set_axes_xlabel<S: Into<String>>(&mut self, axes_index: usize, label: S) {
375 self.ensure_axes_metadata_capacity(axes_index + 1);
376 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
377 meta.x_label = Some(label.into());
378 }
379 if axes_index == self.active_axes_index {
380 self.sync_legacy_fields_from_active_axes();
381 }
382 self.dirty = true;
383 }
384
385 pub fn set_axes_ylabel<S: Into<String>>(&mut self, axes_index: usize, label: S) {
386 self.ensure_axes_metadata_capacity(axes_index + 1);
387 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
388 meta.y_label = Some(label.into());
389 }
390 if axes_index == self.active_axes_index {
391 self.sync_legacy_fields_from_active_axes();
392 }
393 self.dirty = true;
394 }
395
396 pub fn set_axes_zlabel<S: Into<String>>(&mut self, axes_index: usize, label: S) {
397 self.ensure_axes_metadata_capacity(axes_index + 1);
398 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
399 meta.z_label = Some(label.into());
400 }
401 if axes_index == self.active_axes_index {
402 self.sync_legacy_fields_from_active_axes();
403 }
404 self.dirty = true;
405 }
406
407 pub fn add_axes_text_annotation<S: Into<String>>(
408 &mut self,
409 axes_index: usize,
410 position: glam::Vec3,
411 text: S,
412 style: TextStyle,
413 ) -> usize {
414 self.ensure_axes_metadata_capacity(axes_index + 1);
415 let Some(meta) = self.axes_metadata.get_mut(axes_index) else {
416 return 0;
417 };
418 meta.world_text_annotations.push(TextAnnotation {
419 position,
420 text: text.into(),
421 style,
422 });
423 self.dirty = true;
424 meta.world_text_annotations.len() - 1
425 }
426
427 pub fn axes_text_annotation(
428 &self,
429 axes_index: usize,
430 annotation_index: usize,
431 ) -> Option<&TextAnnotation> {
432 self.axes_metadata
433 .get(axes_index)
434 .and_then(|meta| meta.world_text_annotations.get(annotation_index))
435 }
436
437 pub fn set_axes_text_annotation_text<S: Into<String>>(
438 &mut self,
439 axes_index: usize,
440 annotation_index: usize,
441 text: S,
442 ) {
443 if let Some(annotation) = self
444 .axes_metadata
445 .get_mut(axes_index)
446 .and_then(|meta| meta.world_text_annotations.get_mut(annotation_index))
447 {
448 annotation.text = text.into();
449 self.dirty = true;
450 }
451 }
452
453 pub fn set_axes_text_annotation_position(
454 &mut self,
455 axes_index: usize,
456 annotation_index: usize,
457 position: glam::Vec3,
458 ) {
459 if let Some(annotation) = self
460 .axes_metadata
461 .get_mut(axes_index)
462 .and_then(|meta| meta.world_text_annotations.get_mut(annotation_index))
463 {
464 annotation.position = position;
465 self.dirty = true;
466 }
467 }
468
469 pub fn set_axes_text_annotation_style(
470 &mut self,
471 axes_index: usize,
472 annotation_index: usize,
473 style: TextStyle,
474 ) {
475 if let Some(annotation) = self
476 .axes_metadata
477 .get_mut(axes_index)
478 .and_then(|meta| meta.world_text_annotations.get_mut(annotation_index))
479 {
480 annotation.style = style;
481 self.dirty = true;
482 }
483 }
484
485 pub fn axes_text_annotations(&self, axes_index: usize) -> &[TextAnnotation] {
486 self.axes_metadata
487 .get(axes_index)
488 .map(|meta| meta.world_text_annotations.as_slice())
489 .unwrap_or(&[])
490 }
491
492 pub fn set_axes_labels<S: Into<String>>(&mut self, axes_index: usize, x_label: S, y_label: S) {
493 self.ensure_axes_metadata_capacity(axes_index + 1);
494 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
495 meta.x_label = Some(x_label.into());
496 meta.y_label = Some(y_label.into());
497 }
498 if axes_index == self.active_axes_index {
499 self.sync_legacy_fields_from_active_axes();
500 }
501 self.dirty = true;
502 }
503
504 pub fn set_axes_title_style(&mut self, axes_index: usize, style: TextStyle) {
505 self.ensure_axes_metadata_capacity(axes_index + 1);
506 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
507 meta.title_style = style;
508 }
509 self.dirty = true;
510 }
511
512 pub fn set_axes_xlabel_style(&mut self, axes_index: usize, style: TextStyle) {
513 self.ensure_axes_metadata_capacity(axes_index + 1);
514 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
515 meta.x_label_style = style;
516 }
517 self.dirty = true;
518 }
519
520 pub fn set_axes_ylabel_style(&mut self, axes_index: usize, style: TextStyle) {
521 self.ensure_axes_metadata_capacity(axes_index + 1);
522 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
523 meta.y_label_style = style;
524 }
525 self.dirty = true;
526 }
527
528 pub fn set_axes_zlabel_style(&mut self, axes_index: usize, style: TextStyle) {
529 self.ensure_axes_metadata_capacity(axes_index + 1);
530 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
531 meta.z_label_style = style;
532 }
533 self.dirty = true;
534 }
535
536 pub fn with_limits(mut self, x_limits: (f64, f64), y_limits: (f64, f64)) -> Self {
538 self.x_limits = Some(x_limits);
539 self.y_limits = Some(y_limits);
540 self.dirty = true;
541 self
542 }
543
544 pub fn with_legend(mut self, enabled: bool) -> Self {
546 self.set_legend(enabled);
547 self
548 }
549
550 pub fn set_legend(&mut self, enabled: bool) {
551 self.set_axes_legend_enabled(self.active_axes_index, enabled);
552 }
553
554 pub fn set_axes_legend_enabled(&mut self, axes_index: usize, enabled: bool) {
555 self.ensure_axes_metadata_capacity(axes_index + 1);
556 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
557 meta.legend_enabled = enabled;
558 }
559 if axes_index == self.active_axes_index {
560 self.sync_legacy_fields_from_active_axes();
561 }
562 self.dirty = true;
563 }
564
565 pub fn set_axes_legend_style(&mut self, axes_index: usize, style: LegendStyle) {
566 self.ensure_axes_metadata_capacity(axes_index + 1);
567 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
568 meta.legend_style = style;
569 }
570 self.dirty = true;
571 }
572
573 pub fn set_axes_log_modes(&mut self, axes_index: usize, x_log: bool, y_log: bool) {
574 self.ensure_axes_metadata_capacity(axes_index + 1);
575 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
576 meta.x_log = x_log;
577 meta.y_log = y_log;
578 }
579 if axes_index == self.active_axes_index {
580 self.sync_legacy_fields_from_active_axes();
581 }
582 self.dirty = true;
583 }
584
585 pub fn set_axes_view(&mut self, axes_index: usize, azimuth_deg: f32, elevation_deg: f32) {
586 self.ensure_axes_metadata_capacity(axes_index + 1);
587 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
588 meta.view_azimuth_deg = Some(azimuth_deg);
589 meta.view_elevation_deg = Some(elevation_deg);
590 }
591 self.dirty = true;
592 }
593
594 pub fn with_grid(mut self, enabled: bool) -> Self {
596 self.set_grid(enabled);
597 self
598 }
599
600 pub fn set_grid(&mut self, enabled: bool) {
601 self.set_axes_grid_enabled(self.active_axes_index, enabled);
602 self.dirty = true;
603 }
604
605 pub fn set_axes_grid_enabled(&mut self, axes_index: usize, enabled: bool) {
606 self.ensure_axes_metadata_capacity(axes_index + 1);
607 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
608 meta.grid_enabled = enabled;
609 }
610 if axes_index == self.active_axes_index {
611 self.sync_legacy_fields_from_active_axes();
612 }
613 self.dirty = true;
614 }
615
616 pub fn with_background_color(mut self, color: Vec4) -> Self {
618 self.background_color = color;
619 self
620 }
621
622 pub fn with_xlog(mut self, enabled: bool) -> Self {
624 self.set_axes_log_modes(self.active_axes_index, enabled, self.y_log);
625 self
626 }
627 pub fn with_ylog(mut self, enabled: bool) -> Self {
628 self.set_axes_log_modes(self.active_axes_index, self.x_log, enabled);
629 self
630 }
631 pub fn with_axis_equal(mut self, enabled: bool) -> Self {
632 self.set_axis_equal(enabled);
633 self
634 }
635
636 pub fn set_axis_equal(&mut self, enabled: bool) {
637 self.set_axes_axis_equal(self.active_axes_index, enabled);
638 self.dirty = true;
639 }
640 pub fn set_axes_axis_equal(&mut self, axes_index: usize, enabled: bool) {
641 self.ensure_axes_metadata_capacity(axes_index + 1);
642 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
643 meta.axis_equal = enabled;
644 }
645 if axes_index == self.active_axes_index {
646 self.sync_legacy_fields_from_active_axes();
647 }
648 self.dirty = true;
649 }
650 pub fn with_colormap(mut self, cmap: ColorMap) -> Self {
651 self.set_axes_colormap(self.active_axes_index, cmap);
652 self
653 }
654 pub fn with_colorbar(mut self, enabled: bool) -> Self {
655 self.set_axes_colorbar_enabled(self.active_axes_index, enabled);
656 self
657 }
658 pub fn with_color_limits(mut self, limits: Option<(f64, f64)>) -> Self {
659 self.set_axes_color_limits(self.active_axes_index, limits);
660 self
661 }
662
663 pub fn with_subplot_grid(mut self, rows: usize, cols: usize) -> Self {
665 self.set_subplot_grid(rows, cols);
666 self
667 }
668
669 pub fn axes_grid(&self) -> (usize, usize) {
671 (self.axes_rows, self.axes_cols)
672 }
673
674 pub fn plot_axes_indices(&self) -> &[usize] {
676 &self.plot_axes_indices
677 }
678
679 pub fn assign_plot_to_axes(
681 &mut self,
682 plot_index: usize,
683 axes_index: usize,
684 ) -> Result<(), String> {
685 if plot_index >= self.plot_axes_indices.len() {
686 return Err(format!(
687 "assign_plot_to_axes: index {plot_index} out of bounds"
688 ));
689 }
690 let max_axes = self.axes_rows.max(1) * self.axes_cols.max(1);
691 let ai = axes_index.min(max_axes.saturating_sub(1));
692 self.plot_axes_indices[plot_index] = ai;
693 self.dirty = true;
694 Ok(())
695 }
696 pub fn set_subplot_grid(&mut self, rows: usize, cols: usize) {
698 self.axes_rows = rows.max(1);
699 self.axes_cols = cols.max(1);
700 self.ensure_axes_metadata_capacity(self.axes_rows * self.axes_cols);
701 self.active_axes_index = self.active_axes_index.min(
702 self.axes_rows
703 .saturating_mul(self.axes_cols)
704 .saturating_sub(1),
705 );
706 self.sync_legacy_fields_from_active_axes();
707 self.dirty = true;
708 }
709
710 pub fn set_color_limits(&mut self, limits: Option<(f64, f64)>) {
712 self.set_axes_color_limits(self.active_axes_index, limits);
713 self.dirty = true;
714 }
715
716 pub fn set_z_limits(&mut self, limits: Option<(f64, f64)>) {
717 self.set_axes_z_limits(self.active_axes_index, limits);
718 self.dirty = true;
719 }
720
721 pub fn set_axes_limits(
722 &mut self,
723 axes_index: usize,
724 x: Option<(f64, f64)>,
725 y: Option<(f64, f64)>,
726 ) {
727 self.ensure_axes_metadata_capacity(axes_index + 1);
728 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
729 meta.x_limits = x;
730 meta.y_limits = y;
731 }
732 if axes_index == self.active_axes_index {
733 self.sync_legacy_fields_from_active_axes();
734 }
735 self.dirty = true;
736 }
737
738 pub fn set_axes_z_limits(&mut self, axes_index: usize, limits: Option<(f64, f64)>) {
739 self.ensure_axes_metadata_capacity(axes_index + 1);
740 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
741 meta.z_limits = limits;
742 }
743 if axes_index == self.active_axes_index {
744 self.sync_legacy_fields_from_active_axes();
745 }
746 self.dirty = true;
747 }
748
749 pub fn set_axes_box_enabled(&mut self, axes_index: usize, enabled: bool) {
750 self.ensure_axes_metadata_capacity(axes_index + 1);
751 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
752 meta.box_enabled = enabled;
753 }
754 if axes_index == self.active_axes_index {
755 self.sync_legacy_fields_from_active_axes();
756 }
757 self.dirty = true;
758 }
759
760 pub fn set_axes_colorbar_enabled(&mut self, axes_index: usize, enabled: bool) {
761 self.ensure_axes_metadata_capacity(axes_index + 1);
762 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
763 meta.colorbar_enabled = enabled;
764 }
765 if axes_index == self.active_axes_index {
766 self.sync_legacy_fields_from_active_axes();
767 }
768 self.dirty = true;
769 }
770
771 pub fn set_axes_colormap(&mut self, axes_index: usize, cmap: ColorMap) {
772 self.ensure_axes_metadata_capacity(axes_index + 1);
773 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
774 meta.colormap = cmap;
775 }
776 for (idx, plot) in self.plots.iter_mut().enumerate() {
777 if self.plot_axes_indices.get(idx).copied().unwrap_or(0) != axes_index {
778 continue;
779 }
780 if let PlotElement::Surface(surface) = plot {
781 *surface = surface.clone().with_colormap(cmap);
782 }
783 }
784 if axes_index == self.active_axes_index {
785 self.sync_legacy_fields_from_active_axes();
786 }
787 self.dirty = true;
788 }
789
790 pub fn set_axes_color_limits(&mut self, axes_index: usize, limits: Option<(f64, f64)>) {
791 self.ensure_axes_metadata_capacity(axes_index + 1);
792 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
793 meta.color_limits = limits;
794 }
795 for (idx, plot) in self.plots.iter_mut().enumerate() {
796 if self.plot_axes_indices.get(idx).copied().unwrap_or(0) != axes_index {
797 continue;
798 }
799 if let PlotElement::Surface(surface) = plot {
800 surface.set_color_limits(limits);
801 }
802 }
803 if axes_index == self.active_axes_index {
804 self.sync_legacy_fields_from_active_axes();
805 }
806 self.dirty = true;
807 }
808
809 fn total_axes(&self) -> usize {
810 self.axes_rows.max(1) * self.axes_cols.max(1)
811 }
812
813 fn normalize_axes_index(&self, axes_index: usize) -> usize {
814 let total = self.total_axes().max(1);
815 axes_index.min(total - 1)
816 }
817
818 fn push_plot(&mut self, element: PlotElement, axes_index: usize) -> usize {
819 let idx = self.normalize_axes_index(axes_index);
820 self.plots.push(element);
821 self.plot_axes_indices.push(idx);
822 self.dirty = true;
823 self.plots.len() - 1
824 }
825
826 pub fn add_line_plot(&mut self, plot: LinePlot) -> usize {
828 self.add_line_plot_on_axes(plot, 0)
829 }
830
831 pub fn add_line_plot_on_axes(&mut self, plot: LinePlot, axes_index: usize) -> usize {
832 self.push_plot(PlotElement::Line(plot), axes_index)
833 }
834
835 pub fn add_scatter_plot(&mut self, plot: ScatterPlot) -> usize {
837 self.add_scatter_plot_on_axes(plot, 0)
838 }
839
840 pub fn add_scatter_plot_on_axes(&mut self, plot: ScatterPlot, axes_index: usize) -> usize {
841 self.push_plot(PlotElement::Scatter(plot), axes_index)
842 }
843
844 pub fn add_bar_chart(&mut self, plot: BarChart) -> usize {
846 self.add_bar_chart_on_axes(plot, 0)
847 }
848
849 pub fn add_bar_chart_on_axes(&mut self, plot: BarChart, axes_index: usize) -> usize {
850 self.push_plot(PlotElement::Bar(plot), axes_index)
851 }
852
853 pub fn add_errorbar(&mut self, plot: ErrorBar) -> usize {
855 self.add_errorbar_on_axes(plot, 0)
856 }
857
858 pub fn add_errorbar_on_axes(&mut self, plot: ErrorBar, axes_index: usize) -> usize {
859 self.push_plot(PlotElement::ErrorBar(plot), axes_index)
860 }
861
862 pub fn add_stairs_plot(&mut self, plot: StairsPlot) -> usize {
864 self.add_stairs_plot_on_axes(plot, 0)
865 }
866
867 pub fn add_stairs_plot_on_axes(&mut self, plot: StairsPlot, axes_index: usize) -> usize {
868 self.push_plot(PlotElement::Stairs(plot), axes_index)
869 }
870
871 pub fn add_stem_plot(&mut self, plot: StemPlot) -> usize {
873 self.add_stem_plot_on_axes(plot, 0)
874 }
875
876 pub fn add_stem_plot_on_axes(&mut self, plot: StemPlot, axes_index: usize) -> usize {
877 self.push_plot(PlotElement::Stem(plot), axes_index)
878 }
879
880 pub fn add_area_plot(&mut self, plot: AreaPlot) -> usize {
882 self.add_area_plot_on_axes(plot, 0)
883 }
884
885 pub fn add_area_plot_on_axes(&mut self, plot: AreaPlot, axes_index: usize) -> usize {
886 self.push_plot(PlotElement::Area(plot), axes_index)
887 }
888
889 pub fn add_quiver_plot(&mut self, plot: QuiverPlot) -> usize {
890 self.add_quiver_plot_on_axes(plot, 0)
891 }
892
893 pub fn add_quiver_plot_on_axes(&mut self, plot: QuiverPlot, axes_index: usize) -> usize {
894 self.push_plot(PlotElement::Quiver(plot), axes_index)
895 }
896
897 pub fn add_pie_chart(&mut self, plot: PieChart) -> usize {
898 self.add_pie_chart_on_axes(plot, 0)
899 }
900
901 pub fn add_pie_chart_on_axes(&mut self, plot: PieChart, axes_index: usize) -> usize {
902 self.push_plot(PlotElement::Pie(plot), axes_index)
903 }
904
905 pub fn add_surface_plot(&mut self, plot: SurfacePlot) -> usize {
907 self.add_surface_plot_on_axes(plot, 0)
908 }
909
910 pub fn add_surface_plot_on_axes(&mut self, plot: SurfacePlot, axes_index: usize) -> usize {
911 self.push_plot(PlotElement::Surface(plot), axes_index)
912 }
913
914 pub fn add_line3_plot(&mut self, plot: Line3Plot) -> usize {
915 self.add_line3_plot_on_axes(plot, self.active_axes_index)
916 }
917
918 pub fn add_line3_plot_on_axes(&mut self, plot: Line3Plot, axes_index: usize) -> usize {
919 self.push_plot(PlotElement::Line3(plot), axes_index)
920 }
921
922 pub fn add_scatter3_plot(&mut self, plot: Scatter3Plot) -> usize {
924 self.add_scatter3_plot_on_axes(plot, 0)
925 }
926
927 pub fn add_scatter3_plot_on_axes(&mut self, plot: Scatter3Plot, axes_index: usize) -> usize {
928 self.push_plot(PlotElement::Scatter3(plot), axes_index)
929 }
930
931 pub fn add_contour_plot(&mut self, plot: ContourPlot) -> usize {
932 self.add_contour_plot_on_axes(plot, 0)
933 }
934
935 pub fn add_contour_plot_on_axes(&mut self, plot: ContourPlot, axes_index: usize) -> usize {
936 self.push_plot(PlotElement::Contour(plot), axes_index)
937 }
938
939 pub fn add_contour_fill_plot(&mut self, plot: ContourFillPlot) -> usize {
940 self.add_contour_fill_plot_on_axes(plot, 0)
941 }
942
943 pub fn add_contour_fill_plot_on_axes(
944 &mut self,
945 plot: ContourFillPlot,
946 axes_index: usize,
947 ) -> usize {
948 self.push_plot(PlotElement::ContourFill(plot), axes_index)
949 }
950
951 pub fn remove_plot(&mut self, index: usize) -> Result<(), String> {
953 if index >= self.plots.len() {
954 return Err(format!("Plot index {index} out of bounds"));
955 }
956 self.plots.remove(index);
957 self.plot_axes_indices.remove(index);
958 self.dirty = true;
959 Ok(())
960 }
961
962 pub fn clear(&mut self) {
964 self.plots.clear();
965 self.plot_axes_indices.clear();
966 self.dirty = true;
967 }
968
969 pub fn clear_axes(&mut self, axes_index: usize) {
971 let mut i = 0usize;
972 while i < self.plots.len() {
973 let ax = *self.plot_axes_indices.get(i).unwrap_or(&0);
974 if ax == axes_index {
975 self.plots.remove(i);
976 self.plot_axes_indices.remove(i);
977 } else {
978 i += 1;
979 }
980 }
981 self.ensure_axes_metadata_capacity(axes_index + 1);
982 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
983 meta.world_text_annotations.clear();
984 }
985 self.dirty = true;
986 }
987
988 pub fn len(&self) -> usize {
990 self.plots.len()
991 }
992
993 pub fn is_empty(&self) -> bool {
995 self.plots.is_empty()
996 }
997
998 pub fn plots(&self) -> impl Iterator<Item = &PlotElement> {
1000 self.plots.iter()
1001 }
1002
1003 pub fn get_plot_mut(&mut self, index: usize) -> Option<&mut PlotElement> {
1005 self.dirty = true;
1006 self.plots.get_mut(index)
1007 }
1008
1009 pub fn bounds(&mut self) -> BoundingBox {
1011 if self.dirty || self.bounds.is_none() {
1012 self.compute_bounds();
1013 }
1014 self.bounds.unwrap()
1015 }
1016
1017 fn compute_bounds(&mut self) {
1019 if self.plots.is_empty() {
1020 self.bounds = Some(BoundingBox::default());
1021 return;
1022 }
1023
1024 let mut combined_bounds = None;
1025
1026 for plot in &mut self.plots {
1027 if !plot.is_visible() {
1028 continue;
1029 }
1030
1031 let plot_bounds = plot.bounds();
1032
1033 combined_bounds = match combined_bounds {
1034 None => Some(plot_bounds),
1035 Some(existing) => Some(existing.union(&plot_bounds)),
1036 };
1037 }
1038
1039 self.bounds = combined_bounds.or_else(|| Some(BoundingBox::default()));
1040 self.dirty = false;
1041 }
1042
1043 pub fn render_data(&mut self) -> Vec<RenderData> {
1045 self.render_data_with_viewport(None)
1046 }
1047
1048 pub fn render_data_with_viewport(
1054 &mut self,
1055 viewport_px: Option<(u32, u32)>,
1056 ) -> Vec<RenderData> {
1057 self.render_data_with_viewport_and_gpu(viewport_px, None)
1058 }
1059
1060 pub fn render_data_with_viewport_and_gpu(
1061 &mut self,
1062 viewport_px: Option<(u32, u32)>,
1063 gpu: Option<&GpuPackContext<'_>>,
1064 ) -> Vec<RenderData> {
1065 self.render_data_with_axes_with_viewport_and_gpu(viewport_px, None, gpu)
1066 .into_iter()
1067 .map(|(_, render_data)| render_data)
1068 .collect()
1069 }
1070
1071 pub fn render_data_with_axes_with_viewport_and_gpu(
1072 &mut self,
1073 viewport_px: Option<(u32, u32)>,
1074 axes_viewports_px: Option<&[(u32, u32)]>,
1075 gpu: Option<&GpuPackContext<'_>>,
1076 ) -> Vec<(usize, RenderData)> {
1077 fn push_with_optional_markers(
1078 out: &mut Vec<(usize, RenderData)>,
1079 axes_index: usize,
1080 render_data: RenderData,
1081 marker_data: Option<RenderData>,
1082 ) {
1083 out.push((axes_index, render_data));
1084 if let Some(marker_data) = marker_data {
1085 out.push((axes_index, marker_data));
1086 }
1087 }
1088
1089 let mut out = Vec::new();
1090 for (plot_idx, p) in self.plots.iter_mut().enumerate() {
1091 if !p.is_visible() {
1092 continue;
1093 }
1094 let axes_index = self.plot_axes_indices.get(plot_idx).copied().unwrap_or(0);
1095 if let PlotElement::Surface(s) = p {
1096 if let Some(meta) = self.axes_metadata.get(axes_index) {
1097 s.set_color_limits(meta.color_limits);
1098 *s = s.clone().with_colormap(meta.colormap);
1099 }
1100 }
1101
1102 match p {
1103 PlotElement::Line(plot) => {
1104 trace!(
1105 target: "runmat_plot",
1106 "figure: render_data line viewport_px={:?} gpu_ctx_present={} gpu_line_inputs_present={} gpu_vertices_present={}",
1107 viewport_px,
1108 gpu.is_some(),
1109 plot.has_gpu_line_inputs(),
1110 plot.has_gpu_vertices()
1111 );
1112 push_with_optional_markers(
1113 &mut out,
1114 axes_index,
1115 plot.render_data_with_viewport_gpu(
1116 axes_viewports_px
1117 .and_then(|viewports| viewports.get(axes_index).copied())
1118 .or(viewport_px),
1119 gpu,
1120 ),
1121 plot.marker_render_data(),
1122 );
1123 }
1124 PlotElement::ErrorBar(plot) => {
1125 push_with_optional_markers(
1126 &mut out,
1127 axes_index,
1128 plot.render_data_with_viewport(
1129 axes_viewports_px
1130 .and_then(|viewports| viewports.get(axes_index).copied())
1131 .or(viewport_px),
1132 ),
1133 plot.marker_render_data(),
1134 );
1135 }
1136 PlotElement::Stairs(plot) => {
1137 push_with_optional_markers(
1138 &mut out,
1139 axes_index,
1140 plot.render_data_with_viewport(
1141 axes_viewports_px
1142 .and_then(|viewports| viewports.get(axes_index).copied())
1143 .or(viewport_px),
1144 ),
1145 plot.marker_render_data(),
1146 );
1147 }
1148 PlotElement::Stem(plot) => {
1149 push_with_optional_markers(
1150 &mut out,
1151 axes_index,
1152 plot.render_data_with_viewport(
1153 axes_viewports_px
1154 .and_then(|viewports| viewports.get(axes_index).copied())
1155 .or(viewport_px),
1156 ),
1157 plot.marker_render_data(),
1158 );
1159 }
1160 PlotElement::Contour(plot) => out.push((
1161 axes_index,
1162 plot.render_data_with_viewport(
1163 axes_viewports_px
1164 .and_then(|viewports| viewports.get(axes_index).copied())
1165 .or(viewport_px),
1166 ),
1167 )),
1168 _ => out.push((axes_index, p.render_data())),
1169 }
1170 }
1171 out
1172 }
1173
1174 pub fn legend_entries(&self) -> Vec<LegendEntry> {
1176 let mut entries = Vec::new();
1177
1178 for plot in &self.plots {
1179 if let Some(label) = plot.label() {
1180 entries.push(LegendEntry {
1181 label,
1182 color: plot.color(),
1183 plot_type: plot.plot_type(),
1184 });
1185 }
1186 }
1187
1188 entries
1189 }
1190
1191 pub fn legend_entries_for_axes(&self, axes_index: usize) -> Vec<LegendEntry> {
1192 let mut entries = Vec::new();
1193 for (plot_idx, plot) in self.plots.iter().enumerate() {
1194 let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
1195 if plot_axes != axes_index {
1196 continue;
1197 }
1198 match plot {
1199 PlotElement::Pie(pie) => {
1200 for slice in pie.slice_meta() {
1201 entries.push(LegendEntry {
1202 label: slice.label,
1203 color: slice.color,
1204 plot_type: plot.plot_type(),
1205 });
1206 }
1207 }
1208 _ => {
1209 if let Some(label) = plot.label() {
1210 entries.push(LegendEntry {
1211 label,
1212 color: plot.color(),
1213 plot_type: plot.plot_type(),
1214 });
1215 }
1216 }
1217 }
1218 }
1219 entries
1220 }
1221
1222 pub fn pie_labels_for_axes(&self, axes_index: usize) -> Vec<PieLabelEntry> {
1223 let mut out = Vec::new();
1224 for (plot_idx, plot) in self.plots.iter().enumerate() {
1225 let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
1226 if plot_axes != axes_index {
1227 continue;
1228 }
1229 if let PlotElement::Pie(pie) = plot {
1230 for slice in pie.slice_meta() {
1231 out.push(PieLabelEntry {
1232 label: slice.label,
1233 position: glam::Vec2::new(
1234 slice.mid_angle.cos() * 1.15 + slice.offset.x,
1235 slice.mid_angle.sin() * 1.15 + slice.offset.y,
1236 ),
1237 });
1238 }
1239 }
1240 }
1241 out
1242 }
1243
1244 pub fn set_labels(&mut self, labels: &[String]) {
1246 self.set_labels_for_axes(self.active_axes_index, labels);
1247 }
1248
1249 pub fn set_labels_for_axes(&mut self, axes_index: usize, labels: &[String]) {
1250 let mut idx = 0usize;
1251 for (plot_idx, plot) in self.plots.iter_mut().enumerate() {
1252 let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
1253 if plot_axes != axes_index {
1254 continue;
1255 }
1256 if !plot.is_visible() {
1257 continue;
1258 }
1259 if idx >= labels.len() {
1260 break;
1261 }
1262 match plot {
1263 PlotElement::Pie(pie) => {
1264 let remaining = &labels[idx..];
1265 if remaining.len() >= pie.values.len() {
1266 pie.set_slice_labels(remaining[..pie.values.len()].to_vec());
1267 idx += pie.values.len();
1268 } else {
1269 pie.set_slice_labels(remaining.to_vec());
1270 idx = labels.len();
1271 }
1272 }
1273 _ => {
1274 plot.set_label(Some(labels[idx].clone()));
1275 idx += 1;
1276 }
1277 }
1278 }
1279 self.dirty = true;
1280 }
1281
1282 pub fn statistics(&self) -> FigureStatistics {
1284 let plot_counts = self.plots.iter().fold(HashMap::new(), |mut acc, plot| {
1285 let plot_type = plot.plot_type();
1286 *acc.entry(plot_type).or_insert(0) += 1;
1287 acc
1288 });
1289
1290 let total_memory: usize = self
1291 .plots
1292 .iter()
1293 .map(|plot| plot.estimated_memory_usage())
1294 .sum();
1295
1296 let visible_count = self.plots.iter().filter(|plot| plot.is_visible()).count();
1297
1298 FigureStatistics {
1299 total_plots: self.plots.len(),
1300 visible_plots: visible_count,
1301 plot_type_counts: plot_counts,
1302 total_memory_usage: total_memory,
1303 has_legend: self.legend_enabled && !self.legend_entries().is_empty(),
1304 }
1305 }
1306
1307 pub fn categorical_axis_labels(&self) -> Option<(bool, Vec<String>)> {
1311 for plot in &self.plots {
1312 if let PlotElement::Bar(b) = plot {
1313 if b.histogram_bin_edges().is_some() {
1314 continue;
1315 }
1316 let is_x = matches!(b.orientation, crate::plots::bar::Orientation::Vertical);
1317 return Some((is_x, b.labels.clone()));
1318 }
1319 }
1320 None
1321 }
1322
1323 pub fn categorical_axis_labels_for_axes(
1324 &self,
1325 axes_index: usize,
1326 ) -> Option<(bool, Vec<String>)> {
1327 for (plot_idx, plot) in self.plots.iter().enumerate() {
1328 let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
1329 if plot_axes != axes_index {
1330 continue;
1331 }
1332 if let PlotElement::Bar(b) = plot {
1333 if b.histogram_bin_edges().is_some() {
1334 continue;
1335 }
1336 let is_x = matches!(b.orientation, crate::plots::bar::Orientation::Vertical);
1337 return Some((is_x, b.labels.clone()));
1338 }
1339 }
1340 None
1341 }
1342
1343 pub fn histogram_axis_edges_for_axes(&self, axes_index: usize) -> Option<(bool, Vec<f64>)> {
1344 for (plot_idx, plot) in self.plots.iter().enumerate() {
1345 let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
1346 if plot_axes != axes_index {
1347 continue;
1348 }
1349 if let PlotElement::Bar(b) = plot {
1350 if let Some(edges) = b.histogram_bin_edges() {
1351 let is_x = matches!(b.orientation, crate::plots::bar::Orientation::Vertical);
1352 return Some((is_x, edges.to_vec()));
1353 }
1354 }
1355 }
1356 None
1357 }
1358}
1359
1360impl Default for Figure {
1361 fn default() -> Self {
1362 Self::new()
1363 }
1364}
1365
1366impl PlotElement {
1367 pub fn is_visible(&self) -> bool {
1369 match self {
1370 PlotElement::Line(plot) => plot.visible,
1371 PlotElement::Scatter(plot) => plot.visible,
1372 PlotElement::Bar(plot) => plot.visible,
1373 PlotElement::ErrorBar(plot) => plot.visible,
1374 PlotElement::Stairs(plot) => plot.visible,
1375 PlotElement::Stem(plot) => plot.visible,
1376 PlotElement::Area(plot) => plot.visible,
1377 PlotElement::Quiver(plot) => plot.visible,
1378 PlotElement::Pie(plot) => plot.visible,
1379 PlotElement::Surface(plot) => plot.visible,
1380 PlotElement::Line3(plot) => plot.visible,
1381 PlotElement::Scatter3(plot) => plot.visible,
1382 PlotElement::Contour(plot) => plot.visible,
1383 PlotElement::ContourFill(plot) => plot.visible,
1384 }
1385 }
1386
1387 pub fn label(&self) -> Option<String> {
1389 match self {
1390 PlotElement::Line(plot) => plot.label.clone(),
1391 PlotElement::Scatter(plot) => plot.label.clone(),
1392 PlotElement::Bar(plot) => plot.label.clone(),
1393 PlotElement::ErrorBar(plot) => plot.label.clone(),
1394 PlotElement::Stairs(plot) => plot.label.clone(),
1395 PlotElement::Stem(plot) => plot.label.clone(),
1396 PlotElement::Area(plot) => plot.label.clone(),
1397 PlotElement::Quiver(plot) => plot.label.clone(),
1398 PlotElement::Pie(plot) => plot.label.clone(),
1399 PlotElement::Surface(plot) => plot.label.clone(),
1400 PlotElement::Line3(plot) => plot.label.clone(),
1401 PlotElement::Scatter3(plot) => plot.label.clone(),
1402 PlotElement::Contour(plot) => plot.label.clone(),
1403 PlotElement::ContourFill(plot) => plot.label.clone(),
1404 }
1405 }
1406
1407 pub fn set_label(&mut self, label: Option<String>) {
1409 match self {
1410 PlotElement::Line(plot) => plot.label = label,
1411 PlotElement::Scatter(plot) => plot.label = label,
1412 PlotElement::Bar(plot) => plot.label = label,
1413 PlotElement::ErrorBar(plot) => plot.label = label,
1414 PlotElement::Stairs(plot) => plot.label = label,
1415 PlotElement::Stem(plot) => plot.label = label,
1416 PlotElement::Area(plot) => plot.label = label,
1417 PlotElement::Quiver(plot) => plot.label = label,
1418 PlotElement::Pie(plot) => plot.label = label,
1419 PlotElement::Surface(plot) => plot.label = label,
1420 PlotElement::Line3(plot) => plot.label = label,
1421 PlotElement::Scatter3(plot) => plot.label = label,
1422 PlotElement::Contour(plot) => plot.label = label,
1423 PlotElement::ContourFill(plot) => plot.label = label,
1424 }
1425 }
1426
1427 pub fn color(&self) -> Vec4 {
1429 match self {
1430 PlotElement::Line(plot) => plot.color,
1431 PlotElement::Scatter(plot) => plot.color,
1432 PlotElement::Bar(plot) => plot.color,
1433 PlotElement::ErrorBar(plot) => plot.color,
1434 PlotElement::Stairs(plot) => plot.color,
1435 PlotElement::Stem(plot) => plot.color,
1436 PlotElement::Area(plot) => plot.color,
1437 PlotElement::Quiver(plot) => plot.color,
1438 PlotElement::Pie(_plot) => Vec4::new(1.0, 1.0, 1.0, 1.0),
1439 PlotElement::Surface(_plot) => Vec4::new(1.0, 1.0, 1.0, 1.0),
1440 PlotElement::Line3(plot) => plot.color,
1441 PlotElement::Scatter3(plot) => plot.colors.first().copied().unwrap_or(Vec4::ONE),
1442 PlotElement::Contour(_plot) => Vec4::new(1.0, 1.0, 1.0, 1.0),
1443 PlotElement::ContourFill(_plot) => Vec4::new(0.9, 0.9, 0.9, 1.0),
1444 }
1445 }
1446
1447 pub fn plot_type(&self) -> PlotType {
1449 match self {
1450 PlotElement::Line(_) => PlotType::Line,
1451 PlotElement::Scatter(_) => PlotType::Scatter,
1452 PlotElement::Bar(_) => PlotType::Bar,
1453 PlotElement::ErrorBar(_) => PlotType::ErrorBar,
1454 PlotElement::Stairs(_) => PlotType::Stairs,
1455 PlotElement::Stem(_) => PlotType::Stem,
1456 PlotElement::Area(_) => PlotType::Area,
1457 PlotElement::Quiver(_) => PlotType::Quiver,
1458 PlotElement::Pie(_) => PlotType::Pie,
1459 PlotElement::Surface(_) => PlotType::Surface,
1460 PlotElement::Line3(_) => PlotType::Line3,
1461 PlotElement::Scatter3(_) => PlotType::Scatter3,
1462 PlotElement::Contour(_) => PlotType::Contour,
1463 PlotElement::ContourFill(_) => PlotType::ContourFill,
1464 }
1465 }
1466
1467 pub fn bounds(&mut self) -> BoundingBox {
1469 match self {
1470 PlotElement::Line(plot) => plot.bounds(),
1471 PlotElement::Scatter(plot) => plot.bounds(),
1472 PlotElement::Bar(plot) => plot.bounds(),
1473 PlotElement::ErrorBar(plot) => plot.bounds(),
1474 PlotElement::Stairs(plot) => plot.bounds(),
1475 PlotElement::Stem(plot) => plot.bounds(),
1476 PlotElement::Area(plot) => plot.bounds(),
1477 PlotElement::Quiver(plot) => plot.bounds(),
1478 PlotElement::Pie(plot) => plot.bounds(),
1479 PlotElement::Surface(plot) => plot.bounds(),
1480 PlotElement::Line3(plot) => plot.bounds(),
1481 PlotElement::Scatter3(plot) => plot.bounds(),
1482 PlotElement::Contour(plot) => plot.bounds(),
1483 PlotElement::ContourFill(plot) => plot.bounds(),
1484 }
1485 }
1486
1487 pub fn render_data(&mut self) -> RenderData {
1489 match self {
1490 PlotElement::Line(plot) => plot.render_data(),
1491 PlotElement::Scatter(plot) => plot.render_data(),
1492 PlotElement::Bar(plot) => plot.render_data(),
1493 PlotElement::ErrorBar(plot) => plot.render_data(),
1494 PlotElement::Stairs(plot) => plot.render_data(),
1495 PlotElement::Stem(plot) => plot.render_data(),
1496 PlotElement::Area(plot) => plot.render_data(),
1497 PlotElement::Quiver(plot) => plot.render_data(),
1498 PlotElement::Pie(plot) => plot.render_data(),
1499 PlotElement::Surface(plot) => plot.render_data(),
1500 PlotElement::Line3(plot) => plot.render_data(),
1501 PlotElement::Scatter3(plot) => plot.render_data(),
1502 PlotElement::Contour(plot) => plot.render_data(),
1503 PlotElement::ContourFill(plot) => plot.render_data(),
1504 }
1505 }
1506
1507 pub fn estimated_memory_usage(&self) -> usize {
1509 match self {
1510 PlotElement::Line(plot) => plot.estimated_memory_usage(),
1511 PlotElement::Scatter(plot) => plot.estimated_memory_usage(),
1512 PlotElement::Bar(plot) => plot.estimated_memory_usage(),
1513 PlotElement::ErrorBar(plot) => plot.estimated_memory_usage(),
1514 PlotElement::Stairs(plot) => plot.estimated_memory_usage(),
1515 PlotElement::Stem(plot) => plot.estimated_memory_usage(),
1516 PlotElement::Area(plot) => plot.estimated_memory_usage(),
1517 PlotElement::Quiver(plot) => plot.estimated_memory_usage(),
1518 PlotElement::Pie(plot) => plot.estimated_memory_usage(),
1519 PlotElement::Surface(_plot) => 0,
1520 PlotElement::Line3(plot) => plot.estimated_memory_usage(),
1521 PlotElement::Scatter3(plot) => plot.estimated_memory_usage(),
1522 PlotElement::Contour(plot) => plot.estimated_memory_usage(),
1523 PlotElement::ContourFill(plot) => plot.estimated_memory_usage(),
1524 }
1525 }
1526}
1527
1528#[derive(Debug)]
1530pub struct FigureStatistics {
1531 pub total_plots: usize,
1532 pub visible_plots: usize,
1533 pub plot_type_counts: HashMap<PlotType, usize>,
1534 pub total_memory_usage: usize,
1535 pub has_legend: bool,
1536}
1537
1538pub mod matlab_compat {
1540 use super::*;
1541 use crate::plots::{LinePlot, ScatterPlot};
1542
1543 pub fn figure() -> Figure {
1545 Figure::new()
1546 }
1547
1548 pub fn figure_with_title<S: Into<String>>(title: S) -> Figure {
1550 Figure::new().with_title(title)
1551 }
1552
1553 pub fn plot_multiple_lines(
1555 figure: &mut Figure,
1556 data_sets: Vec<(Vec<f64>, Vec<f64>, Option<String>)>,
1557 ) -> Result<Vec<usize>, String> {
1558 let mut indices = Vec::new();
1559
1560 for (i, (x, y, label)) in data_sets.into_iter().enumerate() {
1561 let mut line = LinePlot::new(x, y)?;
1562
1563 let colors = [
1565 Vec4::new(0.0, 0.4470, 0.7410, 1.0), Vec4::new(0.8500, 0.3250, 0.0980, 1.0), Vec4::new(0.9290, 0.6940, 0.1250, 1.0), Vec4::new(0.4940, 0.1840, 0.5560, 1.0), Vec4::new(0.4660, 0.6740, 0.1880, 1.0), Vec4::new(std::f64::consts::LOG10_2 as f32, 0.7450, 0.9330, 1.0), Vec4::new(0.6350, 0.0780, 0.1840, 1.0), ];
1573 let color = colors[i % colors.len()];
1574 line.set_color(color);
1575
1576 if let Some(label) = label {
1577 line = line.with_label(label);
1578 }
1579
1580 indices.push(figure.add_line_plot(line));
1581 }
1582
1583 Ok(indices)
1584 }
1585
1586 pub fn scatter_multiple(
1588 figure: &mut Figure,
1589 data_sets: Vec<(Vec<f64>, Vec<f64>, Option<String>)>,
1590 ) -> Result<Vec<usize>, String> {
1591 let mut indices = Vec::new();
1592
1593 for (i, (x, y, label)) in data_sets.into_iter().enumerate() {
1594 let mut scatter = ScatterPlot::new(x, y)?;
1595
1596 let colors = [
1598 Vec4::new(1.0, 0.0, 0.0, 1.0), Vec4::new(0.0, 1.0, 0.0, 1.0), Vec4::new(0.0, 0.0, 1.0, 1.0), Vec4::new(1.0, 1.0, 0.0, 1.0), Vec4::new(1.0, 0.0, 1.0, 1.0), Vec4::new(0.0, 1.0, 1.0, 1.0), Vec4::new(0.5, 0.5, 0.5, 1.0), ];
1606 let color = colors[i % colors.len()];
1607 scatter.set_color(color);
1608
1609 if let Some(label) = label {
1610 scatter = scatter.with_label(label);
1611 }
1612
1613 indices.push(figure.add_scatter_plot(scatter));
1614 }
1615
1616 Ok(indices)
1617 }
1618}
1619
1620#[cfg(test)]
1621mod tests {
1622 use super::*;
1623 use crate::plots::line::LineStyle;
1624
1625 #[test]
1626 fn test_figure_creation() {
1627 let figure = Figure::new();
1628
1629 assert_eq!(figure.len(), 0);
1630 assert!(figure.is_empty());
1631 assert!(figure.legend_enabled);
1632 assert!(figure.grid_enabled);
1633 }
1634
1635 #[test]
1636 fn test_figure_styling() {
1637 let figure = Figure::new()
1638 .with_title("Test Figure")
1639 .with_sg_title("Overview")
1640 .with_labels("X Axis", "Y Axis")
1641 .with_legend(false)
1642 .with_grid(false);
1643
1644 assert_eq!(figure.title, Some("Test Figure".to_string()));
1645 assert_eq!(figure.sg_title, Some("Overview".to_string()));
1646 assert_eq!(figure.x_label, Some("X Axis".to_string()));
1647 assert_eq!(figure.y_label, Some("Y Axis".to_string()));
1648 assert!(!figure.legend_enabled);
1649 assert!(!figure.grid_enabled);
1650 }
1651
1652 #[test]
1653 fn test_has_any_titles_tracks_super_and_axes_titles() {
1654 let mut figure = Figure::new();
1655 assert!(!figure.has_any_titles());
1656
1657 figure.set_sg_title("Summary");
1658 assert!(figure.has_any_titles());
1659
1660 figure.clear_sg_title();
1661 assert!(!figure.has_any_titles());
1662
1663 figure.set_axes_title(0, "Panel");
1664 assert!(figure.has_any_titles());
1665 }
1666
1667 #[test]
1668 fn test_multiple_line_plots() {
1669 let mut figure = Figure::new();
1670
1671 let line1 = LinePlot::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 4.0])
1673 .unwrap()
1674 .with_label("Quadratic");
1675 let index1 = figure.add_line_plot(line1);
1676
1677 let line2 = LinePlot::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 2.0])
1679 .unwrap()
1680 .with_style(Vec4::new(1.0, 0.0, 0.0, 1.0), 2.0, LineStyle::Dashed)
1681 .with_label("Linear");
1682 let index2 = figure.add_line_plot(line2);
1683
1684 assert_eq!(figure.len(), 2);
1685 assert_eq!(index1, 0);
1686 assert_eq!(index2, 1);
1687
1688 let legend = figure.legend_entries();
1690 assert_eq!(legend.len(), 2);
1691 assert_eq!(legend[0].label, "Quadratic");
1692 assert_eq!(legend[1].label, "Linear");
1693 }
1694
1695 #[test]
1696 fn test_mixed_plot_types() {
1697 let mut figure = Figure::new();
1698
1699 let line = LinePlot::new(vec![0.0, 1.0, 2.0], vec![1.0, 2.0, 3.0])
1701 .unwrap()
1702 .with_label("Line");
1703 figure.add_line_plot(line);
1704
1705 let scatter = ScatterPlot::new(vec![0.5, 1.5, 2.5], vec![1.5, 2.5, 3.5])
1706 .unwrap()
1707 .with_label("Scatter");
1708 figure.add_scatter_plot(scatter);
1709
1710 let bar = BarChart::new(vec!["A".to_string(), "B".to_string()], vec![2.0, 4.0])
1711 .unwrap()
1712 .with_label("Bar");
1713 figure.add_bar_chart(bar);
1714
1715 assert_eq!(figure.len(), 3);
1716
1717 let render_data = figure.render_data();
1719 assert_eq!(render_data.len(), 3);
1720
1721 let stats = figure.statistics();
1723 assert_eq!(stats.total_plots, 3);
1724 assert_eq!(stats.visible_plots, 3);
1725 assert!(stats.has_legend);
1726 }
1727
1728 #[test]
1729 fn test_plot_visibility() {
1730 let mut figure = Figure::new();
1731
1732 let mut line = LinePlot::new(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
1733 line.set_visible(false); figure.add_line_plot(line);
1735
1736 let scatter = ScatterPlot::new(vec![0.0, 1.0], vec![1.0, 2.0]).unwrap();
1737 figure.add_scatter_plot(scatter);
1738
1739 let render_data = figure.render_data();
1741 assert_eq!(render_data.len(), 1);
1742
1743 let stats = figure.statistics();
1744 assert_eq!(stats.total_plots, 2);
1745 assert_eq!(stats.visible_plots, 1);
1746 }
1747
1748 #[test]
1749 fn test_bounds_computation() {
1750 let mut figure = Figure::new();
1751
1752 let line = LinePlot::new(vec![-1.0, 0.0, 1.0], vec![-2.0, 0.0, 2.0]).unwrap();
1754 figure.add_line_plot(line);
1755
1756 let scatter = ScatterPlot::new(vec![2.0, 3.0, 4.0], vec![1.0, 3.0, 5.0]).unwrap();
1757 figure.add_scatter_plot(scatter);
1758
1759 let bounds = figure.bounds();
1760
1761 assert!(bounds.min.x <= -1.0);
1763 assert!(bounds.max.x >= 4.0);
1764 assert!(bounds.min.y <= -2.0);
1765 assert!(bounds.max.y >= 5.0);
1766 }
1767
1768 #[test]
1769 fn test_matlab_compat_multiple_lines() {
1770 use super::matlab_compat::*;
1771
1772 let mut figure = figure_with_title("Multiple Lines Test");
1773
1774 let data_sets = vec![
1775 (
1776 vec![0.0, 1.0, 2.0],
1777 vec![0.0, 1.0, 4.0],
1778 Some("Quadratic".to_string()),
1779 ),
1780 (
1781 vec![0.0, 1.0, 2.0],
1782 vec![0.0, 1.0, 2.0],
1783 Some("Linear".to_string()),
1784 ),
1785 (
1786 vec![0.0, 1.0, 2.0],
1787 vec![1.0, 1.0, 1.0],
1788 Some("Constant".to_string()),
1789 ),
1790 ];
1791
1792 let indices = plot_multiple_lines(&mut figure, data_sets).unwrap();
1793
1794 assert_eq!(indices.len(), 3);
1795 assert_eq!(figure.len(), 3);
1796
1797 let legend = figure.legend_entries();
1799 assert_eq!(legend.len(), 3);
1800 assert_ne!(legend[0].color, legend[1].color);
1801 assert_ne!(legend[1].color, legend[2].color);
1802 }
1803
1804 #[test]
1805 fn axes_metadata_and_labels_are_isolated_per_subplot() {
1806 let mut figure = Figure::new();
1807 figure.set_subplot_grid(1, 2);
1808 figure.set_axes_title(0, "Left Title");
1809 figure.set_axes_xlabel(0, "Left X");
1810 figure.set_axes_ylabel(0, "Left Y");
1811 figure.set_axes_title(1, "Right Title");
1812 figure.set_axes_legend_enabled(0, false);
1813 figure.set_axes_legend_style(
1814 1,
1815 LegendStyle {
1816 location: Some("southwest".into()),
1817 ..Default::default()
1818 },
1819 );
1820
1821 assert_eq!(
1822 figure.axes_metadata(0).and_then(|m| m.title.as_deref()),
1823 Some("Left Title")
1824 );
1825 assert_eq!(
1826 figure.axes_metadata(1).and_then(|m| m.title.as_deref()),
1827 Some("Right Title")
1828 );
1829 assert_eq!(
1830 figure.axes_metadata(0).and_then(|m| m.x_label.as_deref()),
1831 Some("Left X")
1832 );
1833 assert_eq!(
1834 figure.axes_metadata(0).and_then(|m| m.y_label.as_deref()),
1835 Some("Left Y")
1836 );
1837 assert!(!figure.axes_metadata(0).unwrap().legend_enabled);
1838 assert_eq!(
1839 figure
1840 .axes_metadata(1)
1841 .unwrap()
1842 .legend_style
1843 .location
1844 .as_deref(),
1845 Some("southwest")
1846 );
1847 }
1848
1849 #[test]
1850 fn set_labels_for_axes_only_updates_target_subplot() {
1851 let mut figure = Figure::new();
1852 figure.set_subplot_grid(1, 2);
1853 figure.add_line_plot_on_axes(
1854 LinePlot::new(vec![0.0, 1.0], vec![1.0, 2.0])
1855 .unwrap()
1856 .with_label("L0"),
1857 0,
1858 );
1859 figure.add_line_plot_on_axes(
1860 LinePlot::new(vec![0.0, 1.0], vec![2.0, 3.0])
1861 .unwrap()
1862 .with_label("R0"),
1863 1,
1864 );
1865 figure.set_labels_for_axes(1, &["Right Only".into()]);
1866
1867 let left_entries = figure.legend_entries_for_axes(0);
1868 let right_entries = figure.legend_entries_for_axes(1);
1869 assert_eq!(left_entries[0].label, "L0");
1870 assert_eq!(right_entries[0].label, "Right Only");
1871 }
1872
1873 #[test]
1874 fn axes_log_modes_are_isolated_per_subplot() {
1875 let mut figure = Figure::new();
1876 figure.set_subplot_grid(1, 2);
1877 figure.set_axes_log_modes(1, true, false);
1878
1879 assert!(!figure.axes_metadata(0).unwrap().x_log);
1880 assert!(!figure.axes_metadata(0).unwrap().y_log);
1881 assert!(figure.axes_metadata(1).unwrap().x_log);
1882 assert!(!figure.axes_metadata(1).unwrap().y_log);
1883
1884 figure.set_active_axes_index(1);
1885 assert!(figure.x_log);
1886 assert!(!figure.y_log);
1887 }
1888
1889 #[test]
1890 fn z_label_and_view_state_are_isolated_per_subplot() {
1891 let mut figure = Figure::new();
1892 figure.set_subplot_grid(1, 2);
1893 figure.set_axes_zlabel(1, "Height");
1894 figure.set_axes_view(1, 45.0, 20.0);
1895
1896 assert_eq!(figure.axes_metadata(0).unwrap().z_label, None);
1897 assert_eq!(
1898 figure.axes_metadata(1).unwrap().z_label.as_deref(),
1899 Some("Height")
1900 );
1901 assert_eq!(
1902 figure.axes_metadata(1).unwrap().view_azimuth_deg,
1903 Some(45.0)
1904 );
1905 assert_eq!(
1906 figure.axes_metadata(1).unwrap().view_elevation_deg,
1907 Some(20.0)
1908 );
1909 }
1910
1911 #[test]
1912 fn pie_legend_entries_are_slice_based() {
1913 let mut figure = Figure::new();
1914 let pie = PieChart::new(vec![1.0, 2.0], None)
1915 .unwrap()
1916 .with_slice_labels(vec!["A".into(), "B".into()]);
1917 figure.add_pie_chart(pie);
1918 let entries = figure.legend_entries_for_axes(0);
1919 assert_eq!(entries.len(), 2);
1920 assert_eq!(entries[0].label, "A");
1921 assert_eq!(entries[1].label, "B");
1922 }
1923
1924 #[test]
1925 fn histogram_bars_do_not_use_categorical_axis_labels() {
1926 let mut figure = Figure::new();
1927 let mut bar = BarChart::new(vec!["a".into(), "b".into()], vec![2.0, 3.0]).unwrap();
1928 bar.set_histogram_bin_edges(vec![0.0, 0.5, 1.0]);
1929 figure.add_bar_chart(bar);
1930
1931 assert!(figure.categorical_axis_labels().is_none());
1932 assert_eq!(
1933 figure.histogram_axis_edges_for_axes(0),
1934 Some((true, vec![0.0, 0.5, 1.0]))
1935 );
1936 }
1937
1938 #[test]
1939 fn plain_bar_charts_keep_categorical_axis_labels() {
1940 let mut figure = Figure::new();
1941 let bar = BarChart::new(vec!["A".into(), "B".into()], vec![1.0, 2.0]).unwrap();
1942 figure.add_bar_chart(bar);
1943
1944 assert_eq!(
1945 figure.categorical_axis_labels(),
1946 Some((true, vec!["A".to_string(), "B".to_string()]))
1947 );
1948 }
1949
1950 #[test]
1951 fn line3_contributes_to_3d_bounds_and_metadata() {
1952 let mut figure = Figure::new();
1953 let line3 = Line3Plot::new(vec![0.0, 1.0], vec![1.0, 2.0], vec![2.0, 4.0])
1954 .unwrap()
1955 .with_label("Trajectory");
1956 figure.add_line3_plot(line3);
1957 let bounds = figure.bounds();
1958 assert_eq!(bounds.min.z, 2.0);
1959 assert_eq!(bounds.max.z, 4.0);
1960 let entries = figure.legend_entries_for_axes(0);
1961 assert_eq!(entries[0].plot_type, PlotType::Line3);
1962 }
1963
1964 #[test]
1965 fn stem_render_data_includes_marker_pass() {
1966 let mut figure = Figure::new();
1967 figure.add_stem_plot(StemPlot::new(vec![0.0, 1.0], vec![1.0, 2.0]).unwrap());
1968
1969 let render_data = figure.render_data();
1970 assert_eq!(render_data.len(), 2);
1971 assert_eq!(
1972 render_data[0].pipeline_type,
1973 crate::core::PipelineType::Lines
1974 );
1975 assert_eq!(
1976 render_data[1].pipeline_type,
1977 crate::core::PipelineType::Points
1978 );
1979 }
1980
1981 #[test]
1982 fn errorbar_render_data_includes_marker_pass() {
1983 let mut figure = Figure::new();
1984 figure.add_errorbar(
1985 ErrorBar::new_vertical(
1986 vec![0.0, 1.0],
1987 vec![1.0, 2.0],
1988 vec![0.1, 0.2],
1989 vec![0.1, 0.2],
1990 )
1991 .unwrap(),
1992 );
1993
1994 let render_data = figure.render_data();
1995 assert_eq!(render_data.len(), 2);
1996 assert_eq!(
1997 render_data[0].pipeline_type,
1998 crate::core::PipelineType::Lines
1999 );
2000 assert_eq!(
2001 render_data[1].pipeline_type,
2002 crate::core::PipelineType::Points
2003 );
2004 }
2005
2006 #[test]
2007 fn subplot_sensitive_axes_state_is_isolated_per_subplot() {
2008 let mut figure = Figure::new();
2009 figure.set_subplot_grid(1, 2);
2010 figure.set_axes_limits(1, Some((1.0, 2.0)), Some((3.0, 4.0)));
2011 figure.set_axes_z_limits(1, Some((5.0, 6.0)));
2012 figure.set_axes_grid_enabled(1, false);
2013 figure.set_axes_box_enabled(1, false);
2014 figure.set_axes_axis_equal(1, true);
2015 figure.set_axes_colorbar_enabled(1, true);
2016 figure.set_axes_colormap(1, ColorMap::Hot);
2017 figure.set_axes_color_limits(1, Some((0.0, 10.0)));
2018
2019 let left = figure.axes_metadata(0).unwrap();
2020 let right = figure.axes_metadata(1).unwrap();
2021 assert_eq!(left.x_limits, None);
2022 assert_eq!(right.x_limits, Some((1.0, 2.0)));
2023 assert!(!right.grid_enabled);
2024 assert!(!right.box_enabled);
2025 assert!(right.axis_equal);
2026 assert!(right.colorbar_enabled);
2027 assert_eq!(format!("{:?}", right.colormap), "Hot");
2028 assert_eq!(right.color_limits, Some((0.0, 10.0)));
2029 }
2030}