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