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