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