1use crate::core::{BoundingBox, GpuPackContext, RenderData};
7use crate::plots::surface::ColorMap;
8use crate::plots::{
9 AreaPlot, BarChart, ContourFillPlot, ContourPlot, ErrorBar, Line3Plot, LinePlot, PatchPlot,
10 PieChart, QuiverPlot, ReferenceLine, ReferenceLineOrientation, Scatter3Plot, ScatterPlot,
11 StairsPlot, StemPlot, SurfacePlot,
12};
13use glam::Vec4;
14use log::trace;
15use std::collections::HashMap;
16
17type ViewBounds2D = (f64, f64, f64, f64);
18type PerAxesViewBoundsRef<'a> = &'a [Option<ViewBounds2D>];
19
20#[derive(Debug, Clone)]
22pub struct Figure {
23 plots: Vec<PlotElement>,
25
26 pub name: Option<String>,
28 pub number_title: bool,
29 pub visible: bool,
30 pub title: Option<String>,
31 pub sg_title: Option<String>,
32 pub x_label: Option<String>,
33 pub y_label: Option<String>,
34 pub z_label: Option<String>,
35 pub legend_enabled: bool,
36 pub grid_enabled: bool,
37 pub minor_grid_enabled: bool,
38 pub box_enabled: bool,
39 pub background_color: Vec4,
40
41 pub x_limits: Option<(f64, f64)>,
43 pub y_limits: Option<(f64, f64)>,
44 pub z_limits: Option<(f64, f64)>,
45
46 pub x_log: bool,
48 pub y_log: bool,
49
50 pub axis_equal: bool,
52
53 pub colormap: ColorMap,
55 pub colorbar_enabled: bool,
56
57 pub color_limits: Option<(f64, f64)>,
59
60 bounds: Option<BoundingBox>,
62 dirty: bool,
63
64 pub axes_rows: usize,
66 pub axes_cols: usize,
67 plot_axes_indices: Vec<usize>,
69
70 pub active_axes_index: usize,
72
73 pub axes_metadata: Vec<AxesMetadata>,
75 pub sg_title_style: TextStyle,
76}
77
78#[derive(Debug, Clone)]
79pub struct TextStyle {
80 pub color: Option<Vec4>,
81 pub font_size: Option<f32>,
82 pub font_weight: Option<String>,
83 pub font_angle: Option<String>,
84 pub interpreter: Option<String>,
85 pub visible: bool,
86}
87
88impl Default for TextStyle {
89 fn default() -> Self {
90 Self {
91 color: None,
92 font_size: None,
93 font_weight: None,
94 font_angle: None,
95 interpreter: None,
96 visible: true,
97 }
98 }
99}
100
101#[derive(Debug, Clone)]
102pub struct LegendStyle {
103 pub location: Option<String>,
104 pub visible: bool,
105 pub font_size: Option<f32>,
106 pub font_weight: Option<String>,
107 pub font_angle: Option<String>,
108 pub interpreter: Option<String>,
109 pub box_visible: Option<bool>,
110 pub orientation: Option<String>,
111 pub text_color: Option<Vec4>,
112}
113
114impl Default for LegendStyle {
115 fn default() -> Self {
116 Self {
117 location: None,
118 visible: true,
119 font_size: None,
120 font_weight: None,
121 font_angle: None,
122 interpreter: None,
123 box_visible: None,
124 orientation: None,
125 text_color: None,
126 }
127 }
128}
129
130#[derive(Debug, Clone, Default)]
131pub struct AxesMetadata {
132 pub title: Option<String>,
133 pub x_label: Option<String>,
134 pub y_label: Option<String>,
135 pub z_label: Option<String>,
136 pub x_tick_labels: Option<Vec<String>>,
137 pub y_tick_labels: Option<Vec<String>>,
138 pub x_limits: Option<(f64, f64)>,
139 pub y_limits: Option<(f64, f64)>,
140 pub z_limits: Option<(f64, f64)>,
141 pub x_log: bool,
142 pub y_log: bool,
143 pub view_azimuth_deg: Option<f32>,
144 pub view_elevation_deg: Option<f32>,
145 pub view_revision: u64,
146 pub grid_enabled: bool,
147 pub minor_grid_enabled: bool,
148 pub minor_grid_explicit: bool,
149 pub box_enabled: bool,
150 pub axis_equal: bool,
151 pub legend_enabled: bool,
152 pub colorbar_enabled: bool,
153 pub colormap: ColorMap,
154 pub color_limits: Option<(f64, f64)>,
155 pub axes_style: TextStyle,
156 pub title_style: TextStyle,
157 pub x_label_style: TextStyle,
158 pub y_label_style: TextStyle,
159 pub z_label_style: TextStyle,
160 pub legend_style: LegendStyle,
161 pub world_text_annotations: Vec<TextAnnotation>,
162}
163
164#[derive(Debug, Clone)]
165pub struct TextAnnotation {
166 pub position: glam::Vec3,
167 pub text: String,
168 pub style: TextStyle,
169}
170
171#[derive(Debug, Clone)]
173pub enum PlotElement {
174 Line(LinePlot),
175 Scatter(ScatterPlot),
176 Bar(BarChart),
177 ErrorBar(ErrorBar),
178 Stairs(StairsPlot),
179 Stem(StemPlot),
180 Area(AreaPlot),
181 Quiver(QuiverPlot),
182 Pie(PieChart),
183 Surface(SurfacePlot),
184 Patch(PatchPlot),
185 Line3(Line3Plot),
186 Scatter3(Scatter3Plot),
187 Contour(ContourPlot),
188 ContourFill(ContourFillPlot),
189 ReferenceLine(ReferenceLine),
190}
191
192#[derive(Debug, Clone)]
194pub struct LegendEntry {
195 pub label: String,
196 pub color: Vec4,
197 pub plot_type: PlotType,
198}
199
200#[derive(Debug, Clone)]
201pub struct PieLabelEntry {
202 pub label: String,
203 pub position: glam::Vec2,
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
208pub enum PlotType {
209 Line,
210 Scatter,
211 Bar,
212 ErrorBar,
213 Stairs,
214 Stem,
215 Area,
216 Quiver,
217 Pie,
218 Surface,
219 Patch,
220 Line3,
221 Scatter3,
222 Contour,
223 ContourFill,
224 ReferenceLine,
225}
226
227impl Figure {
228 pub fn new() -> Self {
230 Self {
231 plots: Vec::new(),
232 name: None,
233 number_title: true,
234 visible: true,
235 title: None,
236 sg_title: None,
237 x_label: None,
238 y_label: None,
239 z_label: None,
240 legend_enabled: true,
241 grid_enabled: true,
242 minor_grid_enabled: false,
243 box_enabled: true,
244 background_color: Vec4::new(1.0, 1.0, 1.0, 1.0), x_limits: None,
246 y_limits: None,
247 z_limits: None,
248 x_log: false,
249 y_log: false,
250 axis_equal: false,
251 colormap: ColorMap::Parula,
252 colorbar_enabled: false,
253 color_limits: None,
254 bounds: None,
255 dirty: true,
256 axes_rows: 1,
257 axes_cols: 1,
258 plot_axes_indices: Vec::new(),
259 active_axes_index: 0,
260 axes_metadata: vec![AxesMetadata {
261 x_limits: None,
262 y_limits: None,
263 z_limits: None,
264 grid_enabled: true,
265 minor_grid_enabled: false,
266 box_enabled: true,
267 axis_equal: false,
268 legend_enabled: true,
269 colorbar_enabled: false,
270 colormap: ColorMap::Parula,
271 color_limits: None,
272 ..Default::default()
273 }],
274 sg_title_style: TextStyle::default(),
275 }
276 }
277
278 fn ensure_axes_metadata_capacity(&mut self, min_len: usize) {
279 while self.axes_metadata.len() < min_len.max(1) {
280 self.axes_metadata.push(AxesMetadata {
281 x_limits: None,
282 y_limits: None,
283 z_limits: None,
284 grid_enabled: true,
285 minor_grid_enabled: false,
286 box_enabled: true,
287 axis_equal: false,
288 legend_enabled: true,
289 colorbar_enabled: false,
290 colormap: ColorMap::Parula,
291 color_limits: None,
292 ..Default::default()
293 });
294 }
295 }
296
297 fn sync_legacy_fields_from_active_axes(&mut self) {
298 self.ensure_axes_metadata_capacity(self.active_axes_index + 1);
299 if let Some(meta) = self.axes_metadata.get(self.active_axes_index).cloned() {
300 self.title = meta.title;
301 self.x_label = meta.x_label;
302 self.y_label = meta.y_label;
303 self.z_label = meta.z_label;
304 self.x_limits = meta.x_limits;
305 self.y_limits = meta.y_limits;
306 self.z_limits = meta.z_limits;
307 self.x_log = meta.x_log;
308 self.y_log = meta.y_log;
309 self.grid_enabled = meta.grid_enabled;
310 self.box_enabled = meta.box_enabled;
311 self.axis_equal = meta.axis_equal;
312 self.legend_enabled = meta.legend_enabled;
313 self.colorbar_enabled = meta.colorbar_enabled;
314 self.colormap = meta.colormap;
315 self.color_limits = meta.color_limits;
316 }
317 }
318
319 pub fn set_active_axes_index(&mut self, axes_index: usize) {
320 self.ensure_axes_metadata_capacity(axes_index + 1);
321 self.active_axes_index = axes_index;
322 self.sync_legacy_fields_from_active_axes();
323 self.dirty = true;
324 }
325
326 pub fn axes_metadata(&self, axes_index: usize) -> Option<&AxesMetadata> {
327 self.axes_metadata.get(axes_index)
328 }
329
330 pub fn active_axes_metadata(&self) -> Option<&AxesMetadata> {
331 self.axes_metadata(self.active_axes_index)
332 }
333
334 pub fn with_sg_title<S: Into<String>>(mut self, title: S) -> Self {
335 self.set_sg_title(title);
336 self
337 }
338
339 pub fn set_sg_title<S: Into<String>>(&mut self, title: S) {
340 self.sg_title = Some(title.into());
341 self.dirty = true;
342 }
343
344 pub fn clear_sg_title(&mut self) {
345 self.sg_title = None;
346 self.dirty = true;
347 }
348
349 pub fn set_sg_title_style(&mut self, style: TextStyle) {
350 self.sg_title_style = style;
351 self.dirty = true;
352 }
353
354 pub fn set_name<S: Into<String>>(&mut self, name: S) {
355 self.name = Some(name.into());
356 self.dirty = true;
357 }
358
359 pub fn set_number_title(&mut self, enabled: bool) {
360 self.number_title = enabled;
361 self.dirty = true;
362 }
363
364 pub fn set_visible(&mut self, visible: bool) {
365 self.visible = visible;
366 self.dirty = true;
367 }
368
369 pub fn window_title(&self, handle: Option<u32>) -> String {
370 let name = self.name.as_deref().map(str::trim).unwrap_or_default();
371 let numbered = if self.number_title {
372 handle.filter(|h| *h > 0).map(|h| format!("Figure {h}"))
373 } else {
374 None
375 };
376 match (numbered, name.is_empty()) {
377 (Some(numbered), false) => format!("{numbered}: {name}"),
378 (Some(numbered), true) => numbered,
379 (None, false) => name.to_string(),
380 (None, true) => "RunMat Plot".to_string(),
381 }
382 }
383
384 pub fn has_any_titles(&self) -> bool {
385 let non_empty = |s: Option<&str>| s.map(str::trim).is_some_and(|t| !t.is_empty());
386 non_empty(self.sg_title.as_deref())
387 || non_empty(self.title.as_deref())
388 || self
389 .axes_metadata
390 .iter()
391 .any(|meta| non_empty(meta.title.as_deref()))
392 }
393
394 pub fn with_title<S: Into<String>>(mut self, title: S) -> Self {
396 self.set_title(title);
397 self
398 }
399
400 pub fn set_title<S: Into<String>>(&mut self, title: S) {
402 self.set_axes_title(self.active_axes_index, title);
403 }
404
405 pub fn with_labels<S: Into<String>>(mut self, x_label: S, y_label: S) -> Self {
407 self.set_axis_labels(x_label, y_label);
408 self
409 }
410
411 pub fn set_axis_labels<S: Into<String>>(&mut self, x_label: S, y_label: S) {
413 self.set_axes_labels(self.active_axes_index, x_label, y_label);
414 self.dirty = true;
415 }
416
417 pub fn set_axes_title<S: Into<String>>(&mut self, axes_index: usize, title: S) {
418 self.ensure_axes_metadata_capacity(axes_index + 1);
419 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
420 meta.title = Some(title.into());
421 }
422 if axes_index == self.active_axes_index {
423 self.sync_legacy_fields_from_active_axes();
424 }
425 self.dirty = true;
426 }
427
428 pub fn set_axes_xlabel<S: Into<String>>(&mut self, axes_index: usize, label: S) {
429 self.ensure_axes_metadata_capacity(axes_index + 1);
430 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
431 meta.x_label = Some(label.into());
432 }
433 if axes_index == self.active_axes_index {
434 self.sync_legacy_fields_from_active_axes();
435 }
436 self.dirty = true;
437 }
438
439 pub fn set_axes_ylabel<S: Into<String>>(&mut self, axes_index: usize, label: S) {
440 self.ensure_axes_metadata_capacity(axes_index + 1);
441 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
442 meta.y_label = Some(label.into());
443 }
444 if axes_index == self.active_axes_index {
445 self.sync_legacy_fields_from_active_axes();
446 }
447 self.dirty = true;
448 }
449
450 pub fn set_axes_zlabel<S: Into<String>>(&mut self, axes_index: usize, label: S) {
451 self.ensure_axes_metadata_capacity(axes_index + 1);
452 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
453 meta.z_label = Some(label.into());
454 }
455 if axes_index == self.active_axes_index {
456 self.sync_legacy_fields_from_active_axes();
457 }
458 self.dirty = true;
459 }
460
461 pub fn add_axes_text_annotation<S: Into<String>>(
462 &mut self,
463 axes_index: usize,
464 position: glam::Vec3,
465 text: S,
466 style: TextStyle,
467 ) -> usize {
468 self.ensure_axes_metadata_capacity(axes_index + 1);
469 let Some(meta) = self.axes_metadata.get_mut(axes_index) else {
470 return 0;
471 };
472 meta.world_text_annotations.push(TextAnnotation {
473 position,
474 text: text.into(),
475 style,
476 });
477 self.dirty = true;
478 meta.world_text_annotations.len() - 1
479 }
480
481 pub fn axes_text_annotation(
482 &self,
483 axes_index: usize,
484 annotation_index: usize,
485 ) -> Option<&TextAnnotation> {
486 self.axes_metadata
487 .get(axes_index)
488 .and_then(|meta| meta.world_text_annotations.get(annotation_index))
489 }
490
491 pub fn set_axes_text_annotation_text<S: Into<String>>(
492 &mut self,
493 axes_index: usize,
494 annotation_index: usize,
495 text: S,
496 ) {
497 if let Some(annotation) = self
498 .axes_metadata
499 .get_mut(axes_index)
500 .and_then(|meta| meta.world_text_annotations.get_mut(annotation_index))
501 {
502 annotation.text = text.into();
503 self.dirty = true;
504 }
505 }
506
507 pub fn set_axes_text_annotation_position(
508 &mut self,
509 axes_index: usize,
510 annotation_index: usize,
511 position: glam::Vec3,
512 ) {
513 if let Some(annotation) = self
514 .axes_metadata
515 .get_mut(axes_index)
516 .and_then(|meta| meta.world_text_annotations.get_mut(annotation_index))
517 {
518 annotation.position = position;
519 self.dirty = true;
520 }
521 }
522
523 pub fn set_axes_text_annotation_style(
524 &mut self,
525 axes_index: usize,
526 annotation_index: usize,
527 style: TextStyle,
528 ) {
529 if let Some(annotation) = self
530 .axes_metadata
531 .get_mut(axes_index)
532 .and_then(|meta| meta.world_text_annotations.get_mut(annotation_index))
533 {
534 annotation.style = style;
535 self.dirty = true;
536 }
537 }
538
539 pub fn axes_text_annotations(&self, axes_index: usize) -> &[TextAnnotation] {
540 self.axes_metadata
541 .get(axes_index)
542 .map(|meta| meta.world_text_annotations.as_slice())
543 .unwrap_or(&[])
544 }
545
546 pub fn set_axes_labels<S: Into<String>>(&mut self, axes_index: usize, x_label: S, y_label: S) {
547 self.ensure_axes_metadata_capacity(axes_index + 1);
548 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
549 meta.x_label = Some(x_label.into());
550 meta.y_label = Some(y_label.into());
551 }
552 if axes_index == self.active_axes_index {
553 self.sync_legacy_fields_from_active_axes();
554 }
555 self.dirty = true;
556 }
557
558 pub fn set_axes_tick_labels(
559 &mut self,
560 axes_index: usize,
561 x_labels: Option<Vec<String>>,
562 y_labels: Option<Vec<String>>,
563 ) {
564 self.ensure_axes_metadata_capacity(axes_index + 1);
565 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
566 meta.x_tick_labels = x_labels;
567 meta.y_tick_labels = y_labels;
568 }
569 self.dirty = true;
570 }
571
572 pub fn set_axes_style(&mut self, axes_index: usize, style: TextStyle) {
573 self.ensure_axes_metadata_capacity(axes_index + 1);
574 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
575 meta.axes_style = style;
576 }
577 self.dirty = true;
578 }
579
580 pub fn set_axes_title_style(&mut self, axes_index: usize, style: TextStyle) {
581 self.ensure_axes_metadata_capacity(axes_index + 1);
582 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
583 meta.title_style = style;
584 }
585 self.dirty = true;
586 }
587
588 pub fn set_axes_xlabel_style(&mut self, axes_index: usize, style: TextStyle) {
589 self.ensure_axes_metadata_capacity(axes_index + 1);
590 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
591 meta.x_label_style = style;
592 }
593 self.dirty = true;
594 }
595
596 pub fn set_axes_ylabel_style(&mut self, axes_index: usize, style: TextStyle) {
597 self.ensure_axes_metadata_capacity(axes_index + 1);
598 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
599 meta.y_label_style = style;
600 }
601 self.dirty = true;
602 }
603
604 pub fn set_axes_zlabel_style(&mut self, axes_index: usize, style: TextStyle) {
605 self.ensure_axes_metadata_capacity(axes_index + 1);
606 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
607 meta.z_label_style = style;
608 }
609 self.dirty = true;
610 }
611
612 pub fn with_limits(mut self, x_limits: (f64, f64), y_limits: (f64, f64)) -> Self {
614 self.x_limits = Some(x_limits);
615 self.y_limits = Some(y_limits);
616 self.dirty = true;
617 self
618 }
619
620 pub fn with_legend(mut self, enabled: bool) -> Self {
622 self.set_legend(enabled);
623 self
624 }
625
626 pub fn set_legend(&mut self, enabled: bool) {
627 self.set_axes_legend_enabled(self.active_axes_index, enabled);
628 }
629
630 pub fn set_axes_legend_enabled(&mut self, axes_index: usize, enabled: bool) {
631 self.ensure_axes_metadata_capacity(axes_index + 1);
632 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
633 meta.legend_enabled = enabled;
634 }
635 if axes_index == self.active_axes_index {
636 self.sync_legacy_fields_from_active_axes();
637 }
638 self.dirty = true;
639 }
640
641 pub fn set_axes_legend_style(&mut self, axes_index: usize, style: LegendStyle) {
642 self.ensure_axes_metadata_capacity(axes_index + 1);
643 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
644 meta.legend_style = style;
645 }
646 self.dirty = true;
647 }
648
649 pub fn set_axes_log_modes(&mut self, axes_index: usize, x_log: bool, y_log: bool) {
650 self.ensure_axes_metadata_capacity(axes_index + 1);
651 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
652 meta.x_log = x_log;
653 meta.y_log = y_log;
654 }
655 if axes_index == self.active_axes_index {
656 self.sync_legacy_fields_from_active_axes();
657 }
658 self.dirty = true;
659 }
660
661 pub fn set_axes_view(&mut self, axes_index: usize, azimuth_deg: f32, elevation_deg: f32) {
662 self.ensure_axes_metadata_capacity(axes_index + 1);
663 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
664 meta.view_azimuth_deg = Some(azimuth_deg);
665 meta.view_elevation_deg = Some(elevation_deg);
666 meta.view_revision = meta.view_revision.wrapping_add(1);
667 }
668 self.dirty = true;
669 }
670
671 pub fn with_grid(mut self, enabled: bool) -> Self {
673 self.set_grid(enabled);
674 self
675 }
676
677 pub fn set_grid(&mut self, enabled: bool) {
678 self.set_axes_grid_enabled(self.active_axes_index, enabled);
679 self.dirty = true;
680 }
681
682 pub fn set_axes_grid_enabled(&mut self, axes_index: usize, enabled: bool) {
683 self.ensure_axes_metadata_capacity(axes_index + 1);
684 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
685 meta.grid_enabled = enabled;
686 }
687 if axes_index == self.active_axes_index {
688 self.sync_legacy_fields_from_active_axes();
689 }
690 self.dirty = true;
691 }
692
693 pub fn with_minor_grid(mut self, enabled: bool) -> Self {
694 self.set_minor_grid(enabled);
695 self
696 }
697
698 pub fn set_minor_grid(&mut self, enabled: bool) {
699 self.set_axes_minor_grid_enabled(self.active_axes_index, enabled);
700 self.dirty = true;
701 }
702
703 pub fn set_axes_minor_grid_enabled(&mut self, axes_index: usize, enabled: bool) {
704 self.ensure_axes_metadata_capacity(axes_index + 1);
705 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
706 meta.minor_grid_enabled = enabled;
707 meta.minor_grid_explicit = true;
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 minor_grid_enabled_for_axes(&self, axes_index: usize) -> bool {
716 self.axes_metadata(axes_index)
717 .map(|meta| {
718 if meta.minor_grid_explicit {
719 meta.minor_grid_enabled
720 } else {
721 self.minor_grid_enabled
722 }
723 })
724 .unwrap_or(self.minor_grid_enabled)
725 }
726
727 pub fn with_background_color(mut self, color: Vec4) -> Self {
729 self.set_background_color(color);
730 self
731 }
732
733 pub fn set_background_color(&mut self, color: Vec4) {
734 self.background_color = color;
735 self.dirty = true;
736 }
737
738 pub fn with_xlog(mut self, enabled: bool) -> Self {
740 self.set_axes_log_modes(self.active_axes_index, enabled, self.y_log);
741 self
742 }
743 pub fn with_ylog(mut self, enabled: bool) -> Self {
744 self.set_axes_log_modes(self.active_axes_index, self.x_log, enabled);
745 self
746 }
747 pub fn with_axis_equal(mut self, enabled: bool) -> Self {
748 self.set_axis_equal(enabled);
749 self
750 }
751
752 pub fn set_axis_equal(&mut self, enabled: bool) {
753 self.set_axes_axis_equal(self.active_axes_index, enabled);
754 self.dirty = true;
755 }
756 pub fn set_axes_axis_equal(&mut self, axes_index: usize, enabled: bool) {
757 self.ensure_axes_metadata_capacity(axes_index + 1);
758 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
759 meta.axis_equal = enabled;
760 }
761 if axes_index == self.active_axes_index {
762 self.sync_legacy_fields_from_active_axes();
763 }
764 self.dirty = true;
765 }
766 pub fn with_colormap(mut self, cmap: ColorMap) -> Self {
767 self.set_axes_colormap(self.active_axes_index, cmap);
768 self
769 }
770 pub fn with_colorbar(mut self, enabled: bool) -> Self {
771 self.set_axes_colorbar_enabled(self.active_axes_index, enabled);
772 self
773 }
774 pub fn with_color_limits(mut self, limits: Option<(f64, f64)>) -> Self {
775 self.set_axes_color_limits(self.active_axes_index, limits);
776 self
777 }
778
779 pub fn with_subplot_grid(mut self, rows: usize, cols: usize) -> Self {
781 self.set_subplot_grid(rows, cols);
782 self
783 }
784
785 pub fn axes_grid(&self) -> (usize, usize) {
787 (self.axes_rows, self.axes_cols)
788 }
789
790 pub fn plot_axes_indices(&self) -> &[usize] {
792 &self.plot_axes_indices
793 }
794
795 pub fn assign_plot_to_axes(
797 &mut self,
798 plot_index: usize,
799 axes_index: usize,
800 ) -> Result<(), String> {
801 if plot_index >= self.plot_axes_indices.len() {
802 return Err(format!(
803 "assign_plot_to_axes: index {plot_index} out of bounds"
804 ));
805 }
806 let max_axes = self.axes_rows.max(1) * self.axes_cols.max(1);
807 let ai = axes_index.min(max_axes.saturating_sub(1));
808 self.plot_axes_indices[plot_index] = ai;
809 self.dirty = true;
810 Ok(())
811 }
812 pub fn set_subplot_grid(&mut self, rows: usize, cols: usize) {
814 self.axes_rows = rows.max(1);
815 self.axes_cols = cols.max(1);
816 self.ensure_axes_metadata_capacity(self.axes_rows * self.axes_cols);
817 self.active_axes_index = self.active_axes_index.min(
818 self.axes_rows
819 .saturating_mul(self.axes_cols)
820 .saturating_sub(1),
821 );
822 self.sync_legacy_fields_from_active_axes();
823 self.dirty = true;
824 }
825
826 pub fn set_color_limits(&mut self, limits: Option<(f64, f64)>) {
828 self.set_axes_color_limits(self.active_axes_index, limits);
829 self.dirty = true;
830 }
831
832 pub fn set_z_limits(&mut self, limits: Option<(f64, f64)>) {
833 self.set_axes_z_limits(self.active_axes_index, limits);
834 self.dirty = true;
835 }
836
837 pub fn set_axes_limits(
838 &mut self,
839 axes_index: usize,
840 x: Option<(f64, f64)>,
841 y: Option<(f64, f64)>,
842 ) {
843 self.ensure_axes_metadata_capacity(axes_index + 1);
844 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
845 meta.x_limits = x;
846 meta.y_limits = y;
847 }
848 if axes_index == self.active_axes_index {
849 self.sync_legacy_fields_from_active_axes();
850 }
851 self.dirty = true;
852 }
853
854 pub fn set_axes_z_limits(&mut self, axes_index: usize, limits: Option<(f64, f64)>) {
855 self.ensure_axes_metadata_capacity(axes_index + 1);
856 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
857 meta.z_limits = limits;
858 }
859 if axes_index == self.active_axes_index {
860 self.sync_legacy_fields_from_active_axes();
861 }
862 self.dirty = true;
863 }
864
865 pub fn set_axes_box_enabled(&mut self, axes_index: usize, enabled: bool) {
866 self.ensure_axes_metadata_capacity(axes_index + 1);
867 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
868 meta.box_enabled = enabled;
869 }
870 if axes_index == self.active_axes_index {
871 self.sync_legacy_fields_from_active_axes();
872 }
873 self.dirty = true;
874 }
875
876 pub fn set_axes_colorbar_enabled(&mut self, axes_index: usize, enabled: bool) {
877 self.ensure_axes_metadata_capacity(axes_index + 1);
878 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
879 meta.colorbar_enabled = enabled;
880 }
881 if axes_index == self.active_axes_index {
882 self.sync_legacy_fields_from_active_axes();
883 }
884 self.dirty = true;
885 }
886
887 pub fn set_axes_colormap(&mut self, axes_index: usize, cmap: ColorMap) {
888 self.ensure_axes_metadata_capacity(axes_index + 1);
889 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
890 meta.colormap = cmap;
891 }
892 for (idx, plot) in self.plots.iter_mut().enumerate() {
893 if self.plot_axes_indices.get(idx).copied().unwrap_or(0) != axes_index {
894 continue;
895 }
896 if let PlotElement::Surface(surface) = plot {
897 *surface = surface.clone().with_colormap(cmap);
898 }
899 }
900 if axes_index == self.active_axes_index {
901 self.sync_legacy_fields_from_active_axes();
902 }
903 self.dirty = true;
904 }
905
906 pub fn set_axes_color_limits(&mut self, axes_index: usize, limits: Option<(f64, f64)>) {
907 self.ensure_axes_metadata_capacity(axes_index + 1);
908 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
909 meta.color_limits = limits;
910 }
911 for (idx, plot) in self.plots.iter_mut().enumerate() {
912 if self.plot_axes_indices.get(idx).copied().unwrap_or(0) != axes_index {
913 continue;
914 }
915 if let PlotElement::Surface(surface) = plot {
916 surface.set_color_limits(limits);
917 }
918 }
919 if axes_index == self.active_axes_index {
920 self.sync_legacy_fields_from_active_axes();
921 }
922 self.dirty = true;
923 }
924
925 fn total_axes(&self) -> usize {
926 self.axes_rows.max(1) * self.axes_cols.max(1)
927 }
928
929 fn normalize_axes_index(&self, axes_index: usize) -> usize {
930 let total = self.total_axes().max(1);
931 axes_index.min(total - 1)
932 }
933
934 fn push_plot(&mut self, element: PlotElement, axes_index: usize) -> usize {
935 let idx = self.normalize_axes_index(axes_index);
936 self.plots.push(element);
937 self.plot_axes_indices.push(idx);
938 self.dirty = true;
939 self.plots.len() - 1
940 }
941
942 pub fn add_line_plot(&mut self, plot: LinePlot) -> usize {
944 self.add_line_plot_on_axes(plot, 0)
945 }
946
947 pub fn add_line_plot_on_axes(&mut self, plot: LinePlot, axes_index: usize) -> usize {
948 self.push_plot(PlotElement::Line(plot), axes_index)
949 }
950
951 pub fn add_reference_line_on_axes(&mut self, plot: ReferenceLine, axes_index: usize) -> usize {
952 self.push_plot(PlotElement::ReferenceLine(plot), axes_index)
953 }
954
955 pub fn add_scatter_plot(&mut self, plot: ScatterPlot) -> usize {
957 self.add_scatter_plot_on_axes(plot, 0)
958 }
959
960 pub fn add_scatter_plot_on_axes(&mut self, plot: ScatterPlot, axes_index: usize) -> usize {
961 self.push_plot(PlotElement::Scatter(plot), axes_index)
962 }
963
964 pub fn add_bar_chart(&mut self, plot: BarChart) -> usize {
966 self.add_bar_chart_on_axes(plot, 0)
967 }
968
969 pub fn add_bar_chart_on_axes(&mut self, plot: BarChart, axes_index: usize) -> usize {
970 self.push_plot(PlotElement::Bar(plot), axes_index)
971 }
972
973 pub fn add_errorbar(&mut self, plot: ErrorBar) -> usize {
975 self.add_errorbar_on_axes(plot, 0)
976 }
977
978 pub fn add_errorbar_on_axes(&mut self, plot: ErrorBar, axes_index: usize) -> usize {
979 self.push_plot(PlotElement::ErrorBar(plot), axes_index)
980 }
981
982 pub fn add_stairs_plot(&mut self, plot: StairsPlot) -> usize {
984 self.add_stairs_plot_on_axes(plot, 0)
985 }
986
987 pub fn add_stairs_plot_on_axes(&mut self, plot: StairsPlot, axes_index: usize) -> usize {
988 self.push_plot(PlotElement::Stairs(plot), axes_index)
989 }
990
991 pub fn add_stem_plot(&mut self, plot: StemPlot) -> usize {
993 self.add_stem_plot_on_axes(plot, 0)
994 }
995
996 pub fn add_stem_plot_on_axes(&mut self, plot: StemPlot, axes_index: usize) -> usize {
997 self.push_plot(PlotElement::Stem(plot), axes_index)
998 }
999
1000 pub fn add_area_plot(&mut self, plot: AreaPlot) -> usize {
1002 self.add_area_plot_on_axes(plot, 0)
1003 }
1004
1005 pub fn add_area_plot_on_axes(&mut self, plot: AreaPlot, axes_index: usize) -> usize {
1006 self.push_plot(PlotElement::Area(plot), axes_index)
1007 }
1008
1009 pub fn add_quiver_plot(&mut self, plot: QuiverPlot) -> usize {
1010 self.add_quiver_plot_on_axes(plot, 0)
1011 }
1012
1013 pub fn add_quiver_plot_on_axes(&mut self, plot: QuiverPlot, axes_index: usize) -> usize {
1014 self.push_plot(PlotElement::Quiver(plot), axes_index)
1015 }
1016
1017 pub fn add_pie_chart(&mut self, plot: PieChart) -> usize {
1018 self.add_pie_chart_on_axes(plot, 0)
1019 }
1020
1021 pub fn add_pie_chart_on_axes(&mut self, plot: PieChart, axes_index: usize) -> usize {
1022 self.push_plot(PlotElement::Pie(plot), axes_index)
1023 }
1024
1025 pub fn add_surface_plot(&mut self, plot: SurfacePlot) -> usize {
1027 self.add_surface_plot_on_axes(plot, 0)
1028 }
1029
1030 pub fn add_surface_plot_on_axes(&mut self, plot: SurfacePlot, axes_index: usize) -> usize {
1031 self.push_plot(PlotElement::Surface(plot), axes_index)
1032 }
1033
1034 pub fn add_patch_plot(&mut self, plot: PatchPlot) -> usize {
1035 self.add_patch_plot_on_axes(plot, 0)
1036 }
1037
1038 pub fn add_patch_plot_on_axes(&mut self, plot: PatchPlot, axes_index: usize) -> usize {
1039 self.push_plot(PlotElement::Patch(plot), axes_index)
1040 }
1041
1042 pub fn add_line3_plot(&mut self, plot: Line3Plot) -> usize {
1043 self.add_line3_plot_on_axes(plot, self.active_axes_index)
1044 }
1045
1046 pub fn add_line3_plot_on_axes(&mut self, plot: Line3Plot, axes_index: usize) -> usize {
1047 self.push_plot(PlotElement::Line3(plot), axes_index)
1048 }
1049
1050 pub fn add_scatter3_plot(&mut self, plot: Scatter3Plot) -> usize {
1052 self.add_scatter3_plot_on_axes(plot, 0)
1053 }
1054
1055 pub fn add_scatter3_plot_on_axes(&mut self, plot: Scatter3Plot, axes_index: usize) -> usize {
1056 self.push_plot(PlotElement::Scatter3(plot), axes_index)
1057 }
1058
1059 pub fn add_contour_plot(&mut self, plot: ContourPlot) -> usize {
1060 self.add_contour_plot_on_axes(plot, 0)
1061 }
1062
1063 pub fn add_contour_plot_on_axes(&mut self, plot: ContourPlot, axes_index: usize) -> usize {
1064 self.push_plot(PlotElement::Contour(plot), axes_index)
1065 }
1066
1067 pub fn add_contour_fill_plot(&mut self, plot: ContourFillPlot) -> usize {
1068 self.add_contour_fill_plot_on_axes(plot, 0)
1069 }
1070
1071 pub fn add_contour_fill_plot_on_axes(
1072 &mut self,
1073 plot: ContourFillPlot,
1074 axes_index: usize,
1075 ) -> usize {
1076 self.push_plot(PlotElement::ContourFill(plot), axes_index)
1077 }
1078
1079 pub fn remove_plot(&mut self, index: usize) -> Result<(), String> {
1081 if index >= self.plots.len() {
1082 return Err(format!("Plot index {index} out of bounds"));
1083 }
1084 self.plots.remove(index);
1085 self.plot_axes_indices.remove(index);
1086 self.dirty = true;
1087 Ok(())
1088 }
1089
1090 pub fn clear(&mut self) {
1092 self.plots.clear();
1093 self.plot_axes_indices.clear();
1094 self.dirty = true;
1095 }
1096
1097 pub fn clear_axes(&mut self, axes_index: usize) {
1099 let mut i = 0usize;
1100 while i < self.plots.len() {
1101 let ax = *self.plot_axes_indices.get(i).unwrap_or(&0);
1102 if ax == axes_index {
1103 self.plots.remove(i);
1104 self.plot_axes_indices.remove(i);
1105 } else {
1106 i += 1;
1107 }
1108 }
1109 self.ensure_axes_metadata_capacity(axes_index + 1);
1110 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1111 meta.world_text_annotations.clear();
1112 }
1113 self.dirty = true;
1114 }
1115
1116 pub fn len(&self) -> usize {
1118 self.plots.len()
1119 }
1120
1121 pub fn is_empty(&self) -> bool {
1123 self.plots.is_empty()
1124 }
1125
1126 pub fn plots(&self) -> impl Iterator<Item = &PlotElement> {
1128 self.plots.iter()
1129 }
1130
1131 pub fn get_plot_mut(&mut self, index: usize) -> Option<&mut PlotElement> {
1133 self.dirty = true;
1134 self.plots.get_mut(index)
1135 }
1136
1137 pub fn bounds(&mut self) -> BoundingBox {
1139 if self.dirty || self.bounds.is_none() {
1140 self.compute_bounds();
1141 }
1142 self.bounds.unwrap()
1143 }
1144
1145 fn compute_bounds(&mut self) {
1147 if self.plots.is_empty() {
1148 self.bounds = Some(BoundingBox::default());
1149 return;
1150 }
1151
1152 let mut combined_bounds = None;
1153 let mut reference_lines = Vec::new();
1154
1155 for plot in &mut self.plots {
1156 if !plot.is_visible() {
1157 continue;
1158 }
1159 if let PlotElement::ReferenceLine(reference_line) = plot {
1160 reference_lines.push(reference_line.clone());
1161 continue;
1162 }
1163
1164 let plot_bounds = plot.bounds();
1165
1166 combined_bounds = match combined_bounds {
1167 None => Some(plot_bounds),
1168 Some(existing) => Some(existing.union(&plot_bounds)),
1169 };
1170 }
1171
1172 for line in reference_lines {
1173 let mut point_bounds = line.coordinate_bounds();
1174 if let Some(existing) = combined_bounds {
1175 match line.orientation {
1176 ReferenceLineOrientation::Vertical => {
1177 point_bounds.min.y = existing.min.y;
1178 point_bounds.max.y = existing.max.y;
1179 }
1180 ReferenceLineOrientation::Horizontal => {
1181 point_bounds.min.x = existing.min.x;
1182 point_bounds.max.x = existing.max.x;
1183 }
1184 }
1185 } else {
1186 let (x_range, y_range) =
1187 Self::reference_line_ranges(self.x_limits, self.y_limits, None, None, &line);
1188 point_bounds.min.x = x_range.0 as f32;
1189 point_bounds.max.x = x_range.1 as f32;
1190 point_bounds.min.y = y_range.0 as f32;
1191 point_bounds.max.y = y_range.1 as f32;
1192 }
1193 combined_bounds = match combined_bounds {
1194 None => Some(point_bounds),
1195 Some(existing) => Some(existing.union(&point_bounds)),
1196 };
1197 }
1198
1199 self.bounds = combined_bounds.or_else(|| Some(BoundingBox::default()));
1200 self.dirty = false;
1201 }
1202
1203 pub fn render_data(&mut self) -> Vec<RenderData> {
1205 self.render_data_with_viewport(None)
1206 }
1207
1208 pub fn render_data_with_viewport(
1214 &mut self,
1215 viewport_px: Option<(u32, u32)>,
1216 ) -> Vec<RenderData> {
1217 self.render_data_with_viewport_and_gpu(viewport_px, None)
1218 }
1219
1220 pub fn render_data_with_viewport_and_gpu(
1221 &mut self,
1222 viewport_px: Option<(u32, u32)>,
1223 gpu: Option<&GpuPackContext<'_>>,
1224 ) -> Vec<RenderData> {
1225 self.render_data_with_axes_with_viewport_and_gpu(viewport_px, None, None, gpu)
1226 .into_iter()
1227 .map(|(_, render_data)| render_data)
1228 .collect()
1229 }
1230
1231 pub fn render_data_with_axes_with_viewport_and_gpu(
1232 &mut self,
1233 viewport_px: Option<(u32, u32)>,
1234 axes_viewports_px: Option<&[(u32, u32)]>,
1235 axes_view_bounds: Option<PerAxesViewBoundsRef<'_>>,
1236 gpu: Option<&GpuPackContext<'_>>,
1237 ) -> Vec<(usize, RenderData)> {
1238 fn push_with_optional_markers(
1239 out: &mut Vec<(usize, RenderData)>,
1240 axes_index: usize,
1241 render_data: RenderData,
1242 marker_data: Option<RenderData>,
1243 ) {
1244 out.push((axes_index, render_data));
1245 if let Some(marker_data) = marker_data {
1246 out.push((axes_index, marker_data));
1247 }
1248 }
1249
1250 let reference_base_bounds = self.reference_base_bounds_by_axes();
1251 let mut out = Vec::new();
1252 for (plot_idx, p) in self.plots.iter_mut().enumerate() {
1253 if !p.is_visible() {
1254 continue;
1255 }
1256 let axes_index = self.plot_axes_indices.get(plot_idx).copied().unwrap_or(0);
1257 let axes_view_bounds = axes_view_bounds
1258 .and_then(|bounds| bounds.get(axes_index).copied())
1259 .flatten();
1260 if let PlotElement::Surface(s) = p {
1261 if let Some(meta) = self.axes_metadata.get(axes_index) {
1262 s.set_color_limits(meta.color_limits);
1263 *s = s.clone().with_colormap(meta.colormap);
1264 }
1265 }
1266
1267 match p {
1268 PlotElement::Line(plot) => {
1269 let axes_viewport_px = axes_viewports_px
1270 .and_then(|viewports| viewports.get(axes_index).copied())
1271 .or(viewport_px);
1272 trace!(
1273 target: "runmat_plot",
1274 "figure: render_data line viewport_px={:?} axes_index={} axes_viewport_px={:?} axes_view_bounds={:?} gpu_ctx_present={} gpu_line_inputs_present={} gpu_vertices_present={}",
1275 viewport_px,
1276 axes_index,
1277 axes_viewport_px,
1278 axes_view_bounds,
1279 gpu.is_some(),
1280 plot.has_gpu_line_inputs(),
1281 plot.has_gpu_vertices()
1282 );
1283 push_with_optional_markers(
1284 &mut out,
1285 axes_index,
1286 plot.render_data_with_viewport_gpu(axes_viewport_px, axes_view_bounds, gpu),
1287 plot.marker_render_data(),
1288 );
1289 }
1290 PlotElement::ErrorBar(plot) => {
1291 push_with_optional_markers(
1292 &mut out,
1293 axes_index,
1294 plot.render_data_with_viewport_gpu(
1295 axes_viewports_px
1296 .and_then(|viewports| viewports.get(axes_index).copied())
1297 .or(viewport_px),
1298 gpu,
1299 ),
1300 plot.marker_render_data(),
1301 );
1302 }
1303 PlotElement::Stairs(plot) => {
1304 push_with_optional_markers(
1305 &mut out,
1306 axes_index,
1307 plot.render_data_with_viewport(
1308 axes_viewports_px
1309 .and_then(|viewports| viewports.get(axes_index).copied())
1310 .or(viewport_px),
1311 ),
1312 plot.marker_render_data(),
1313 );
1314 }
1315 PlotElement::Stem(plot) => {
1316 push_with_optional_markers(
1317 &mut out,
1318 axes_index,
1319 plot.render_data_with_viewport(
1320 axes_viewports_px
1321 .and_then(|viewports| viewports.get(axes_index).copied())
1322 .or(viewport_px),
1323 ),
1324 plot.marker_render_data(),
1325 );
1326 }
1327 PlotElement::Contour(plot) => out.push((
1328 axes_index,
1329 plot.render_data_with_viewport(
1330 axes_viewports_px
1331 .and_then(|viewports| viewports.get(axes_index).copied())
1332 .or(viewport_px),
1333 ),
1334 )),
1335 PlotElement::ReferenceLine(plot) => {
1336 let (x_range, y_range) = Self::reference_line_ranges(
1337 self.x_limits,
1338 self.y_limits,
1339 self.axes_metadata.get(axes_index),
1340 reference_base_bounds.get(axes_index).copied().flatten(),
1341 plot,
1342 );
1343 out.push((
1344 axes_index,
1345 plot.render_data_with_range(
1346 x_range,
1347 y_range,
1348 axes_viewports_px
1349 .and_then(|viewports| viewports.get(axes_index).copied())
1350 .or(viewport_px),
1351 ),
1352 ));
1353 }
1354 PlotElement::Patch(plot) => {
1355 out.push((axes_index, plot.render_data()));
1356 if let Some(edge_data) = plot.edge_render_data_with_viewport(
1357 axes_viewports_px
1358 .and_then(|viewports| viewports.get(axes_index).copied())
1359 .or(viewport_px),
1360 ) {
1361 out.push((axes_index, edge_data));
1362 }
1363 }
1364 PlotElement::Line3(plot) => out.push((
1365 axes_index,
1366 plot.render_data_with_viewport_gpu(
1367 axes_viewports_px
1368 .and_then(|viewports| viewports.get(axes_index).copied())
1369 .or(viewport_px),
1370 self.axes_metadata.get(axes_index).and_then(|meta| {
1371 match (meta.view_azimuth_deg, meta.view_elevation_deg) {
1372 (Some(az), Some(el)) => Some((az, el)),
1373 _ => None,
1374 }
1375 }),
1376 gpu,
1377 ),
1378 )),
1379 _ => out.push((axes_index, p.render_data())),
1380 }
1381 }
1382 out
1383 }
1384
1385 fn reference_base_bounds_by_axes(&mut self) -> Vec<Option<BoundingBox>> {
1386 let axes_count = self.total_axes().max(1);
1387 let mut bounds: Vec<Option<BoundingBox>> = vec![None; axes_count];
1388 for (plot_idx, plot) in self.plots.iter_mut().enumerate() {
1389 if !plot.is_visible() || matches!(plot, PlotElement::ReferenceLine(_)) {
1390 continue;
1391 }
1392 let axes_index = self
1393 .plot_axes_indices
1394 .get(plot_idx)
1395 .copied()
1396 .unwrap_or(0)
1397 .min(axes_count - 1);
1398 let plot_bounds = plot.bounds();
1399 bounds[axes_index] = Some(match bounds[axes_index] {
1400 None => plot_bounds,
1401 Some(existing) => existing.union(&plot_bounds),
1402 });
1403 }
1404 bounds
1405 }
1406
1407 fn reference_line_ranges(
1408 x_limits: Option<(f64, f64)>,
1409 y_limits: Option<(f64, f64)>,
1410 meta: Option<&AxesMetadata>,
1411 base: Option<BoundingBox>,
1412 line: &ReferenceLine,
1413 ) -> ((f64, f64), (f64, f64)) {
1414 let x_range = x_limits
1415 .or_else(|| meta.and_then(|m| m.x_limits))
1416 .or_else(|| base.map(|b| (b.min.x as f64, b.max.x as f64)))
1417 .unwrap_or(match line.orientation {
1418 ReferenceLineOrientation::Vertical => (line.value - 0.5, line.value + 0.5),
1419 ReferenceLineOrientation::Horizontal => (0.0, 1.0),
1420 });
1421 let y_range = y_limits
1422 .or_else(|| meta.and_then(|m| m.y_limits))
1423 .or_else(|| base.map(|b| (b.min.y as f64, b.max.y as f64)))
1424 .unwrap_or(match line.orientation {
1425 ReferenceLineOrientation::Vertical => (0.0, 1.0),
1426 ReferenceLineOrientation::Horizontal => (line.value - 0.5, line.value + 0.5),
1427 });
1428 (
1429 normalize_reference_range(x_range),
1430 normalize_reference_range(y_range),
1431 )
1432 }
1433
1434 pub fn legend_entries(&self) -> Vec<LegendEntry> {
1436 let mut entries = Vec::new();
1437
1438 for plot in &self.plots {
1439 if let Some(label) = plot.label() {
1440 entries.push(LegendEntry {
1441 label,
1442 color: plot.color(),
1443 plot_type: plot.plot_type(),
1444 });
1445 }
1446 }
1447
1448 entries
1449 }
1450
1451 pub fn legend_entries_for_axes(&self, axes_index: usize) -> Vec<LegendEntry> {
1452 let mut entries = Vec::new();
1453 for (plot_idx, plot) in self.plots.iter().enumerate() {
1454 let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
1455 if plot_axes != axes_index {
1456 continue;
1457 }
1458 match plot {
1459 PlotElement::Pie(pie) => {
1460 for slice in pie.slice_meta() {
1461 entries.push(LegendEntry {
1462 label: slice.label,
1463 color: slice.color,
1464 plot_type: plot.plot_type(),
1465 });
1466 }
1467 }
1468 _ => {
1469 if let Some(label) = plot.label() {
1470 entries.push(LegendEntry {
1471 label,
1472 color: plot.color(),
1473 plot_type: plot.plot_type(),
1474 });
1475 }
1476 }
1477 }
1478 }
1479 entries
1480 }
1481
1482 pub fn pie_labels_for_axes(&self, axes_index: usize) -> Vec<PieLabelEntry> {
1483 let mut out = Vec::new();
1484 for (plot_idx, plot) in self.plots.iter().enumerate() {
1485 let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
1486 if plot_axes != axes_index {
1487 continue;
1488 }
1489 if let PlotElement::Pie(pie) = plot {
1490 for slice in pie.slice_meta() {
1491 out.push(PieLabelEntry {
1492 label: slice.label,
1493 position: glam::Vec2::new(
1494 slice.mid_angle.cos() * 1.15 + slice.offset.x,
1495 slice.mid_angle.sin() * 1.15 + slice.offset.y,
1496 ),
1497 });
1498 }
1499 }
1500 }
1501 out
1502 }
1503
1504 pub fn set_labels(&mut self, labels: &[String]) {
1506 self.set_labels_for_axes(self.active_axes_index, labels);
1507 }
1508
1509 pub fn set_labels_for_axes(&mut self, axes_index: usize, labels: &[String]) {
1510 let mut idx = 0usize;
1511 for (plot_idx, plot) in self.plots.iter_mut().enumerate() {
1512 let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
1513 if plot_axes != axes_index {
1514 continue;
1515 }
1516 if !plot.is_visible() {
1517 continue;
1518 }
1519 if idx >= labels.len() {
1520 break;
1521 }
1522 match plot {
1523 PlotElement::Pie(pie) => {
1524 let remaining = &labels[idx..];
1525 if remaining.len() >= pie.values.len() {
1526 pie.set_slice_labels(remaining[..pie.values.len()].to_vec());
1527 idx += pie.values.len();
1528 } else {
1529 pie.set_slice_labels(remaining.to_vec());
1530 idx = labels.len();
1531 }
1532 }
1533 _ => {
1534 plot.set_label(Some(labels[idx].clone()));
1535 idx += 1;
1536 }
1537 }
1538 }
1539 self.dirty = true;
1540 }
1541
1542 pub fn statistics(&self) -> FigureStatistics {
1544 let plot_counts = self.plots.iter().fold(HashMap::new(), |mut acc, plot| {
1545 let plot_type = plot.plot_type();
1546 *acc.entry(plot_type).or_insert(0) += 1;
1547 acc
1548 });
1549
1550 let total_memory: usize = self
1551 .plots
1552 .iter()
1553 .map(|plot| plot.estimated_memory_usage())
1554 .sum();
1555
1556 let visible_count = self.plots.iter().filter(|plot| plot.is_visible()).count();
1557
1558 FigureStatistics {
1559 total_plots: self.plots.len(),
1560 visible_plots: visible_count,
1561 plot_type_counts: plot_counts,
1562 total_memory_usage: total_memory,
1563 has_legend: self.legend_enabled && !self.legend_entries().is_empty(),
1564 }
1565 }
1566
1567 pub fn categorical_axis_labels(&self) -> Option<(bool, Vec<String>)> {
1571 for plot in &self.plots {
1572 if let PlotElement::Bar(b) = plot {
1573 if b.histogram_bin_edges().is_some() {
1574 continue;
1575 }
1576 let is_x = matches!(b.orientation, crate::plots::bar::Orientation::Vertical);
1577 return Some((is_x, b.labels.clone()));
1578 }
1579 }
1580 None
1581 }
1582
1583 pub fn categorical_axis_labels_for_axes(
1584 &self,
1585 axes_index: usize,
1586 ) -> Option<(bool, Vec<String>)> {
1587 for (plot_idx, plot) in self.plots.iter().enumerate() {
1588 let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
1589 if plot_axes != axes_index {
1590 continue;
1591 }
1592 if let PlotElement::Bar(b) = plot {
1593 if b.histogram_bin_edges().is_some() {
1594 continue;
1595 }
1596 let is_x = matches!(b.orientation, crate::plots::bar::Orientation::Vertical);
1597 return Some((is_x, b.labels.clone()));
1598 }
1599 }
1600 None
1601 }
1602
1603 pub fn x_axis_tick_labels_for_axes(&self, axes_index: usize) -> Option<Vec<String>> {
1604 self.axes_metadata
1605 .get(axes_index)
1606 .and_then(|meta| meta.x_tick_labels.clone())
1607 }
1608
1609 pub fn y_axis_tick_labels_for_axes(&self, axes_index: usize) -> Option<Vec<String>> {
1610 self.axes_metadata
1611 .get(axes_index)
1612 .and_then(|meta| meta.y_tick_labels.clone())
1613 }
1614
1615 pub fn histogram_axis_edges_for_axes(&self, axes_index: usize) -> Option<(bool, Vec<f64>)> {
1616 for (plot_idx, plot) in self.plots.iter().enumerate() {
1617 let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
1618 if plot_axes != axes_index {
1619 continue;
1620 }
1621 if let PlotElement::Bar(b) = plot {
1622 if let Some(edges) = b.histogram_bin_edges() {
1623 let is_x = matches!(b.orientation, crate::plots::bar::Orientation::Vertical);
1624 return Some((is_x, edges.to_vec()));
1625 }
1626 }
1627 }
1628 None
1629 }
1630}
1631
1632impl Default for Figure {
1633 fn default() -> Self {
1634 Self::new()
1635 }
1636}
1637
1638fn normalize_reference_range(range: (f64, f64)) -> (f64, f64) {
1639 let (mut lo, mut hi) = range;
1640 if !lo.is_finite() || !hi.is_finite() {
1641 return (0.0, 1.0);
1642 }
1643 if hi < lo {
1644 std::mem::swap(&mut lo, &mut hi);
1645 }
1646 if (hi - lo).abs() < f64::EPSILON {
1647 let pad = lo.abs().max(1.0) * 0.5;
1648 return (lo - pad, hi + pad);
1649 }
1650 (lo, hi)
1651}
1652
1653impl PlotElement {
1654 pub fn is_visible(&self) -> bool {
1656 match self {
1657 PlotElement::Line(plot) => plot.visible,
1658 PlotElement::Scatter(plot) => plot.visible,
1659 PlotElement::Bar(plot) => plot.visible,
1660 PlotElement::ErrorBar(plot) => plot.visible,
1661 PlotElement::Stairs(plot) => plot.visible,
1662 PlotElement::Stem(plot) => plot.visible,
1663 PlotElement::Area(plot) => plot.visible,
1664 PlotElement::Quiver(plot) => plot.visible,
1665 PlotElement::Pie(plot) => plot.visible,
1666 PlotElement::Surface(plot) => plot.visible,
1667 PlotElement::Patch(plot) => plot.is_visible(),
1668 PlotElement::Line3(plot) => plot.visible,
1669 PlotElement::Scatter3(plot) => plot.visible,
1670 PlotElement::Contour(plot) => plot.visible,
1671 PlotElement::ContourFill(plot) => plot.visible,
1672 PlotElement::ReferenceLine(plot) => plot.visible,
1673 }
1674 }
1675
1676 pub fn label(&self) -> Option<String> {
1678 match self {
1679 PlotElement::Line(plot) => plot.label.clone(),
1680 PlotElement::Scatter(plot) => plot.label.clone(),
1681 PlotElement::Bar(plot) => plot.label.clone(),
1682 PlotElement::ErrorBar(plot) => plot.label.clone(),
1683 PlotElement::Stairs(plot) => plot.label.clone(),
1684 PlotElement::Stem(plot) => plot.label.clone(),
1685 PlotElement::Area(plot) => plot.label.clone(),
1686 PlotElement::Quiver(plot) => plot.label.clone(),
1687 PlotElement::Pie(plot) => plot.label.clone(),
1688 PlotElement::Surface(plot) => plot.label.clone(),
1689 PlotElement::Patch(plot) => plot.label().map(str::to_string),
1690 PlotElement::Line3(plot) => plot.label.clone(),
1691 PlotElement::Scatter3(plot) => plot.label.clone(),
1692 PlotElement::Contour(plot) => plot.label.clone(),
1693 PlotElement::ContourFill(plot) => plot.label.clone(),
1694 PlotElement::ReferenceLine(plot) => plot.label_for_legend(),
1695 }
1696 }
1697
1698 pub fn set_label(&mut self, label: Option<String>) {
1700 match self {
1701 PlotElement::Line(plot) => plot.label = label,
1702 PlotElement::Scatter(plot) => plot.label = label,
1703 PlotElement::Bar(plot) => plot.label = label,
1704 PlotElement::ErrorBar(plot) => plot.label = label,
1705 PlotElement::Stairs(plot) => plot.label = label,
1706 PlotElement::Stem(plot) => plot.label = label,
1707 PlotElement::Area(plot) => plot.label = label,
1708 PlotElement::Quiver(plot) => plot.label = label,
1709 PlotElement::Pie(plot) => plot.label = label,
1710 PlotElement::Surface(plot) => plot.label = label,
1711 PlotElement::Patch(plot) => plot.set_label(label),
1712 PlotElement::Line3(plot) => plot.label = label,
1713 PlotElement::Scatter3(plot) => plot.label = label,
1714 PlotElement::Contour(plot) => plot.label = label,
1715 PlotElement::ContourFill(plot) => plot.label = label,
1716 PlotElement::ReferenceLine(plot) => plot.label = label,
1717 }
1718 }
1719
1720 pub fn color(&self) -> Vec4 {
1722 match self {
1723 PlotElement::Line(plot) => plot.color,
1724 PlotElement::Scatter(plot) => plot.color,
1725 PlotElement::Bar(plot) => plot.color,
1726 PlotElement::ErrorBar(plot) => plot.color,
1727 PlotElement::Stairs(plot) => plot.color,
1728 PlotElement::Stem(plot) => plot.color,
1729 PlotElement::Area(plot) => plot.color,
1730 PlotElement::Quiver(plot) => plot.color,
1731 PlotElement::Pie(_plot) => Vec4::new(1.0, 1.0, 1.0, 1.0),
1732 PlotElement::Surface(_plot) => Vec4::new(1.0, 1.0, 1.0, 1.0),
1733 PlotElement::Patch(plot) => plot.effective_face_color(),
1734 PlotElement::Line3(plot) => plot.color,
1735 PlotElement::Scatter3(plot) => plot.colors.first().copied().unwrap_or(Vec4::ONE),
1736 PlotElement::Contour(_plot) => Vec4::new(1.0, 1.0, 1.0, 1.0),
1737 PlotElement::ContourFill(_plot) => Vec4::new(0.9, 0.9, 0.9, 1.0),
1738 PlotElement::ReferenceLine(plot) => plot.color,
1739 }
1740 }
1741
1742 pub fn plot_type(&self) -> PlotType {
1744 match self {
1745 PlotElement::Line(_) => PlotType::Line,
1746 PlotElement::Scatter(_) => PlotType::Scatter,
1747 PlotElement::Bar(_) => PlotType::Bar,
1748 PlotElement::ErrorBar(_) => PlotType::ErrorBar,
1749 PlotElement::Stairs(_) => PlotType::Stairs,
1750 PlotElement::Stem(_) => PlotType::Stem,
1751 PlotElement::Area(_) => PlotType::Area,
1752 PlotElement::Quiver(_) => PlotType::Quiver,
1753 PlotElement::Pie(_) => PlotType::Pie,
1754 PlotElement::Surface(_) => PlotType::Surface,
1755 PlotElement::Patch(_) => PlotType::Patch,
1756 PlotElement::Line3(_) => PlotType::Line3,
1757 PlotElement::Scatter3(_) => PlotType::Scatter3,
1758 PlotElement::Contour(_) => PlotType::Contour,
1759 PlotElement::ContourFill(_) => PlotType::ContourFill,
1760 PlotElement::ReferenceLine(_) => PlotType::ReferenceLine,
1761 }
1762 }
1763
1764 pub fn bounds(&mut self) -> BoundingBox {
1766 match self {
1767 PlotElement::Line(plot) => plot.bounds(),
1768 PlotElement::Scatter(plot) => plot.bounds(),
1769 PlotElement::Bar(plot) => plot.bounds(),
1770 PlotElement::ErrorBar(plot) => plot.bounds(),
1771 PlotElement::Stairs(plot) => plot.bounds(),
1772 PlotElement::Stem(plot) => plot.bounds(),
1773 PlotElement::Area(plot) => plot.bounds(),
1774 PlotElement::Quiver(plot) => plot.bounds(),
1775 PlotElement::Pie(plot) => plot.bounds(),
1776 PlotElement::Surface(plot) => plot.bounds(),
1777 PlotElement::Patch(plot) => plot.bounds(),
1778 PlotElement::Line3(plot) => plot.bounds(),
1779 PlotElement::Scatter3(plot) => plot.bounds(),
1780 PlotElement::Contour(plot) => plot.bounds(),
1781 PlotElement::ContourFill(plot) => plot.bounds(),
1782 PlotElement::ReferenceLine(plot) => plot.coordinate_bounds(),
1783 }
1784 }
1785
1786 pub fn render_data(&mut self) -> RenderData {
1788 match self {
1789 PlotElement::Line(plot) => plot.render_data(),
1790 PlotElement::Scatter(plot) => plot.render_data(),
1791 PlotElement::Bar(plot) => plot.render_data(),
1792 PlotElement::ErrorBar(plot) => plot.render_data(),
1793 PlotElement::Stairs(plot) => plot.render_data(),
1794 PlotElement::Stem(plot) => plot.render_data(),
1795 PlotElement::Area(plot) => plot.render_data(),
1796 PlotElement::Quiver(plot) => plot.render_data(),
1797 PlotElement::Pie(plot) => plot.render_data(),
1798 PlotElement::Surface(plot) => plot.render_data(),
1799 PlotElement::Patch(plot) => plot.render_data(),
1800 PlotElement::Line3(plot) => plot.render_data(),
1801 PlotElement::Scatter3(plot) => plot.render_data(),
1802 PlotElement::Contour(plot) => plot.render_data(),
1803 PlotElement::ContourFill(plot) => plot.render_data(),
1804 PlotElement::ReferenceLine(plot) => {
1805 plot.render_data_with_range((0.0, 1.0), (0.0, 1.0), None)
1806 }
1807 }
1808 }
1809
1810 pub fn estimated_memory_usage(&self) -> usize {
1812 match self {
1813 PlotElement::Line(plot) => plot.estimated_memory_usage(),
1814 PlotElement::Scatter(plot) => plot.estimated_memory_usage(),
1815 PlotElement::Bar(plot) => plot.estimated_memory_usage(),
1816 PlotElement::ErrorBar(plot) => plot.estimated_memory_usage(),
1817 PlotElement::Stairs(plot) => plot.estimated_memory_usage(),
1818 PlotElement::Stem(plot) => plot.estimated_memory_usage(),
1819 PlotElement::Area(plot) => plot.estimated_memory_usage(),
1820 PlotElement::Quiver(plot) => plot.estimated_memory_usage(),
1821 PlotElement::Pie(plot) => plot.estimated_memory_usage(),
1822 PlotElement::Surface(_plot) => 0,
1823 PlotElement::Patch(plot) => plot.estimated_memory_usage(),
1824 PlotElement::Line3(plot) => plot.estimated_memory_usage(),
1825 PlotElement::Scatter3(plot) => plot.estimated_memory_usage(),
1826 PlotElement::Contour(plot) => plot.estimated_memory_usage(),
1827 PlotElement::ContourFill(plot) => plot.estimated_memory_usage(),
1828 PlotElement::ReferenceLine(plot) => plot.estimated_memory_usage(),
1829 }
1830 }
1831}
1832
1833#[derive(Debug)]
1835pub struct FigureStatistics {
1836 pub total_plots: usize,
1837 pub visible_plots: usize,
1838 pub plot_type_counts: HashMap<PlotType, usize>,
1839 pub total_memory_usage: usize,
1840 pub has_legend: bool,
1841}
1842
1843pub mod matlab_compat {
1845 use super::*;
1846 use crate::plots::{LinePlot, ScatterPlot};
1847
1848 pub fn figure() -> Figure {
1850 Figure::new()
1851 }
1852
1853 pub fn figure_with_title<S: Into<String>>(title: S) -> Figure {
1855 Figure::new().with_title(title)
1856 }
1857
1858 pub fn plot_multiple_lines(
1860 figure: &mut Figure,
1861 data_sets: Vec<(Vec<f64>, Vec<f64>, Option<String>)>,
1862 ) -> Result<Vec<usize>, String> {
1863 let mut indices = Vec::new();
1864
1865 for (i, (x, y, label)) in data_sets.into_iter().enumerate() {
1866 let mut line = LinePlot::new(x, y)?;
1867
1868 let colors = [
1870 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), ];
1878 let color = colors[i % colors.len()];
1879 line.set_color(color);
1880
1881 if let Some(label) = label {
1882 line = line.with_label(label);
1883 }
1884
1885 indices.push(figure.add_line_plot(line));
1886 }
1887
1888 Ok(indices)
1889 }
1890
1891 pub fn scatter_multiple(
1893 figure: &mut Figure,
1894 data_sets: Vec<(Vec<f64>, Vec<f64>, Option<String>)>,
1895 ) -> Result<Vec<usize>, String> {
1896 let mut indices = Vec::new();
1897
1898 for (i, (x, y, label)) in data_sets.into_iter().enumerate() {
1899 let mut scatter = ScatterPlot::new(x, y)?;
1900
1901 let colors = [
1903 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), ];
1911 let color = colors[i % colors.len()];
1912 scatter.set_color(color);
1913
1914 if let Some(label) = label {
1915 scatter = scatter.with_label(label);
1916 }
1917
1918 indices.push(figure.add_scatter_plot(scatter));
1919 }
1920
1921 Ok(indices)
1922 }
1923}
1924
1925#[cfg(test)]
1926mod tests {
1927 use super::*;
1928 use crate::plots::line::LineStyle;
1929
1930 #[test]
1931 fn test_figure_creation() {
1932 let figure = Figure::new();
1933
1934 assert_eq!(figure.len(), 0);
1935 assert!(figure.is_empty());
1936 assert!(figure.legend_enabled);
1937 assert!(figure.grid_enabled);
1938 }
1939
1940 #[test]
1941 fn test_figure_styling() {
1942 let figure = Figure::new()
1943 .with_title("Test Figure")
1944 .with_sg_title("Overview")
1945 .with_labels("X Axis", "Y Axis")
1946 .with_legend(false)
1947 .with_grid(false);
1948
1949 assert_eq!(figure.title, Some("Test Figure".to_string()));
1950 assert_eq!(figure.sg_title, Some("Overview".to_string()));
1951 assert_eq!(figure.x_label, Some("X Axis".to_string()));
1952 assert_eq!(figure.y_label, Some("Y Axis".to_string()));
1953 assert!(!figure.legend_enabled);
1954 assert!(!figure.grid_enabled);
1955 }
1956
1957 #[test]
1958 fn test_window_title_follows_name_and_number_title() {
1959 let mut figure = Figure::new();
1960 assert_eq!(figure.window_title(Some(7)), "Figure 7");
1961
1962 figure.set_name("demo");
1963 assert_eq!(figure.window_title(Some(7)), "Figure 7: demo");
1964
1965 figure.set_number_title(false);
1966 assert_eq!(figure.window_title(Some(7)), "demo");
1967
1968 figure.set_name(" ");
1969 assert_eq!(figure.window_title(Some(7)), "RunMat Plot");
1970 }
1971
1972 #[test]
1973 fn test_has_any_titles_tracks_super_and_axes_titles() {
1974 let mut figure = Figure::new();
1975 assert!(!figure.has_any_titles());
1976
1977 figure.set_sg_title("Summary");
1978 assert!(figure.has_any_titles());
1979
1980 figure.clear_sg_title();
1981 assert!(!figure.has_any_titles());
1982
1983 figure.set_axes_title(0, "Panel");
1984 assert!(figure.has_any_titles());
1985 }
1986
1987 #[test]
1988 fn test_multiple_line_plots() {
1989 let mut figure = Figure::new();
1990
1991 let line1 = LinePlot::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 4.0])
1993 .unwrap()
1994 .with_label("Quadratic");
1995 let index1 = figure.add_line_plot(line1);
1996
1997 let line2 = LinePlot::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 2.0])
1999 .unwrap()
2000 .with_style(Vec4::new(1.0, 0.0, 0.0, 1.0), 2.0, LineStyle::Dashed)
2001 .with_label("Linear");
2002 let index2 = figure.add_line_plot(line2);
2003
2004 assert_eq!(figure.len(), 2);
2005 assert_eq!(index1, 0);
2006 assert_eq!(index2, 1);
2007
2008 let legend = figure.legend_entries();
2010 assert_eq!(legend.len(), 2);
2011 assert_eq!(legend[0].label, "Quadratic");
2012 assert_eq!(legend[1].label, "Linear");
2013 }
2014
2015 #[test]
2016 fn test_mixed_plot_types() {
2017 let mut figure = Figure::new();
2018
2019 let line = LinePlot::new(vec![0.0, 1.0, 2.0], vec![1.0, 2.0, 3.0])
2021 .unwrap()
2022 .with_label("Line");
2023 figure.add_line_plot(line);
2024
2025 let scatter = ScatterPlot::new(vec![0.5, 1.5, 2.5], vec![1.5, 2.5, 3.5])
2026 .unwrap()
2027 .with_label("Scatter");
2028 figure.add_scatter_plot(scatter);
2029
2030 let bar = BarChart::new(vec!["A".to_string(), "B".to_string()], vec![2.0, 4.0])
2031 .unwrap()
2032 .with_label("Bar");
2033 figure.add_bar_chart(bar);
2034
2035 assert_eq!(figure.len(), 3);
2036
2037 let render_data = figure.render_data();
2039 assert_eq!(render_data.len(), 3);
2040
2041 let stats = figure.statistics();
2043 assert_eq!(stats.total_plots, 3);
2044 assert_eq!(stats.visible_plots, 3);
2045 assert!(stats.has_legend);
2046 }
2047
2048 #[test]
2049 fn test_plot_visibility() {
2050 let mut figure = Figure::new();
2051
2052 let mut line = LinePlot::new(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
2053 line.set_visible(false); figure.add_line_plot(line);
2055
2056 let scatter = ScatterPlot::new(vec![0.0, 1.0], vec![1.0, 2.0]).unwrap();
2057 figure.add_scatter_plot(scatter);
2058
2059 let render_data = figure.render_data();
2061 assert_eq!(render_data.len(), 1);
2062
2063 let stats = figure.statistics();
2064 assert_eq!(stats.total_plots, 2);
2065 assert_eq!(stats.visible_plots, 1);
2066 }
2067
2068 #[test]
2069 fn test_bounds_computation() {
2070 let mut figure = Figure::new();
2071
2072 let line = LinePlot::new(vec![-1.0, 0.0, 1.0], vec![-2.0, 0.0, 2.0]).unwrap();
2074 figure.add_line_plot(line);
2075
2076 let scatter = ScatterPlot::new(vec![2.0, 3.0, 4.0], vec![1.0, 3.0, 5.0]).unwrap();
2077 figure.add_scatter_plot(scatter);
2078
2079 let bounds = figure.bounds();
2080
2081 assert!(bounds.min.x <= -1.0);
2083 assert!(bounds.max.x >= 4.0);
2084 assert!(bounds.min.y <= -2.0);
2085 assert!(bounds.max.y >= 5.0);
2086 }
2087
2088 #[test]
2089 fn test_reference_line_only_bounds_use_default_span() {
2090 let mut vertical_figure = Figure::new();
2091 vertical_figure.add_reference_line_on_axes(
2092 ReferenceLine::new(ReferenceLineOrientation::Vertical, 2.0).unwrap(),
2093 0,
2094 );
2095 let vertical_bounds = vertical_figure.bounds();
2096 assert_eq!(vertical_bounds.min.x, 1.5);
2097 assert_eq!(vertical_bounds.max.x, 2.5);
2098 assert_eq!(vertical_bounds.min.y, 0.0);
2099 assert_eq!(vertical_bounds.max.y, 1.0);
2100
2101 let mut horizontal_figure = Figure::new();
2102 horizontal_figure.add_reference_line_on_axes(
2103 ReferenceLine::new(ReferenceLineOrientation::Horizontal, 3.0).unwrap(),
2104 0,
2105 );
2106 let horizontal_bounds = horizontal_figure.bounds();
2107 assert_eq!(horizontal_bounds.min.x, 0.0);
2108 assert_eq!(horizontal_bounds.max.x, 1.0);
2109 assert_eq!(horizontal_bounds.min.y, 2.5);
2110 assert_eq!(horizontal_bounds.max.y, 3.5);
2111 }
2112
2113 #[test]
2114 fn test_reference_line_render_data_prefers_figure_limits() {
2115 let mut horizontal_figure = Figure::new().with_limits((-2.0, 8.0), (-10.0, 10.0));
2116 horizontal_figure.axes_metadata[0].x_limits = Some((0.0, 1.0));
2117 horizontal_figure.add_reference_line_on_axes(
2118 ReferenceLine::new(ReferenceLineOrientation::Horizontal, 3.0).unwrap(),
2119 0,
2120 );
2121 let horizontal_bounds = horizontal_figure.render_data()[0].bounds.unwrap();
2122 assert_eq!(horizontal_bounds.min.x, -2.0);
2123 assert_eq!(horizontal_bounds.max.x, 8.0);
2124 assert_eq!(horizontal_bounds.min.y, 3.0);
2125 assert_eq!(horizontal_bounds.max.y, 3.0);
2126
2127 let mut vertical_figure = Figure::new().with_limits((-2.0, 8.0), (-10.0, 10.0));
2128 vertical_figure.axes_metadata[0].y_limits = Some((0.0, 1.0));
2129 vertical_figure.add_reference_line_on_axes(
2130 ReferenceLine::new(ReferenceLineOrientation::Vertical, 4.0).unwrap(),
2131 0,
2132 );
2133 let vertical_bounds = vertical_figure.render_data()[0].bounds.unwrap();
2134 assert_eq!(vertical_bounds.min.x, 4.0);
2135 assert_eq!(vertical_bounds.max.x, 4.0);
2136 assert_eq!(vertical_bounds.min.y, -10.0);
2137 assert_eq!(vertical_bounds.max.y, 10.0);
2138 }
2139
2140 #[test]
2141 fn test_matlab_compat_multiple_lines() {
2142 use super::matlab_compat::*;
2143
2144 let mut figure = figure_with_title("Multiple Lines Test");
2145
2146 let data_sets = vec![
2147 (
2148 vec![0.0, 1.0, 2.0],
2149 vec![0.0, 1.0, 4.0],
2150 Some("Quadratic".to_string()),
2151 ),
2152 (
2153 vec![0.0, 1.0, 2.0],
2154 vec![0.0, 1.0, 2.0],
2155 Some("Linear".to_string()),
2156 ),
2157 (
2158 vec![0.0, 1.0, 2.0],
2159 vec![1.0, 1.0, 1.0],
2160 Some("Constant".to_string()),
2161 ),
2162 ];
2163
2164 let indices = plot_multiple_lines(&mut figure, data_sets).unwrap();
2165
2166 assert_eq!(indices.len(), 3);
2167 assert_eq!(figure.len(), 3);
2168
2169 let legend = figure.legend_entries();
2171 assert_eq!(legend.len(), 3);
2172 assert_ne!(legend[0].color, legend[1].color);
2173 assert_ne!(legend[1].color, legend[2].color);
2174 }
2175
2176 #[test]
2177 fn axes_metadata_and_labels_are_isolated_per_subplot() {
2178 let mut figure = Figure::new();
2179 figure.set_subplot_grid(1, 2);
2180 figure.set_axes_title(0, "Left Title");
2181 figure.set_axes_xlabel(0, "Left X");
2182 figure.set_axes_ylabel(0, "Left Y");
2183 figure.set_axes_title(1, "Right Title");
2184 figure.set_axes_style(
2185 1,
2186 TextStyle {
2187 font_size: Some(14.0),
2188 ..Default::default()
2189 },
2190 );
2191 figure.set_axes_legend_enabled(0, false);
2192 figure.set_axes_legend_style(
2193 1,
2194 LegendStyle {
2195 location: Some("southwest".into()),
2196 ..Default::default()
2197 },
2198 );
2199
2200 assert_eq!(
2201 figure.axes_metadata(0).and_then(|m| m.title.as_deref()),
2202 Some("Left Title")
2203 );
2204 assert_eq!(
2205 figure.axes_metadata(1).and_then(|m| m.title.as_deref()),
2206 Some("Right Title")
2207 );
2208 assert_eq!(
2209 figure.axes_metadata(0).and_then(|m| m.x_label.as_deref()),
2210 Some("Left X")
2211 );
2212 assert_eq!(
2213 figure.axes_metadata(0).and_then(|m| m.y_label.as_deref()),
2214 Some("Left Y")
2215 );
2216 assert!(!figure.axes_metadata(0).unwrap().legend_enabled);
2217 assert_eq!(
2218 figure
2219 .axes_metadata(1)
2220 .unwrap()
2221 .legend_style
2222 .location
2223 .as_deref(),
2224 Some("southwest")
2225 );
2226 assert_eq!(figure.axes_metadata(0).unwrap().axes_style.font_size, None);
2227 assert_eq!(
2228 figure.axes_metadata(1).unwrap().axes_style.font_size,
2229 Some(14.0)
2230 );
2231 }
2232
2233 #[test]
2234 fn set_labels_for_axes_only_updates_target_subplot() {
2235 let mut figure = Figure::new();
2236 figure.set_subplot_grid(1, 2);
2237 figure.add_line_plot_on_axes(
2238 LinePlot::new(vec![0.0, 1.0], vec![1.0, 2.0])
2239 .unwrap()
2240 .with_label("L0"),
2241 0,
2242 );
2243 figure.add_line_plot_on_axes(
2244 LinePlot::new(vec![0.0, 1.0], vec![2.0, 3.0])
2245 .unwrap()
2246 .with_label("R0"),
2247 1,
2248 );
2249 figure.set_labels_for_axes(1, &["Right Only".into()]);
2250
2251 let left_entries = figure.legend_entries_for_axes(0);
2252 let right_entries = figure.legend_entries_for_axes(1);
2253 assert_eq!(left_entries[0].label, "L0");
2254 assert_eq!(right_entries[0].label, "Right Only");
2255 }
2256
2257 #[test]
2258 fn axes_log_modes_are_isolated_per_subplot() {
2259 let mut figure = Figure::new();
2260 figure.set_subplot_grid(1, 2);
2261 figure.set_axes_log_modes(1, true, false);
2262
2263 assert!(!figure.axes_metadata(0).unwrap().x_log);
2264 assert!(!figure.axes_metadata(0).unwrap().y_log);
2265 assert!(figure.axes_metadata(1).unwrap().x_log);
2266 assert!(!figure.axes_metadata(1).unwrap().y_log);
2267
2268 figure.set_active_axes_index(1);
2269 assert!(figure.x_log);
2270 assert!(!figure.y_log);
2271 }
2272
2273 #[test]
2274 fn z_label_and_view_state_are_isolated_per_subplot() {
2275 let mut figure = Figure::new();
2276 figure.set_subplot_grid(1, 2);
2277 figure.set_axes_zlabel(1, "Height");
2278 figure.set_axes_view(1, 45.0, 20.0);
2279
2280 assert_eq!(figure.axes_metadata(0).unwrap().z_label, None);
2281 assert_eq!(
2282 figure.axes_metadata(1).unwrap().z_label.as_deref(),
2283 Some("Height")
2284 );
2285 assert_eq!(
2286 figure.axes_metadata(1).unwrap().view_azimuth_deg,
2287 Some(45.0)
2288 );
2289 assert_eq!(
2290 figure.axes_metadata(1).unwrap().view_elevation_deg,
2291 Some(20.0)
2292 );
2293 }
2294
2295 #[test]
2296 fn axes_view_revision_advances_for_each_explicit_view_update() {
2297 let mut figure = Figure::new();
2298
2299 assert_eq!(figure.axes_metadata(0).unwrap().view_revision, 0);
2300
2301 figure.set_axes_view(0, 45.0, 20.0);
2302 assert_eq!(figure.axes_metadata(0).unwrap().view_revision, 1);
2303
2304 figure.set_axes_view(0, 45.0, 20.0);
2305 assert_eq!(figure.axes_metadata(0).unwrap().view_revision, 2);
2306 }
2307
2308 #[test]
2309 fn pie_legend_entries_are_slice_based() {
2310 let mut figure = Figure::new();
2311 let pie = PieChart::new(vec![1.0, 2.0], None)
2312 .unwrap()
2313 .with_slice_labels(vec!["A".into(), "B".into()]);
2314 figure.add_pie_chart(pie);
2315 let entries = figure.legend_entries_for_axes(0);
2316 assert_eq!(entries.len(), 2);
2317 assert_eq!(entries[0].label, "A");
2318 assert_eq!(entries[1].label, "B");
2319 }
2320
2321 #[test]
2322 fn histogram_bars_do_not_use_categorical_axis_labels() {
2323 let mut figure = Figure::new();
2324 let mut bar = BarChart::new(vec!["a".into(), "b".into()], vec![2.0, 3.0]).unwrap();
2325 bar.set_histogram_bin_edges(vec![0.0, 0.5, 1.0]);
2326 figure.add_bar_chart(bar);
2327
2328 assert!(figure.categorical_axis_labels().is_none());
2329 assert_eq!(
2330 figure.histogram_axis_edges_for_axes(0),
2331 Some((true, vec![0.0, 0.5, 1.0]))
2332 );
2333 }
2334
2335 #[test]
2336 fn plain_bar_charts_keep_categorical_axis_labels() {
2337 let mut figure = Figure::new();
2338 let bar = BarChart::new(vec!["A".into(), "B".into()], vec![1.0, 2.0]).unwrap();
2339 figure.add_bar_chart(bar);
2340
2341 assert_eq!(
2342 figure.categorical_axis_labels(),
2343 Some((true, vec!["A".to_string(), "B".to_string()]))
2344 );
2345 }
2346
2347 #[test]
2348 fn line3_contributes_to_3d_bounds_and_metadata() {
2349 let mut figure = Figure::new();
2350 let line3 = Line3Plot::new(vec![0.0, 1.0], vec![1.0, 2.0], vec![2.0, 4.0])
2351 .unwrap()
2352 .with_label("Trajectory");
2353 figure.add_line3_plot(line3);
2354 let bounds = figure.bounds();
2355 assert_eq!(bounds.min.z, 2.0);
2356 assert_eq!(bounds.max.z, 4.0);
2357 let entries = figure.legend_entries_for_axes(0);
2358 assert_eq!(entries[0].plot_type, PlotType::Line3);
2359 }
2360
2361 #[test]
2362 fn stem_render_data_includes_marker_pass() {
2363 let mut figure = Figure::new();
2364 figure.add_stem_plot(StemPlot::new(vec![0.0, 1.0], vec![1.0, 2.0]).unwrap());
2365
2366 let render_data = figure.render_data();
2367 assert_eq!(render_data.len(), 2);
2368 assert_eq!(
2369 render_data[0].pipeline_type,
2370 crate::core::PipelineType::Lines
2371 );
2372 assert_eq!(
2373 render_data[1].pipeline_type,
2374 crate::core::PipelineType::Points
2375 );
2376 }
2377
2378 #[test]
2379 fn errorbar_render_data_includes_marker_pass() {
2380 let mut figure = Figure::new();
2381 figure.add_errorbar(
2382 ErrorBar::new_vertical(
2383 vec![0.0, 1.0],
2384 vec![1.0, 2.0],
2385 vec![0.1, 0.2],
2386 vec![0.1, 0.2],
2387 )
2388 .unwrap(),
2389 );
2390
2391 let render_data = figure.render_data();
2392 assert_eq!(render_data.len(), 2);
2393 assert_eq!(
2394 render_data[0].pipeline_type,
2395 crate::core::PipelineType::Lines
2396 );
2397 assert_eq!(
2398 render_data[1].pipeline_type,
2399 crate::core::PipelineType::Points
2400 );
2401 }
2402
2403 #[test]
2404 fn subplot_sensitive_axes_state_is_isolated_per_subplot() {
2405 let mut figure = Figure::new();
2406 figure.set_subplot_grid(1, 2);
2407 figure.set_axes_limits(1, Some((1.0, 2.0)), Some((3.0, 4.0)));
2408 figure.set_axes_z_limits(1, Some((5.0, 6.0)));
2409 figure.set_axes_grid_enabled(1, false);
2410 figure.set_axes_minor_grid_enabled(1, true);
2411 figure.set_axes_box_enabled(1, false);
2412 figure.set_axes_axis_equal(1, true);
2413 figure.set_axes_colorbar_enabled(1, true);
2414 figure.set_axes_colormap(1, ColorMap::Hot);
2415 figure.set_axes_color_limits(1, Some((0.0, 10.0)));
2416
2417 let left = figure.axes_metadata(0).unwrap();
2418 let right = figure.axes_metadata(1).unwrap();
2419 assert_eq!(left.x_limits, None);
2420 assert_eq!(right.x_limits, Some((1.0, 2.0)));
2421 assert!(!left.minor_grid_enabled);
2422 assert!(!left.minor_grid_explicit);
2423 assert!(!right.grid_enabled);
2424 assert!(right.minor_grid_enabled);
2425 assert!(right.minor_grid_explicit);
2426 assert!(!right.box_enabled);
2427 assert!(right.axis_equal);
2428 assert!(right.colorbar_enabled);
2429 assert_eq!(format!("{:?}", right.colormap), "Hot");
2430 assert_eq!(right.color_limits, Some((0.0, 10.0)));
2431 }
2432
2433 #[test]
2434 fn active_axes_sync_does_not_clobber_figure_minor_grid_default() {
2435 let mut figure = Figure::new();
2436 figure.set_subplot_grid(1, 2);
2437 figure.minor_grid_enabled = true;
2438
2439 assert!(figure.minor_grid_enabled_for_axes(0));
2440 assert!(figure.minor_grid_enabled_for_axes(1));
2441
2442 figure.set_active_axes_index(1);
2443
2444 assert!(figure.minor_grid_enabled);
2445 assert!(figure.minor_grid_enabled_for_axes(0));
2446 assert!(figure.minor_grid_enabled_for_axes(1));
2447
2448 figure.set_axes_minor_grid_enabled(1, false);
2449
2450 assert!(figure.minor_grid_enabled);
2451 assert!(figure.minor_grid_enabled_for_axes(0));
2452 assert!(!figure.minor_grid_enabled_for_axes(1));
2453 }
2454}