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 position: [f64; 4],
31 pub title: Option<String>,
32 pub sg_title: Option<String>,
33 pub x_label: Option<String>,
34 pub y_label: Option<String>,
35 pub z_label: Option<String>,
36 pub legend_enabled: bool,
37 pub grid_enabled: bool,
38 pub minor_grid_enabled: bool,
39 pub box_enabled: bool,
40 pub background_color: Vec4,
41
42 pub x_limits: Option<(f64, f64)>,
44 pub y_limits: Option<(f64, f64)>,
45 pub z_limits: Option<(f64, f64)>,
46
47 pub x_log: bool,
49 pub y_log: bool,
50
51 pub axis_equal: bool,
53
54 pub colormap: ColorMap,
56 pub colorbar_enabled: bool,
57
58 pub color_limits: Option<(f64, f64)>,
60
61 bounds: Option<BoundingBox>,
63 dirty: bool,
64
65 pub axes_rows: usize,
67 pub axes_cols: usize,
68 plot_axes_indices: Vec<usize>,
70
71 pub active_axes_index: usize,
73
74 pub axes_metadata: Vec<AxesMetadata>,
76 pub sg_title_style: TextStyle,
77}
78
79#[derive(Debug, Clone)]
80pub struct TextStyle {
81 pub color: Option<Vec4>,
82 pub font_size: Option<f32>,
83 pub font_weight: Option<String>,
84 pub font_angle: Option<String>,
85 pub interpreter: Option<String>,
86 pub visible: bool,
87}
88
89impl Default for TextStyle {
90 fn default() -> Self {
91 Self {
92 color: None,
93 font_size: None,
94 font_weight: None,
95 font_angle: None,
96 interpreter: None,
97 visible: true,
98 }
99 }
100}
101
102#[derive(Debug, Clone)]
103pub struct LegendStyle {
104 pub location: Option<String>,
105 pub visible: bool,
106 pub font_size: Option<f32>,
107 pub font_weight: Option<String>,
108 pub font_angle: Option<String>,
109 pub interpreter: Option<String>,
110 pub box_visible: Option<bool>,
111 pub orientation: Option<String>,
112 pub text_color: Option<Vec4>,
113}
114
115impl Default for LegendStyle {
116 fn default() -> Self {
117 Self {
118 location: None,
119 visible: true,
120 font_size: None,
121 font_weight: None,
122 font_angle: None,
123 interpreter: None,
124 box_visible: None,
125 orientation: None,
126 text_color: None,
127 }
128 }
129}
130
131#[derive(Debug, Clone, Default)]
132pub struct AxesMetadata {
133 pub axes_kind: AxesKind,
134 pub overlay_parent: Option<usize>,
135 pub y_axis_location: String,
136 pub position: [f64; 4],
137 pub position_explicit: bool,
138 pub units: String,
139 pub title: Option<String>,
140 pub subtitle: Option<String>,
141 pub x_label: Option<String>,
142 pub y_label: Option<String>,
143 pub z_label: Option<String>,
144 pub x_ticks: Option<Vec<f64>>,
145 pub y_ticks: Option<Vec<f64>>,
146 pub x_tick_labels: Option<Vec<String>>,
147 pub y_tick_labels: Option<Vec<String>>,
148 pub x_tick_format: Option<String>,
149 pub y_tick_format: Option<String>,
150 pub x_tick_label_rotation: Option<f64>,
151 pub y_tick_label_rotation: Option<f64>,
152 pub x_limits: Option<(f64, f64)>,
153 pub y_limits: Option<(f64, f64)>,
154 pub z_limits: Option<(f64, f64)>,
155 pub x_log: bool,
156 pub y_log: bool,
157 pub view_azimuth_deg: Option<f32>,
158 pub view_elevation_deg: Option<f32>,
159 pub view_revision: u64,
160 pub grid_enabled: bool,
161 pub minor_grid_enabled: bool,
162 pub minor_grid_explicit: bool,
163 pub hidden_line_removal: bool,
164 pub box_enabled: bool,
165 pub axis_equal: bool,
166 pub data_aspect_ratio: [f64; 3],
167 pub data_aspect_ratio_mode: String,
168 pub legend_enabled: bool,
169 pub colorbar_enabled: bool,
170 pub colormap: ColorMap,
171 pub color_order: Option<Vec<Vec4>>,
172 pub color_limits: Option<(f64, f64)>,
173 pub axes_style: TextStyle,
174 pub title_style: TextStyle,
175 pub subtitle_style: TextStyle,
176 pub x_label_style: TextStyle,
177 pub y_label_style: TextStyle,
178 pub z_label_style: TextStyle,
179 pub legend_style: LegendStyle,
180 pub world_text_annotations: Vec<TextAnnotation>,
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
184pub enum AxesKind {
185 #[default]
186 Cartesian,
187 Polar,
188}
189
190#[derive(Debug, Clone)]
191pub struct TextAnnotation {
192 pub position: glam::Vec3,
193 pub text: String,
194 pub style: TextStyle,
195}
196
197#[derive(Debug, Clone)]
199pub enum PlotElement {
200 Line(LinePlot),
201 Scatter(ScatterPlot),
202 Bar(BarChart),
203 ErrorBar(Box<ErrorBar>),
204 Stairs(StairsPlot),
205 Stem(StemPlot),
206 Area(AreaPlot),
207 Quiver(QuiverPlot),
208 Pie(PieChart),
209 Surface(SurfacePlot),
210 Mesh(Box<MeshPlot>),
211 Patch(PatchPlot),
212 Line3(Line3Plot),
213 Scatter3(Scatter3Plot),
214 Contour(ContourPlot),
215 ContourFill(ContourFillPlot),
216 ReferenceLine(ReferenceLine),
217}
218
219#[derive(Debug, Clone)]
221pub struct LegendEntry {
222 pub label: String,
223 pub color: Vec4,
224 pub plot_type: PlotType,
225}
226
227#[derive(Debug, Clone)]
228pub struct PieLabelEntry {
229 pub label: String,
230 pub position: glam::Vec2,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
235pub enum PlotType {
236 Line,
237 Scatter,
238 Bar,
239 ErrorBar,
240 Stairs,
241 Stem,
242 Area,
243 Quiver,
244 Pie,
245 Surface,
246 Mesh,
247 Patch,
248 Line3,
249 Scatter3,
250 Contour,
251 ContourFill,
252 ReferenceLine,
253}
254
255impl Figure {
256 fn default_axes_metadata() -> AxesMetadata {
257 AxesMetadata {
258 axes_kind: AxesKind::Cartesian,
259 overlay_parent: None,
260 y_axis_location: "left".into(),
261 position: [0.13, 0.11, 0.775, 0.815],
262 position_explicit: false,
263 units: "normalized".into(),
264 x_limits: None,
265 y_limits: None,
266 z_limits: None,
267 grid_enabled: true,
268 minor_grid_enabled: false,
269 hidden_line_removal: true,
270 box_enabled: true,
271 axis_equal: false,
272 data_aspect_ratio: [1.0, 1.0, 1.0],
273 data_aspect_ratio_mode: "auto".into(),
274 legend_enabled: true,
275 colorbar_enabled: false,
276 colormap: ColorMap::Parula,
277 color_limits: None,
278 ..Default::default()
279 }
280 }
281
282 pub fn new() -> Self {
284 Self {
285 plots: Vec::new(),
286 name: None,
287 number_title: true,
288 visible: true,
289 position: [0.0, 0.0, 560.0, 420.0],
290 title: None,
291 sg_title: None,
292 x_label: None,
293 y_label: None,
294 z_label: None,
295 legend_enabled: true,
296 grid_enabled: true,
297 minor_grid_enabled: false,
298 box_enabled: true,
299 background_color: Vec4::new(1.0, 1.0, 1.0, 1.0), x_limits: None,
301 y_limits: None,
302 z_limits: None,
303 x_log: false,
304 y_log: false,
305 axis_equal: false,
306 colormap: ColorMap::Parula,
307 colorbar_enabled: false,
308 color_limits: None,
309 bounds: None,
310 dirty: true,
311 axes_rows: 1,
312 axes_cols: 1,
313 plot_axes_indices: Vec::new(),
314 active_axes_index: 0,
315 axes_metadata: vec![Self::default_axes_metadata()],
316 sg_title_style: TextStyle::default(),
317 }
318 }
319
320 fn ensure_axes_metadata_capacity(&mut self, min_len: usize) {
321 while self.axes_metadata.len() < min_len.max(1) {
322 self.axes_metadata.push(Self::default_axes_metadata());
323 }
324 }
325
326 fn sync_legacy_fields_from_active_axes(&mut self) {
327 self.ensure_axes_metadata_capacity(self.active_axes_index + 1);
328 if let Some(meta) = self.axes_metadata.get(self.active_axes_index).cloned() {
329 self.title = meta.title;
330 self.x_label = meta.x_label;
331 self.y_label = meta.y_label;
332 self.z_label = meta.z_label;
333 self.x_limits = meta.x_limits;
334 self.y_limits = meta.y_limits;
335 self.z_limits = meta.z_limits;
336 self.x_log = meta.x_log;
337 self.y_log = meta.y_log;
338 self.grid_enabled = meta.grid_enabled;
339 self.box_enabled = meta.box_enabled;
340 self.axis_equal = meta.axis_equal;
341 self.legend_enabled = meta.legend_enabled;
342 self.colorbar_enabled = meta.colorbar_enabled;
343 self.colormap = meta.colormap.clone();
344 self.color_limits = meta.color_limits;
345 }
346 }
347
348 pub fn set_active_axes_index(&mut self, axes_index: usize) {
349 self.ensure_axes_metadata_capacity(axes_index + 1);
350 self.active_axes_index = axes_index;
351 self.sync_legacy_fields_from_active_axes();
352 self.dirty = true;
353 }
354
355 pub fn ensure_axes(&mut self, axes_index: usize) {
356 self.ensure_axes_metadata_capacity(axes_index + 1);
357 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
358 meta.axes_kind = AxesKind::Cartesian;
359 meta.overlay_parent = None;
360 }
361 self.dirty = true;
362 }
363
364 pub fn axes_metadata(&self, axes_index: usize) -> Option<&AxesMetadata> {
365 self.axes_metadata.get(axes_index)
366 }
367
368 pub fn active_axes_metadata(&self) -> Option<&AxesMetadata> {
369 self.axes_metadata(self.active_axes_index)
370 }
371
372 pub fn with_sg_title<S: Into<String>>(mut self, title: S) -> Self {
373 self.set_sg_title(title);
374 self
375 }
376
377 pub fn set_sg_title<S: Into<String>>(&mut self, title: S) {
378 self.sg_title = Some(title.into());
379 self.dirty = true;
380 }
381
382 pub fn clear_sg_title(&mut self) {
383 self.sg_title = None;
384 self.dirty = true;
385 }
386
387 pub fn set_sg_title_style(&mut self, style: TextStyle) {
388 self.sg_title_style = style;
389 self.dirty = true;
390 }
391
392 pub fn set_name<S: Into<String>>(&mut self, name: S) {
393 self.name = Some(name.into());
394 self.dirty = true;
395 }
396
397 pub fn set_number_title(&mut self, enabled: bool) {
398 self.number_title = enabled;
399 self.dirty = true;
400 }
401
402 pub fn set_visible(&mut self, visible: bool) {
403 self.visible = visible;
404 self.dirty = true;
405 }
406
407 pub fn set_position(&mut self, position: [f64; 4]) {
408 self.position = position;
409 self.dirty = true;
410 }
411
412 pub fn window_title(&self, handle: Option<u32>) -> String {
413 let name = self.name.as_deref().map(str::trim).unwrap_or_default();
414 let numbered = if self.number_title {
415 handle.filter(|h| *h > 0).map(|h| format!("Figure {h}"))
416 } else {
417 None
418 };
419 match (numbered, name.is_empty()) {
420 (Some(numbered), false) => format!("{numbered}: {name}"),
421 (Some(numbered), true) => numbered,
422 (None, false) => name.to_string(),
423 (None, true) => "RunMat Plot".to_string(),
424 }
425 }
426
427 pub fn has_any_titles(&self) -> bool {
428 let non_empty = |s: Option<&str>| s.map(str::trim).is_some_and(|t| !t.is_empty());
429 non_empty(self.sg_title.as_deref())
430 || non_empty(self.title.as_deref())
431 || self
432 .axes_metadata
433 .iter()
434 .any(|meta| non_empty(meta.title.as_deref()) || non_empty(meta.subtitle.as_deref()))
435 }
436
437 pub fn with_title<S: Into<String>>(mut self, title: S) -> Self {
439 self.set_title(title);
440 self
441 }
442
443 pub fn set_title<S: Into<String>>(&mut self, title: S) {
445 self.set_axes_title(self.active_axes_index, title);
446 }
447
448 pub fn with_labels<S: Into<String>>(mut self, x_label: S, y_label: S) -> Self {
450 self.set_axis_labels(x_label, y_label);
451 self
452 }
453
454 pub fn set_axis_labels<S: Into<String>>(&mut self, x_label: S, y_label: S) {
456 self.set_axes_labels(self.active_axes_index, x_label, y_label);
457 self.dirty = true;
458 }
459
460 pub fn set_axes_title<S: Into<String>>(&mut self, axes_index: usize, title: S) {
461 self.ensure_axes_metadata_capacity(axes_index + 1);
462 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
463 meta.title = Some(title.into());
464 }
465 if axes_index == self.active_axes_index {
466 self.sync_legacy_fields_from_active_axes();
467 }
468 self.dirty = true;
469 }
470
471 pub fn set_axes_subtitle<S: Into<String>>(&mut self, axes_index: usize, subtitle: S) {
472 self.ensure_axes_metadata_capacity(axes_index + 1);
473 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
474 meta.subtitle = Some(subtitle.into());
475 }
476 self.dirty = true;
477 }
478
479 pub fn set_axes_xlabel<S: Into<String>>(&mut self, axes_index: usize, label: S) {
480 self.ensure_axes_metadata_capacity(axes_index + 1);
481 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
482 meta.x_label = Some(label.into());
483 }
484 if axes_index == self.active_axes_index {
485 self.sync_legacy_fields_from_active_axes();
486 }
487 self.dirty = true;
488 }
489
490 pub fn set_axes_ylabel<S: Into<String>>(&mut self, axes_index: usize, label: S) {
491 self.ensure_axes_metadata_capacity(axes_index + 1);
492 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
493 meta.y_label = Some(label.into());
494 }
495 if axes_index == self.active_axes_index {
496 self.sync_legacy_fields_from_active_axes();
497 }
498 self.dirty = true;
499 }
500
501 pub fn set_axes_zlabel<S: Into<String>>(&mut self, axes_index: usize, label: S) {
502 self.ensure_axes_metadata_capacity(axes_index + 1);
503 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
504 meta.z_label = Some(label.into());
505 }
506 if axes_index == self.active_axes_index {
507 self.sync_legacy_fields_from_active_axes();
508 }
509 self.dirty = true;
510 }
511
512 pub fn add_axes_text_annotation<S: Into<String>>(
513 &mut self,
514 axes_index: usize,
515 position: glam::Vec3,
516 text: S,
517 style: TextStyle,
518 ) -> usize {
519 self.ensure_axes_metadata_capacity(axes_index + 1);
520 let Some(meta) = self.axes_metadata.get_mut(axes_index) else {
521 return 0;
522 };
523 meta.world_text_annotations.push(TextAnnotation {
524 position,
525 text: text.into(),
526 style,
527 });
528 self.dirty = true;
529 meta.world_text_annotations.len() - 1
530 }
531
532 pub fn axes_text_annotation(
533 &self,
534 axes_index: usize,
535 annotation_index: usize,
536 ) -> Option<&TextAnnotation> {
537 self.axes_metadata
538 .get(axes_index)
539 .and_then(|meta| meta.world_text_annotations.get(annotation_index))
540 }
541
542 pub fn set_axes_text_annotation_text<S: Into<String>>(
543 &mut self,
544 axes_index: usize,
545 annotation_index: usize,
546 text: S,
547 ) {
548 if let Some(annotation) = self
549 .axes_metadata
550 .get_mut(axes_index)
551 .and_then(|meta| meta.world_text_annotations.get_mut(annotation_index))
552 {
553 annotation.text = text.into();
554 self.dirty = true;
555 }
556 }
557
558 pub fn set_axes_text_annotation_position(
559 &mut self,
560 axes_index: usize,
561 annotation_index: usize,
562 position: glam::Vec3,
563 ) {
564 if let Some(annotation) = self
565 .axes_metadata
566 .get_mut(axes_index)
567 .and_then(|meta| meta.world_text_annotations.get_mut(annotation_index))
568 {
569 annotation.position = position;
570 self.dirty = true;
571 }
572 }
573
574 pub fn set_axes_text_annotation_style(
575 &mut self,
576 axes_index: usize,
577 annotation_index: usize,
578 style: TextStyle,
579 ) {
580 if let Some(annotation) = self
581 .axes_metadata
582 .get_mut(axes_index)
583 .and_then(|meta| meta.world_text_annotations.get_mut(annotation_index))
584 {
585 annotation.style = style;
586 self.dirty = true;
587 }
588 }
589
590 pub fn axes_text_annotations(&self, axes_index: usize) -> &[TextAnnotation] {
591 self.axes_metadata
592 .get(axes_index)
593 .map(|meta| meta.world_text_annotations.as_slice())
594 .unwrap_or(&[])
595 }
596
597 pub fn set_axes_labels<S: Into<String>>(&mut self, axes_index: usize, x_label: S, y_label: S) {
598 self.ensure_axes_metadata_capacity(axes_index + 1);
599 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
600 meta.x_label = Some(x_label.into());
601 meta.y_label = Some(y_label.into());
602 }
603 if axes_index == self.active_axes_index {
604 self.sync_legacy_fields_from_active_axes();
605 }
606 self.dirty = true;
607 }
608
609 pub fn set_axes_tick_labels(
610 &mut self,
611 axes_index: usize,
612 x_labels: Option<Vec<String>>,
613 y_labels: Option<Vec<String>>,
614 ) {
615 self.ensure_axes_metadata_capacity(axes_index + 1);
616 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
617 meta.x_tick_labels = x_labels;
618 meta.y_tick_labels = y_labels;
619 }
620 self.dirty = true;
621 }
622
623 pub fn set_axes_tick_formats(
624 &mut self,
625 axes_index: usize,
626 x_format: Option<String>,
627 y_format: Option<String>,
628 ) {
629 self.ensure_axes_metadata_capacity(axes_index + 1);
630 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
631 meta.x_tick_format = x_format;
632 meta.y_tick_format = y_format;
633 }
634 self.dirty = true;
635 }
636
637 pub fn set_axes_tick_label_rotations(
638 &mut self,
639 axes_index: usize,
640 x_angle: Option<f64>,
641 y_angle: Option<f64>,
642 ) {
643 self.ensure_axes_metadata_capacity(axes_index + 1);
644 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
645 meta.x_tick_label_rotation = x_angle;
646 meta.y_tick_label_rotation = y_angle;
647 }
648 self.dirty = true;
649 }
650
651 pub fn set_axes_ticks(
652 &mut self,
653 axes_index: usize,
654 x_ticks: Option<Vec<f64>>,
655 y_ticks: Option<Vec<f64>>,
656 ) {
657 self.ensure_axes_metadata_capacity(axes_index + 1);
658 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
659 meta.x_ticks = x_ticks;
660 meta.y_ticks = y_ticks;
661 }
662 self.dirty = true;
663 }
664
665 pub fn set_axes_style(&mut self, axes_index: usize, style: TextStyle) {
666 self.ensure_axes_metadata_capacity(axes_index + 1);
667 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
668 meta.axes_style = style;
669 }
670 self.dirty = true;
671 }
672
673 pub fn set_axes_title_style(&mut self, axes_index: usize, style: TextStyle) {
674 self.ensure_axes_metadata_capacity(axes_index + 1);
675 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
676 meta.title_style = style;
677 }
678 self.dirty = true;
679 }
680
681 pub fn set_axes_subtitle_style(&mut self, axes_index: usize, style: TextStyle) {
682 self.ensure_axes_metadata_capacity(axes_index + 1);
683 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
684 meta.subtitle_style = style;
685 }
686 self.dirty = true;
687 }
688
689 pub fn set_axes_xlabel_style(&mut self, axes_index: usize, style: TextStyle) {
690 self.ensure_axes_metadata_capacity(axes_index + 1);
691 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
692 meta.x_label_style = style;
693 }
694 self.dirty = true;
695 }
696
697 pub fn set_axes_ylabel_style(&mut self, axes_index: usize, style: TextStyle) {
698 self.ensure_axes_metadata_capacity(axes_index + 1);
699 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
700 meta.y_label_style = style;
701 }
702 self.dirty = true;
703 }
704
705 pub fn set_axes_zlabel_style(&mut self, axes_index: usize, style: TextStyle) {
706 self.ensure_axes_metadata_capacity(axes_index + 1);
707 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
708 meta.z_label_style = style;
709 }
710 self.dirty = true;
711 }
712
713 pub fn with_limits(mut self, x_limits: (f64, f64), y_limits: (f64, f64)) -> Self {
715 self.x_limits = Some(x_limits);
716 self.y_limits = Some(y_limits);
717 self.dirty = true;
718 self
719 }
720
721 pub fn with_legend(mut self, enabled: bool) -> Self {
723 self.set_legend(enabled);
724 self
725 }
726
727 pub fn set_legend(&mut self, enabled: bool) {
728 self.set_axes_legend_enabled(self.active_axes_index, enabled);
729 }
730
731 pub fn set_axes_legend_enabled(&mut self, axes_index: usize, enabled: bool) {
732 self.ensure_axes_metadata_capacity(axes_index + 1);
733 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
734 meta.legend_enabled = enabled;
735 }
736 if axes_index == self.active_axes_index {
737 self.sync_legacy_fields_from_active_axes();
738 }
739 self.dirty = true;
740 }
741
742 pub fn set_axes_legend_style(&mut self, axes_index: usize, style: LegendStyle) {
743 self.ensure_axes_metadata_capacity(axes_index + 1);
744 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
745 meta.legend_style = style;
746 }
747 self.dirty = true;
748 }
749
750 pub fn set_axes_log_modes(&mut self, axes_index: usize, x_log: bool, y_log: bool) {
751 self.ensure_axes_metadata_capacity(axes_index + 1);
752 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
753 meta.x_log = x_log;
754 meta.y_log = y_log;
755 }
756 if axes_index == self.active_axes_index {
757 self.sync_legacy_fields_from_active_axes();
758 }
759 self.dirty = true;
760 }
761
762 pub fn set_axes_view(&mut self, axes_index: usize, azimuth_deg: f32, elevation_deg: f32) {
763 self.ensure_axes_metadata_capacity(axes_index + 1);
764 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
765 meta.view_azimuth_deg = Some(azimuth_deg);
766 meta.view_elevation_deg = Some(elevation_deg);
767 meta.view_revision = meta.view_revision.wrapping_add(1);
768 }
769 self.dirty = true;
770 }
771
772 pub fn with_grid(mut self, enabled: bool) -> Self {
774 self.set_grid(enabled);
775 self
776 }
777
778 pub fn set_grid(&mut self, enabled: bool) {
779 self.set_axes_grid_enabled(self.active_axes_index, enabled);
780 self.dirty = true;
781 }
782
783 pub fn set_axes_grid_enabled(&mut self, axes_index: usize, enabled: bool) {
784 self.ensure_axes_metadata_capacity(axes_index + 1);
785 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
786 meta.grid_enabled = enabled;
787 }
788 if axes_index == self.active_axes_index {
789 self.sync_legacy_fields_from_active_axes();
790 }
791 self.dirty = true;
792 }
793
794 pub fn set_axes_kind(&mut self, axes_index: usize, axes_kind: AxesKind) {
795 self.ensure_axes_metadata_capacity(axes_index + 1);
796 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
797 meta.axes_kind = axes_kind;
798 }
799 if axes_index == self.active_axes_index {
800 self.sync_legacy_fields_from_active_axes();
801 }
802 self.dirty = true;
803 }
804
805 pub fn axes_kind(&self, axes_index: usize) -> AxesKind {
806 self.axes_metadata(axes_index)
807 .map(|meta| meta.axes_kind)
808 .unwrap_or(AxesKind::Cartesian)
809 }
810
811 pub fn with_minor_grid(mut self, enabled: bool) -> Self {
812 self.set_minor_grid(enabled);
813 self
814 }
815
816 pub fn set_minor_grid(&mut self, enabled: bool) {
817 self.set_axes_minor_grid_enabled(self.active_axes_index, enabled);
818 self.dirty = true;
819 }
820
821 pub fn set_axes_minor_grid_enabled(&mut self, axes_index: usize, enabled: bool) {
822 self.ensure_axes_metadata_capacity(axes_index + 1);
823 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
824 meta.minor_grid_enabled = enabled;
825 meta.minor_grid_explicit = true;
826 }
827 if axes_index == self.active_axes_index {
828 self.sync_legacy_fields_from_active_axes();
829 }
830 self.dirty = true;
831 }
832
833 pub fn minor_grid_enabled_for_axes(&self, axes_index: usize) -> bool {
834 self.axes_metadata(axes_index)
835 .map(|meta| {
836 if meta.minor_grid_explicit {
837 meta.minor_grid_enabled
838 } else {
839 self.minor_grid_enabled
840 }
841 })
842 .unwrap_or(self.minor_grid_enabled)
843 }
844
845 pub fn with_background_color(mut self, color: Vec4) -> Self {
847 self.set_background_color(color);
848 self
849 }
850
851 pub fn set_background_color(&mut self, color: Vec4) {
852 self.background_color = color;
853 self.dirty = true;
854 }
855
856 pub fn with_xlog(mut self, enabled: bool) -> Self {
858 self.set_axes_log_modes(self.active_axes_index, enabled, self.y_log);
859 self
860 }
861 pub fn with_ylog(mut self, enabled: bool) -> Self {
862 self.set_axes_log_modes(self.active_axes_index, self.x_log, enabled);
863 self
864 }
865 pub fn with_axis_equal(mut self, enabled: bool) -> Self {
866 self.set_axis_equal(enabled);
867 self
868 }
869
870 pub fn set_axis_equal(&mut self, enabled: bool) {
871 self.set_axes_axis_equal(self.active_axes_index, enabled);
872 self.dirty = true;
873 }
874 pub fn set_axes_axis_equal(&mut self, axes_index: usize, enabled: bool) {
875 self.ensure_axes_metadata_capacity(axes_index + 1);
876 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
877 meta.axis_equal = enabled;
878 meta.data_aspect_ratio_mode = if enabled { "manual" } else { "auto" }.into();
879 if enabled {
880 meta.data_aspect_ratio = [1.0, 1.0, 1.0];
881 }
882 }
883 if axes_index == self.active_axes_index {
884 self.sync_legacy_fields_from_active_axes();
885 }
886 self.dirty = true;
887 }
888
889 pub fn set_axes_data_aspect_ratio(
890 &mut self,
891 axes_index: usize,
892 ratio: [f64; 3],
893 mode: impl Into<String>,
894 ) {
895 self.ensure_axes_metadata_capacity(axes_index + 1);
896 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
897 meta.data_aspect_ratio = ratio;
898 meta.data_aspect_ratio_mode = mode.into();
899 meta.axis_equal = meta.data_aspect_ratio_mode == "manual" && ratio == [1.0, 1.0, 1.0];
900 }
901 if axes_index == self.active_axes_index {
902 self.sync_legacy_fields_from_active_axes();
903 }
904 self.dirty = true;
905 }
906 pub fn with_colormap(mut self, cmap: ColorMap) -> Self {
907 self.set_axes_colormap(self.active_axes_index, cmap);
908 self
909 }
910 pub fn with_colorbar(mut self, enabled: bool) -> Self {
911 self.set_axes_colorbar_enabled(self.active_axes_index, enabled);
912 self
913 }
914 pub fn with_color_limits(mut self, limits: Option<(f64, f64)>) -> Self {
915 self.set_axes_color_limits(self.active_axes_index, limits);
916 self
917 }
918
919 pub fn with_subplot_grid(mut self, rows: usize, cols: usize) -> Self {
921 self.set_subplot_grid(rows, cols);
922 self
923 }
924
925 pub fn axes_grid(&self) -> (usize, usize) {
927 (self.axes_rows, self.axes_cols)
928 }
929
930 pub fn axes_count(&self) -> usize {
931 let plotted_axes = self
932 .plot_axes_indices
933 .iter()
934 .copied()
935 .max()
936 .map(|index| index + 1)
937 .unwrap_or(0);
938 let overlay_axes = self
939 .axes_metadata
940 .iter()
941 .enumerate()
942 .filter(|(_, meta)| meta.overlay_parent.is_some())
943 .map(|(index, _)| index + 1)
944 .max()
945 .unwrap_or(0);
946 self.total_axes()
947 .max(plotted_axes)
948 .max(overlay_axes)
949 .max(self.axes_metadata.len())
950 .max(1)
951 }
952
953 pub fn ensure_overlay_axes(&mut self, parent_axes_index: usize) -> usize {
954 let parent = parent_axes_index.min(self.total_axes().saturating_sub(1));
955 if let Some((index, _)) = self
956 .axes_metadata
957 .iter()
958 .enumerate()
959 .find(|(_, meta)| meta.overlay_parent == Some(parent))
960 {
961 return index;
962 }
963
964 let index = self.axes_metadata.len().max(self.total_axes());
965 self.ensure_axes_metadata_capacity(index + 1);
966 if let Some(parent_meta) = self.axes_metadata.get(parent).cloned() {
967 if let Some(meta) = self.axes_metadata.get_mut(index) {
968 *meta = parent_meta;
969 meta.overlay_parent = Some(parent);
970 meta.y_axis_location = "right".into();
971 meta.y_limits = None;
972 meta.y_ticks = None;
973 meta.y_tick_labels = None;
974 meta.y_tick_format = None;
975 meta.y_label = None;
976 meta.legend_enabled = false;
977 meta.grid_enabled = false;
978 meta.minor_grid_enabled = false;
979 }
980 }
981 self.dirty = true;
982 index
983 }
984
985 pub fn axes_overlay_parent(&self, axes_index: usize) -> Option<usize> {
986 self.axes_metadata
987 .get(axes_index)
988 .and_then(|meta| meta.overlay_parent)
989 }
990
991 pub fn set_axes_y_axis_location(&mut self, axes_index: usize, location: impl Into<String>) {
992 self.ensure_axes_metadata_capacity(axes_index + 1);
993 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
994 meta.y_axis_location = location.into();
995 }
996 self.dirty = true;
997 }
998
999 pub fn set_axes_position(&mut self, axes_index: usize, position: [f64; 4]) {
1000 self.ensure_axes_metadata_capacity(axes_index + 1);
1001 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1002 meta.position = position;
1003 meta.position_explicit = true;
1004 }
1005 self.dirty = true;
1006 }
1007
1008 pub fn set_axes_units(&mut self, axes_index: usize, units: impl Into<String>) {
1009 self.ensure_axes_metadata_capacity(axes_index + 1);
1010 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1011 meta.units = units.into();
1012 }
1013 self.dirty = true;
1014 }
1015
1016 pub fn plot_axes_indices(&self) -> &[usize] {
1018 &self.plot_axes_indices
1019 }
1020
1021 pub fn assign_plot_to_axes(
1023 &mut self,
1024 plot_index: usize,
1025 axes_index: usize,
1026 ) -> Result<(), String> {
1027 if plot_index >= self.plot_axes_indices.len() {
1028 return Err(format!(
1029 "assign_plot_to_axes: index {plot_index} out of bounds"
1030 ));
1031 }
1032 let max_axes = self.axes_count();
1033 let ai = axes_index.min(max_axes.saturating_sub(1));
1034 self.plot_axes_indices[plot_index] = ai;
1035 self.dirty = true;
1036 Ok(())
1037 }
1038 pub fn set_subplot_grid(&mut self, rows: usize, cols: usize) {
1040 self.axes_rows = rows.max(1);
1041 self.axes_cols = cols.max(1);
1042 let grid_axes = self.axes_rows * self.axes_cols;
1043 self.ensure_axes_metadata_capacity(grid_axes);
1044 for axes_index in 0..grid_axes {
1045 if self
1046 .axes_metadata
1047 .get(axes_index)
1048 .is_some_and(|meta| meta.overlay_parent.is_some())
1049 {
1050 self.clear_axes(axes_index);
1051 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1052 *meta = Self::default_axes_metadata();
1053 }
1054 }
1055 }
1056 self.active_axes_index = self.active_axes_index.min(
1057 self.axes_rows
1058 .saturating_mul(self.axes_cols)
1059 .saturating_sub(1),
1060 );
1061 self.sync_legacy_fields_from_active_axes();
1062 self.dirty = true;
1063 }
1064
1065 pub fn set_color_limits(&mut self, limits: Option<(f64, f64)>) {
1067 self.set_axes_color_limits(self.active_axes_index, limits);
1068 self.dirty = true;
1069 }
1070
1071 pub fn set_z_limits(&mut self, limits: Option<(f64, f64)>) {
1072 self.set_axes_z_limits(self.active_axes_index, limits);
1073 self.dirty = true;
1074 }
1075
1076 pub fn set_axes_limits(
1077 &mut self,
1078 axes_index: usize,
1079 x: Option<(f64, f64)>,
1080 y: Option<(f64, f64)>,
1081 ) {
1082 self.ensure_axes_metadata_capacity(axes_index + 1);
1083 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1084 meta.x_limits = x;
1085 meta.y_limits = y;
1086 }
1087 if axes_index == self.active_axes_index {
1088 self.sync_legacy_fields_from_active_axes();
1089 }
1090 self.dirty = true;
1091 }
1092
1093 pub fn set_axes_z_limits(&mut self, axes_index: usize, limits: Option<(f64, f64)>) {
1094 self.ensure_axes_metadata_capacity(axes_index + 1);
1095 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1096 meta.z_limits = limits;
1097 }
1098 if axes_index == self.active_axes_index {
1099 self.sync_legacy_fields_from_active_axes();
1100 }
1101 self.dirty = true;
1102 }
1103
1104 pub fn set_axes_box_enabled(&mut self, axes_index: usize, enabled: bool) {
1105 self.ensure_axes_metadata_capacity(axes_index + 1);
1106 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1107 meta.box_enabled = enabled;
1108 }
1109 if axes_index == self.active_axes_index {
1110 self.sync_legacy_fields_from_active_axes();
1111 }
1112 self.dirty = true;
1113 }
1114
1115 pub fn set_axes_hidden_line_removal(&mut self, axes_index: usize, enabled: bool) {
1116 self.ensure_axes_metadata_capacity(axes_index + 1);
1117 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1118 meta.hidden_line_removal = enabled;
1119 }
1120 self.dirty = true;
1121 }
1122
1123 pub fn set_axes_colorbar_enabled(&mut self, axes_index: usize, enabled: bool) {
1124 self.ensure_axes_metadata_capacity(axes_index + 1);
1125 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1126 meta.colorbar_enabled = enabled;
1127 }
1128 if axes_index == self.active_axes_index {
1129 self.sync_legacy_fields_from_active_axes();
1130 }
1131 self.dirty = true;
1132 }
1133
1134 pub fn set_axes_colormap(&mut self, axes_index: usize, cmap: ColorMap) {
1135 self.ensure_axes_metadata_capacity(axes_index + 1);
1136 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1137 meta.colormap = cmap.clone();
1138 }
1139 for (idx, plot) in self.plots.iter_mut().enumerate() {
1140 if self.plot_axes_indices.get(idx).copied().unwrap_or(0) != axes_index {
1141 continue;
1142 }
1143 if let PlotElement::Surface(surface) = plot {
1144 *surface = surface.clone().with_colormap(cmap.clone());
1145 }
1146 }
1147 if axes_index == self.active_axes_index {
1148 self.sync_legacy_fields_from_active_axes();
1149 }
1150 self.dirty = true;
1151 }
1152
1153 pub fn set_axes_color_limits(&mut self, axes_index: usize, limits: Option<(f64, f64)>) {
1154 self.ensure_axes_metadata_capacity(axes_index + 1);
1155 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1156 meta.color_limits = limits;
1157 }
1158 for (idx, plot) in self.plots.iter_mut().enumerate() {
1159 if self.plot_axes_indices.get(idx).copied().unwrap_or(0) != axes_index {
1160 continue;
1161 }
1162 if let PlotElement::Surface(surface) = plot {
1163 surface.set_color_limits(limits);
1164 }
1165 }
1166 if axes_index == self.active_axes_index {
1167 self.sync_legacy_fields_from_active_axes();
1168 }
1169 self.dirty = true;
1170 }
1171
1172 pub fn set_axes_color_order(&mut self, axes_index: usize, colors: Vec<Vec4>) {
1173 self.ensure_axes_metadata_capacity(axes_index + 1);
1174 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1175 meta.color_order = Some(colors.clone());
1176 }
1177 self.recolor_axes_plots(axes_index, &colors);
1178 self.dirty = true;
1179 }
1180
1181 pub fn set_all_axes_color_order(&mut self, colors: Vec<Vec4>) {
1182 let total_axes = self.total_axes().max(1);
1183 self.ensure_axes_metadata_capacity(total_axes);
1184 for axes_index in 0..total_axes {
1185 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1186 meta.color_order = Some(colors.clone());
1187 }
1188 self.recolor_axes_plots(axes_index, &colors);
1189 }
1190 self.dirty = true;
1191 }
1192
1193 fn recolor_axes_plots(&mut self, axes_index: usize, colors: &[Vec4]) {
1194 if colors.is_empty() {
1195 return;
1196 }
1197 let mut series = 0usize;
1198 for (plot_index, plot) in self.plots.iter_mut().enumerate() {
1199 if self.plot_axes_indices.get(plot_index).copied().unwrap_or(0) != axes_index {
1200 continue;
1201 }
1202 let color = colors[series % colors.len()];
1203 match plot {
1204 PlotElement::Line(line) => {
1205 line.set_color(color);
1206 series += 1;
1207 }
1208 PlotElement::Scatter(scatter) => {
1209 scatter.set_color(color);
1210 scatter.set_edge_color(color);
1211 series += 1;
1212 }
1213 PlotElement::Bar(bar) => {
1214 bar.set_color(color);
1215 series += 1;
1216 }
1217 PlotElement::Stairs(stairs) => {
1218 stairs.color = color;
1219 series += 1;
1220 }
1221 PlotElement::Stem(stem) => {
1222 stem.color = color;
1223 series += 1;
1224 }
1225 PlotElement::Area(area) => {
1226 area.color = Vec4::new(color.x, color.y, color.z, area.color.w);
1227 series += 1;
1228 }
1229 PlotElement::ErrorBar(errorbar) => {
1230 errorbar.color = color;
1231 series += 1;
1232 }
1233 PlotElement::Quiver(quiver) => {
1234 quiver.color = color;
1235 series += 1;
1236 }
1237 PlotElement::Line3(line) => {
1238 line.color = color;
1239 series += 1;
1240 }
1241 PlotElement::Scatter3(scatter) => {
1242 *scatter = scatter.clone().with_color(color);
1243 scatter.edge_color = color;
1244 series += 1;
1245 }
1246 _ => {}
1247 }
1248 }
1249 }
1250
1251 fn total_axes(&self) -> usize {
1252 self.axes_rows.max(1) * self.axes_cols.max(1)
1253 }
1254
1255 fn normalize_axes_index(&self, axes_index: usize) -> usize {
1256 let total = self.axes_count();
1257 axes_index.min(total - 1)
1258 }
1259
1260 fn push_plot(&mut self, element: PlotElement, axes_index: usize) -> usize {
1261 let idx = self.normalize_axes_index(axes_index);
1262 self.plots.push(element);
1263 self.plot_axes_indices.push(idx);
1264 self.dirty = true;
1265 self.plots.len() - 1
1266 }
1267
1268 pub fn add_plot_element_on_axes(&mut self, element: PlotElement, axes_index: usize) -> usize {
1270 self.push_plot(element, axes_index)
1271 }
1272
1273 pub fn add_line_plot(&mut self, plot: LinePlot) -> usize {
1275 self.add_line_plot_on_axes(plot, 0)
1276 }
1277
1278 pub fn add_line_plot_on_axes(&mut self, plot: LinePlot, axes_index: usize) -> usize {
1279 self.push_plot(PlotElement::Line(plot), axes_index)
1280 }
1281
1282 pub fn add_reference_line_on_axes(&mut self, plot: ReferenceLine, axes_index: usize) -> usize {
1283 self.push_plot(PlotElement::ReferenceLine(plot), axes_index)
1284 }
1285
1286 pub fn add_scatter_plot(&mut self, plot: ScatterPlot) -> usize {
1288 self.add_scatter_plot_on_axes(plot, 0)
1289 }
1290
1291 pub fn add_scatter_plot_on_axes(&mut self, plot: ScatterPlot, axes_index: usize) -> usize {
1292 self.push_plot(PlotElement::Scatter(plot), axes_index)
1293 }
1294
1295 pub fn add_bar_chart(&mut self, plot: BarChart) -> usize {
1297 self.add_bar_chart_on_axes(plot, 0)
1298 }
1299
1300 pub fn add_bar_chart_on_axes(&mut self, plot: BarChart, axes_index: usize) -> usize {
1301 self.push_plot(PlotElement::Bar(plot), axes_index)
1302 }
1303
1304 pub fn add_errorbar(&mut self, plot: ErrorBar) -> usize {
1306 self.add_errorbar_on_axes(plot, 0)
1307 }
1308
1309 pub fn add_errorbar_on_axes(&mut self, plot: ErrorBar, axes_index: usize) -> usize {
1310 self.push_plot(PlotElement::ErrorBar(Box::new(plot)), axes_index)
1311 }
1312
1313 pub fn add_stairs_plot(&mut self, plot: StairsPlot) -> usize {
1315 self.add_stairs_plot_on_axes(plot, 0)
1316 }
1317
1318 pub fn add_stairs_plot_on_axes(&mut self, plot: StairsPlot, axes_index: usize) -> usize {
1319 self.push_plot(PlotElement::Stairs(plot), axes_index)
1320 }
1321
1322 pub fn add_stem_plot(&mut self, plot: StemPlot) -> usize {
1324 self.add_stem_plot_on_axes(plot, 0)
1325 }
1326
1327 pub fn add_stem_plot_on_axes(&mut self, plot: StemPlot, axes_index: usize) -> usize {
1328 self.push_plot(PlotElement::Stem(plot), axes_index)
1329 }
1330
1331 pub fn add_area_plot(&mut self, plot: AreaPlot) -> usize {
1333 self.add_area_plot_on_axes(plot, 0)
1334 }
1335
1336 pub fn add_area_plot_on_axes(&mut self, plot: AreaPlot, axes_index: usize) -> usize {
1337 self.push_plot(PlotElement::Area(plot), axes_index)
1338 }
1339
1340 pub fn add_quiver_plot(&mut self, plot: QuiverPlot) -> usize {
1341 self.add_quiver_plot_on_axes(plot, 0)
1342 }
1343
1344 pub fn add_quiver_plot_on_axes(&mut self, plot: QuiverPlot, axes_index: usize) -> usize {
1345 self.push_plot(PlotElement::Quiver(plot), axes_index)
1346 }
1347
1348 pub fn add_pie_chart(&mut self, plot: PieChart) -> usize {
1349 self.add_pie_chart_on_axes(plot, 0)
1350 }
1351
1352 pub fn add_pie_chart_on_axes(&mut self, plot: PieChart, axes_index: usize) -> usize {
1353 self.push_plot(PlotElement::Pie(plot), axes_index)
1354 }
1355
1356 pub fn add_surface_plot(&mut self, plot: SurfacePlot) -> usize {
1358 self.add_surface_plot_on_axes(plot, 0)
1359 }
1360
1361 pub fn add_surface_plot_on_axes(&mut self, plot: SurfacePlot, axes_index: usize) -> usize {
1362 self.push_plot(PlotElement::Surface(plot), axes_index)
1363 }
1364
1365 pub fn add_patch_plot(&mut self, plot: PatchPlot) -> usize {
1366 self.add_patch_plot_on_axes(plot, 0)
1367 }
1368
1369 pub fn add_patch_plot_on_axes(&mut self, plot: PatchPlot, axes_index: usize) -> usize {
1370 self.push_plot(PlotElement::Patch(plot), axes_index)
1371 }
1372
1373 pub fn add_mesh_plot(&mut self, plot: MeshPlot) -> usize {
1374 self.add_mesh_plot_on_axes(plot, 0)
1375 }
1376
1377 pub fn add_mesh_plot_on_axes(&mut self, plot: MeshPlot, axes_index: usize) -> usize {
1378 self.push_plot(PlotElement::Mesh(Box::new(plot)), axes_index)
1379 }
1380
1381 pub fn add_line3_plot(&mut self, plot: Line3Plot) -> usize {
1382 self.add_line3_plot_on_axes(plot, self.active_axes_index)
1383 }
1384
1385 pub fn add_line3_plot_on_axes(&mut self, plot: Line3Plot, axes_index: usize) -> usize {
1386 self.push_plot(PlotElement::Line3(plot), axes_index)
1387 }
1388
1389 pub fn add_scatter3_plot(&mut self, plot: Scatter3Plot) -> usize {
1391 self.add_scatter3_plot_on_axes(plot, 0)
1392 }
1393
1394 pub fn add_scatter3_plot_on_axes(&mut self, plot: Scatter3Plot, axes_index: usize) -> usize {
1395 self.push_plot(PlotElement::Scatter3(plot), axes_index)
1396 }
1397
1398 pub fn add_contour_plot(&mut self, plot: ContourPlot) -> usize {
1399 self.add_contour_plot_on_axes(plot, 0)
1400 }
1401
1402 pub fn add_contour_plot_on_axes(&mut self, plot: ContourPlot, axes_index: usize) -> usize {
1403 self.push_plot(PlotElement::Contour(plot), axes_index)
1404 }
1405
1406 pub fn add_contour_fill_plot(&mut self, plot: ContourFillPlot) -> usize {
1407 self.add_contour_fill_plot_on_axes(plot, 0)
1408 }
1409
1410 pub fn add_contour_fill_plot_on_axes(
1411 &mut self,
1412 plot: ContourFillPlot,
1413 axes_index: usize,
1414 ) -> usize {
1415 self.push_plot(PlotElement::ContourFill(plot), axes_index)
1416 }
1417
1418 pub fn remove_plot(&mut self, index: usize) -> Result<(), String> {
1420 if index >= self.plots.len() {
1421 return Err(format!("Plot index {index} out of bounds"));
1422 }
1423 self.plots.remove(index);
1424 self.plot_axes_indices.remove(index);
1425 self.dirty = true;
1426 Ok(())
1427 }
1428
1429 pub fn clear(&mut self) {
1431 self.plots.clear();
1432 self.plot_axes_indices.clear();
1433 self.dirty = true;
1434 }
1435
1436 pub fn clear_axes(&mut self, axes_index: usize) {
1438 let mut i = 0usize;
1439 while i < self.plots.len() {
1440 let ax = *self.plot_axes_indices.get(i).unwrap_or(&0);
1441 if ax == axes_index {
1442 self.plots.remove(i);
1443 self.plot_axes_indices.remove(i);
1444 } else {
1445 i += 1;
1446 }
1447 }
1448 self.ensure_axes_metadata_capacity(axes_index + 1);
1449 if let Some(meta) = self.axes_metadata.get_mut(axes_index) {
1450 meta.world_text_annotations.clear();
1451 }
1452 self.dirty = true;
1453 }
1454
1455 pub fn len(&self) -> usize {
1457 self.plots.len()
1458 }
1459
1460 pub fn is_empty(&self) -> bool {
1462 self.plots.is_empty()
1463 }
1464
1465 pub fn plots(&self) -> impl Iterator<Item = &PlotElement> {
1467 self.plots.iter()
1468 }
1469
1470 pub fn get_plot_mut(&mut self, index: usize) -> Option<&mut PlotElement> {
1472 self.dirty = true;
1473 self.plots.get_mut(index)
1474 }
1475
1476 pub fn bounds(&mut self) -> BoundingBox {
1478 if self.dirty || self.bounds.is_none() {
1479 self.compute_bounds();
1480 }
1481 self.bounds.unwrap()
1482 }
1483
1484 pub fn data_bounds_for_axes(&mut self, axes_index: usize) -> BoundingBox {
1490 let axes_index = self.normalize_axes_index(axes_index);
1491 let mut combined_bounds = None;
1492
1493 for (plot_index, plot) in self.plots.iter_mut().enumerate() {
1494 if self.plot_axes_indices.get(plot_index).copied().unwrap_or(0) != axes_index {
1495 continue;
1496 }
1497 if !plot.is_visible() || matches!(plot, PlotElement::ReferenceLine(_)) {
1498 continue;
1499 }
1500
1501 let plot_bounds = plot.bounds();
1502 combined_bounds = match combined_bounds {
1503 None => Some(plot_bounds),
1504 Some(existing) => Some(existing.union(&plot_bounds)),
1505 };
1506 }
1507
1508 combined_bounds.unwrap_or_default()
1509 }
1510
1511 fn compute_bounds(&mut self) {
1513 if self.plots.is_empty() {
1514 self.bounds = Some(BoundingBox::default());
1515 return;
1516 }
1517
1518 let mut combined_bounds = None;
1519 let mut reference_lines = Vec::new();
1520
1521 for plot in &mut self.plots {
1522 if !plot.is_visible() {
1523 continue;
1524 }
1525 if let PlotElement::ReferenceLine(reference_line) = plot {
1526 reference_lines.push(reference_line.clone());
1527 continue;
1528 }
1529
1530 let plot_bounds = plot.bounds();
1531
1532 combined_bounds = match combined_bounds {
1533 None => Some(plot_bounds),
1534 Some(existing) => Some(existing.union(&plot_bounds)),
1535 };
1536 }
1537
1538 for line in reference_lines {
1539 let mut point_bounds = line.coordinate_bounds();
1540 if let Some(existing) = combined_bounds {
1541 match line.orientation {
1542 ReferenceLineOrientation::Vertical => {
1543 point_bounds.min.y = existing.min.y;
1544 point_bounds.max.y = existing.max.y;
1545 }
1546 ReferenceLineOrientation::Horizontal => {
1547 point_bounds.min.x = existing.min.x;
1548 point_bounds.max.x = existing.max.x;
1549 }
1550 }
1551 } else {
1552 let (x_range, y_range) =
1553 Self::reference_line_ranges(self.x_limits, self.y_limits, None, None, &line);
1554 point_bounds.min.x = x_range.0 as f32;
1555 point_bounds.max.x = x_range.1 as f32;
1556 point_bounds.min.y = y_range.0 as f32;
1557 point_bounds.max.y = y_range.1 as f32;
1558 }
1559 combined_bounds = match combined_bounds {
1560 None => Some(point_bounds),
1561 Some(existing) => Some(existing.union(&point_bounds)),
1562 };
1563 }
1564
1565 self.bounds = combined_bounds.or_else(|| Some(BoundingBox::default()));
1566 self.dirty = false;
1567 }
1568
1569 pub fn render_data(&mut self) -> Vec<RenderData> {
1571 self.render_data_with_viewport(None)
1572 }
1573
1574 pub fn render_data_with_viewport(
1580 &mut self,
1581 viewport_px: Option<(u32, u32)>,
1582 ) -> Vec<RenderData> {
1583 self.render_data_with_viewport_and_gpu(viewport_px, None)
1584 }
1585
1586 pub fn render_data_with_viewport_and_gpu(
1587 &mut self,
1588 viewport_px: Option<(u32, u32)>,
1589 gpu: Option<&GpuPackContext<'_>>,
1590 ) -> Vec<RenderData> {
1591 self.render_data_with_axes_with_viewport_and_gpu(viewport_px, None, None, gpu)
1592 .into_iter()
1593 .map(|(_, render_data)| render_data)
1594 .collect()
1595 }
1596
1597 pub fn render_data_with_axes_with_viewport_and_gpu(
1598 &mut self,
1599 viewport_px: Option<(u32, u32)>,
1600 axes_viewports_px: Option<&[(u32, u32)]>,
1601 axes_view_bounds: Option<PerAxesViewBoundsRef<'_>>,
1602 gpu: Option<&GpuPackContext<'_>>,
1603 ) -> Vec<(usize, RenderData)> {
1604 fn push_with_optional_markers(
1605 out: &mut Vec<(usize, RenderData)>,
1606 axes_index: usize,
1607 render_data: RenderData,
1608 marker_data: Option<RenderData>,
1609 ) {
1610 out.push((axes_index, render_data));
1611 if let Some(marker_data) = marker_data {
1612 out.push((axes_index, marker_data));
1613 }
1614 }
1615
1616 let reference_base_bounds = self.reference_base_bounds_by_axes();
1617 let mut out = Vec::new();
1618 for (plot_idx, p) in self.plots.iter_mut().enumerate() {
1619 if !p.is_visible() {
1620 continue;
1621 }
1622 let axes_index = self.plot_axes_indices.get(plot_idx).copied().unwrap_or(0);
1623 let axes_view_bounds = axes_view_bounds
1624 .and_then(|bounds| bounds.get(axes_index).copied())
1625 .flatten();
1626 let hidden_line_removal = self
1627 .axes_metadata
1628 .get(axes_index)
1629 .map(|meta| meta.hidden_line_removal)
1630 .unwrap_or(true);
1631 if let PlotElement::Surface(s) = p {
1632 if let Some(meta) = self.axes_metadata.get(axes_index) {
1633 s.set_color_limits(meta.color_limits);
1634 *s = s.clone().with_colormap(meta.colormap.clone());
1635 }
1636 }
1637
1638 match p {
1639 PlotElement::Line(plot) => {
1640 let axes_viewport_px = axes_viewports_px
1641 .and_then(|viewports| viewports.get(axes_index).copied())
1642 .or(viewport_px);
1643 trace!(
1644 target: "runmat_plot",
1645 "figure: render_data line viewport_px={:?} axes_index={} axes_viewport_px={:?} axes_view_bounds={:?} gpu_ctx_present={} gpu_line_inputs_present={} gpu_vertices_present={}",
1646 viewport_px,
1647 axes_index,
1648 axes_viewport_px,
1649 axes_view_bounds,
1650 gpu.is_some(),
1651 plot.has_gpu_line_inputs(),
1652 plot.has_gpu_vertices()
1653 );
1654 push_with_optional_markers(
1655 &mut out,
1656 axes_index,
1657 plot.render_data_with_viewport_gpu(axes_viewport_px, axes_view_bounds, gpu),
1658 plot.marker_render_data(),
1659 );
1660 }
1661 PlotElement::ErrorBar(plot) => {
1662 push_with_optional_markers(
1663 &mut out,
1664 axes_index,
1665 plot.render_data_with_viewport_gpu(
1666 axes_viewports_px
1667 .and_then(|viewports| viewports.get(axes_index).copied())
1668 .or(viewport_px),
1669 gpu,
1670 ),
1671 plot.marker_render_data(),
1672 );
1673 }
1674 PlotElement::Stairs(plot) => {
1675 push_with_optional_markers(
1676 &mut out,
1677 axes_index,
1678 plot.render_data_with_viewport(
1679 axes_viewports_px
1680 .and_then(|viewports| viewports.get(axes_index).copied())
1681 .or(viewport_px),
1682 ),
1683 plot.marker_render_data(),
1684 );
1685 }
1686 PlotElement::Stem(plot) => {
1687 push_with_optional_markers(
1688 &mut out,
1689 axes_index,
1690 plot.render_data_with_viewport(
1691 axes_viewports_px
1692 .and_then(|viewports| viewports.get(axes_index).copied())
1693 .or(viewport_px),
1694 ),
1695 plot.marker_render_data(),
1696 );
1697 }
1698 PlotElement::Contour(plot) => out.push((
1699 axes_index,
1700 plot.render_data_with_viewport(
1701 axes_viewports_px
1702 .and_then(|viewports| viewports.get(axes_index).copied())
1703 .or(viewport_px),
1704 ),
1705 )),
1706 PlotElement::ReferenceLine(plot) => {
1707 let (x_range, y_range) = Self::reference_line_ranges(
1708 self.x_limits,
1709 self.y_limits,
1710 self.axes_metadata.get(axes_index),
1711 reference_base_bounds.get(axes_index).copied().flatten(),
1712 plot,
1713 );
1714 out.push((
1715 axes_index,
1716 plot.render_data_with_range(
1717 x_range,
1718 y_range,
1719 axes_viewports_px
1720 .and_then(|viewports| viewports.get(axes_index).copied())
1721 .or(viewport_px),
1722 ),
1723 ));
1724 }
1725 PlotElement::Patch(plot) => {
1726 out.push((axes_index, plot.render_data()));
1727 if let Some(edge_data) = plot.edge_render_data_with_viewport(
1728 axes_viewports_px
1729 .and_then(|viewports| viewports.get(axes_index).copied())
1730 .or(viewport_px),
1731 ) {
1732 out.push((axes_index, edge_data));
1733 }
1734 }
1735 PlotElement::Mesh(plot) => {
1736 out.push((axes_index, plot.render_data()));
1737 if let Some(mut edge_data) = plot.edge_render_data() {
1738 if !hidden_line_removal {
1739 edge_data.pipeline_type = crate::core::PipelineType::LinesNoDepth;
1740 }
1741 out.push((axes_index, edge_data));
1742 }
1743 if let Some(vector_data) = plot.vector_render_data() {
1744 out.push((axes_index, vector_data));
1745 }
1746 }
1747 PlotElement::Line3(plot) => out.push((
1748 axes_index,
1749 plot.render_data_with_viewport_gpu(
1750 axes_viewports_px
1751 .and_then(|viewports| viewports.get(axes_index).copied())
1752 .or(viewport_px),
1753 self.axes_metadata.get(axes_index).and_then(|meta| {
1754 match (meta.view_azimuth_deg, meta.view_elevation_deg) {
1755 (Some(az), Some(el)) => Some((az, el)),
1756 _ => None,
1757 }
1758 }),
1759 gpu,
1760 ),
1761 )),
1762 PlotElement::Surface(plot) => {
1763 let mut render_data = plot.render_data();
1764 if !hidden_line_removal
1765 && render_data.pipeline_type == crate::core::PipelineType::Lines
1766 {
1767 render_data.pipeline_type = crate::core::PipelineType::LinesNoDepth;
1768 }
1769 out.push((axes_index, render_data));
1770 }
1771 _ => out.push((axes_index, p.render_data())),
1772 }
1773 }
1774 out
1775 }
1776
1777 fn reference_base_bounds_by_axes(&mut self) -> Vec<Option<BoundingBox>> {
1778 let axes_count = self.total_axes().max(1);
1779 let mut bounds: Vec<Option<BoundingBox>> = vec![None; axes_count];
1780 for (plot_idx, plot) in self.plots.iter_mut().enumerate() {
1781 if !plot.is_visible() || matches!(plot, PlotElement::ReferenceLine(_)) {
1782 continue;
1783 }
1784 let axes_index = self
1785 .plot_axes_indices
1786 .get(plot_idx)
1787 .copied()
1788 .unwrap_or(0)
1789 .min(axes_count - 1);
1790 let plot_bounds = plot.bounds();
1791 bounds[axes_index] = Some(match bounds[axes_index] {
1792 None => plot_bounds,
1793 Some(existing) => existing.union(&plot_bounds),
1794 });
1795 }
1796 bounds
1797 }
1798
1799 fn reference_line_ranges(
1800 x_limits: Option<(f64, f64)>,
1801 y_limits: Option<(f64, f64)>,
1802 meta: Option<&AxesMetadata>,
1803 base: Option<BoundingBox>,
1804 line: &ReferenceLine,
1805 ) -> ((f64, f64), (f64, f64)) {
1806 let x_range = x_limits
1807 .or_else(|| meta.and_then(|m| m.x_limits))
1808 .or_else(|| base.map(|b| (b.min.x as f64, b.max.x as f64)))
1809 .unwrap_or(match line.orientation {
1810 ReferenceLineOrientation::Vertical => (line.value - 0.5, line.value + 0.5),
1811 ReferenceLineOrientation::Horizontal => (0.0, 1.0),
1812 });
1813 let y_range = y_limits
1814 .or_else(|| meta.and_then(|m| m.y_limits))
1815 .or_else(|| base.map(|b| (b.min.y as f64, b.max.y as f64)))
1816 .unwrap_or(match line.orientation {
1817 ReferenceLineOrientation::Vertical => (0.0, 1.0),
1818 ReferenceLineOrientation::Horizontal => (line.value - 0.5, line.value + 0.5),
1819 });
1820 (
1821 normalize_reference_range(x_range),
1822 normalize_reference_range(y_range),
1823 )
1824 }
1825
1826 pub fn legend_entries(&self) -> Vec<LegendEntry> {
1828 let mut entries = Vec::new();
1829
1830 for plot in &self.plots {
1831 if plot.is_hidden_from_legend() {
1832 continue;
1833 }
1834 if let Some(label) = plot.label() {
1835 entries.push(LegendEntry {
1836 label,
1837 color: plot.color(),
1838 plot_type: plot.plot_type(),
1839 });
1840 }
1841 }
1842
1843 entries
1844 }
1845
1846 pub fn legend_entries_for_axes(&self, axes_index: usize) -> Vec<LegendEntry> {
1847 let mut entries = Vec::new();
1848 for (plot_idx, plot) in self.plots.iter().enumerate() {
1849 let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
1850 if plot_axes != axes_index {
1851 continue;
1852 }
1853 match plot {
1854 PlotElement::Pie(pie) => {
1855 for slice in pie.slice_meta() {
1856 entries.push(LegendEntry {
1857 label: slice.label,
1858 color: slice.color,
1859 plot_type: plot.plot_type(),
1860 });
1861 }
1862 }
1863 _ => {
1864 if plot.is_hidden_from_legend() {
1865 continue;
1866 }
1867 if let Some(label) = plot.label() {
1868 entries.push(LegendEntry {
1869 label,
1870 color: plot.color(),
1871 plot_type: plot.plot_type(),
1872 });
1873 }
1874 }
1875 }
1876 }
1877 entries
1878 }
1879
1880 pub fn pie_labels_for_axes(&self, axes_index: usize) -> Vec<PieLabelEntry> {
1881 let mut out = Vec::new();
1882 for (plot_idx, plot) in self.plots.iter().enumerate() {
1883 let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
1884 if plot_axes != axes_index {
1885 continue;
1886 }
1887 if let PlotElement::Pie(pie) = plot {
1888 for slice in pie.slice_meta() {
1889 out.push(PieLabelEntry {
1890 label: slice.label,
1891 position: glam::Vec2::new(
1892 slice.mid_angle.cos() * 1.15 + slice.offset.x,
1893 slice.mid_angle.sin() * 1.15 + slice.offset.y,
1894 ),
1895 });
1896 }
1897 }
1898 }
1899 out
1900 }
1901
1902 pub fn set_labels(&mut self, labels: &[String]) {
1904 self.set_labels_for_axes(self.active_axes_index, labels);
1905 }
1906
1907 pub fn set_labels_for_axes(&mut self, axes_index: usize, labels: &[String]) {
1908 let mut idx = 0usize;
1909 for (plot_idx, plot) in self.plots.iter_mut().enumerate() {
1910 let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
1911 if plot_axes != axes_index {
1912 continue;
1913 }
1914 if !plot.is_visible() {
1915 continue;
1916 }
1917 if idx >= labels.len() {
1918 break;
1919 }
1920 match plot {
1921 PlotElement::Pie(pie) => {
1922 let remaining = &labels[idx..];
1923 if remaining.len() >= pie.values.len() {
1924 pie.set_slice_labels(remaining[..pie.values.len()].to_vec());
1925 idx += pie.values.len();
1926 } else {
1927 pie.set_slice_labels(remaining.to_vec());
1928 idx = labels.len();
1929 }
1930 }
1931 _ => {
1932 plot.set_label(Some(labels[idx].clone()));
1933 idx += 1;
1934 }
1935 }
1936 }
1937 self.dirty = true;
1938 }
1939
1940 pub fn statistics(&self) -> FigureStatistics {
1942 let plot_counts = self.plots.iter().fold(HashMap::new(), |mut acc, plot| {
1943 let plot_type = plot.plot_type();
1944 *acc.entry(plot_type).or_insert(0) += 1;
1945 acc
1946 });
1947
1948 let total_memory: usize = self
1949 .plots
1950 .iter()
1951 .map(|plot| plot.estimated_memory_usage())
1952 .sum();
1953
1954 let visible_count = self.plots.iter().filter(|plot| plot.is_visible()).count();
1955
1956 FigureStatistics {
1957 total_plots: self.plots.len(),
1958 visible_plots: visible_count,
1959 plot_type_counts: plot_counts,
1960 total_memory_usage: total_memory,
1961 has_legend: self.legend_enabled && !self.legend_entries().is_empty(),
1962 }
1963 }
1964
1965 pub fn categorical_axis_labels(&self) -> Option<(bool, Vec<String>)> {
1969 for plot in &self.plots {
1970 if let PlotElement::Bar(b) = plot {
1971 if b.histogram_bin_edges().is_some() {
1972 continue;
1973 }
1974 let is_x = matches!(b.orientation, crate::plots::bar::Orientation::Vertical);
1975 return Some((is_x, b.labels.clone()));
1976 }
1977 }
1978 None
1979 }
1980
1981 pub fn categorical_axis_labels_for_axes(
1982 &self,
1983 axes_index: usize,
1984 ) -> Option<(bool, Vec<String>)> {
1985 for (plot_idx, plot) in self.plots.iter().enumerate() {
1986 let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
1987 if plot_axes != axes_index {
1988 continue;
1989 }
1990 if let PlotElement::Bar(b) = plot {
1991 if b.histogram_bin_edges().is_some() {
1992 continue;
1993 }
1994 let is_x = matches!(b.orientation, crate::plots::bar::Orientation::Vertical);
1995 return Some((is_x, b.labels.clone()));
1996 }
1997 }
1998 None
1999 }
2000
2001 pub fn x_axis_tick_labels_for_axes(&self, axes_index: usize) -> Option<Vec<String>> {
2002 self.axes_metadata
2003 .get(axes_index)
2004 .and_then(|meta| meta.x_tick_labels.clone())
2005 }
2006
2007 pub fn y_axis_tick_labels_for_axes(&self, axes_index: usize) -> Option<Vec<String>> {
2008 self.axes_metadata
2009 .get(axes_index)
2010 .and_then(|meta| meta.y_tick_labels.clone())
2011 }
2012
2013 pub fn x_axis_tick_format_for_axes(&self, axes_index: usize) -> Option<String> {
2014 self.axes_metadata
2015 .get(axes_index)
2016 .and_then(|meta| meta.x_tick_format.clone())
2017 }
2018
2019 pub fn y_axis_tick_format_for_axes(&self, axes_index: usize) -> Option<String> {
2020 self.axes_metadata
2021 .get(axes_index)
2022 .and_then(|meta| meta.y_tick_format.clone())
2023 }
2024
2025 pub fn x_axis_tick_label_rotation_for_axes(&self, axes_index: usize) -> Option<f64> {
2026 self.axes_metadata
2027 .get(axes_index)
2028 .and_then(|meta| meta.x_tick_label_rotation)
2029 }
2030
2031 pub fn y_axis_tick_label_rotation_for_axes(&self, axes_index: usize) -> Option<f64> {
2032 self.axes_metadata
2033 .get(axes_index)
2034 .and_then(|meta| meta.y_tick_label_rotation)
2035 }
2036
2037 pub fn x_axis_ticks_for_axes(&self, axes_index: usize) -> Option<Vec<f64>> {
2038 self.axes_metadata
2039 .get(axes_index)
2040 .and_then(|meta| meta.x_ticks.clone())
2041 }
2042
2043 pub fn y_axis_ticks_for_axes(&self, axes_index: usize) -> Option<Vec<f64>> {
2044 self.axes_metadata
2045 .get(axes_index)
2046 .and_then(|meta| meta.y_ticks.clone())
2047 }
2048
2049 pub fn histogram_axis_edges_for_axes(&self, axes_index: usize) -> Option<(bool, Vec<f64>)> {
2050 for (plot_idx, plot) in self.plots.iter().enumerate() {
2051 let plot_axes = *self.plot_axes_indices.get(plot_idx).unwrap_or(&0);
2052 if plot_axes != axes_index {
2053 continue;
2054 }
2055 if let PlotElement::Bar(b) = plot {
2056 if let Some(edges) = b.histogram_bin_edges() {
2057 let is_x = matches!(b.orientation, crate::plots::bar::Orientation::Vertical);
2058 return Some((is_x, edges.to_vec()));
2059 }
2060 }
2061 }
2062 None
2063 }
2064}
2065
2066impl Default for Figure {
2067 fn default() -> Self {
2068 Self::new()
2069 }
2070}
2071
2072fn normalize_reference_range(range: (f64, f64)) -> (f64, f64) {
2073 let (mut lo, mut hi) = range;
2074 if !lo.is_finite() || !hi.is_finite() {
2075 return (0.0, 1.0);
2076 }
2077 if hi < lo {
2078 std::mem::swap(&mut lo, &mut hi);
2079 }
2080 if (hi - lo).abs() < f64::EPSILON {
2081 let pad = lo.abs().max(1.0) * 0.5;
2082 return (lo - pad, hi + pad);
2083 }
2084 (lo, hi)
2085}
2086
2087impl PlotElement {
2088 pub fn is_visible(&self) -> bool {
2090 match self {
2091 PlotElement::Line(plot) => plot.visible,
2092 PlotElement::Scatter(plot) => plot.visible,
2093 PlotElement::Bar(plot) => plot.visible,
2094 PlotElement::ErrorBar(plot) => plot.visible,
2095 PlotElement::Stairs(plot) => plot.visible,
2096 PlotElement::Stem(plot) => plot.visible,
2097 PlotElement::Area(plot) => plot.visible,
2098 PlotElement::Quiver(plot) => plot.visible,
2099 PlotElement::Pie(plot) => plot.visible,
2100 PlotElement::Surface(plot) => plot.visible,
2101 PlotElement::Mesh(plot) => plot.is_visible(),
2102 PlotElement::Patch(plot) => plot.is_visible(),
2103 PlotElement::Line3(plot) => plot.visible,
2104 PlotElement::Scatter3(plot) => plot.visible,
2105 PlotElement::Contour(plot) => plot.visible,
2106 PlotElement::ContourFill(plot) => plot.visible,
2107 PlotElement::ReferenceLine(plot) => plot.visible,
2108 }
2109 }
2110
2111 pub fn label(&self) -> Option<String> {
2113 match self {
2114 PlotElement::Line(plot) => plot.label.clone(),
2115 PlotElement::Scatter(plot) => plot.label.clone(),
2116 PlotElement::Bar(plot) => plot.label.clone(),
2117 PlotElement::ErrorBar(plot) => plot.label.clone(),
2118 PlotElement::Stairs(plot) => plot.label.clone(),
2119 PlotElement::Stem(plot) => plot.label.clone(),
2120 PlotElement::Area(plot) => plot.label.clone(),
2121 PlotElement::Quiver(plot) => plot.label.clone(),
2122 PlotElement::Pie(plot) => plot.label.clone(),
2123 PlotElement::Surface(plot) => plot.label.clone(),
2124 PlotElement::Mesh(plot) => plot.label().map(str::to_string),
2125 PlotElement::Patch(plot) => plot.label().map(str::to_string),
2126 PlotElement::Line3(plot) => plot.label.clone(),
2127 PlotElement::Scatter3(plot) => plot.label.clone(),
2128 PlotElement::Contour(plot) => plot.label.clone(),
2129 PlotElement::ContourFill(plot) => plot.label.clone(),
2130 PlotElement::ReferenceLine(plot) => plot.label_for_legend(),
2131 }
2132 }
2133
2134 fn is_hidden_from_legend(&self) -> bool {
2135 matches!(
2136 self,
2137 PlotElement::Line(plot) if plot.handle_visibility.eq_ignore_ascii_case("off")
2138 )
2139 }
2140
2141 pub fn set_label(&mut self, label: Option<String>) {
2143 match self {
2144 PlotElement::Line(plot) => plot.label = label,
2145 PlotElement::Scatter(plot) => plot.label = label,
2146 PlotElement::Bar(plot) => plot.label = label,
2147 PlotElement::ErrorBar(plot) => plot.label = label,
2148 PlotElement::Stairs(plot) => plot.label = label,
2149 PlotElement::Stem(plot) => plot.label = label,
2150 PlotElement::Area(plot) => plot.label = label,
2151 PlotElement::Quiver(plot) => plot.label = label,
2152 PlotElement::Pie(plot) => plot.label = label,
2153 PlotElement::Surface(plot) => plot.label = label,
2154 PlotElement::Mesh(plot) => plot.set_label(label),
2155 PlotElement::Patch(plot) => plot.set_label(label),
2156 PlotElement::Line3(plot) => plot.label = label,
2157 PlotElement::Scatter3(plot) => plot.label = label,
2158 PlotElement::Contour(plot) => plot.label = label,
2159 PlotElement::ContourFill(plot) => plot.label = label,
2160 PlotElement::ReferenceLine(plot) => plot.label = label,
2161 }
2162 }
2163
2164 pub fn color(&self) -> Vec4 {
2166 match self {
2167 PlotElement::Line(plot) => plot.color,
2168 PlotElement::Scatter(plot) => plot.color,
2169 PlotElement::Bar(plot) => plot.color,
2170 PlotElement::ErrorBar(plot) => plot.color,
2171 PlotElement::Stairs(plot) => plot.color,
2172 PlotElement::Stem(plot) => plot.color,
2173 PlotElement::Area(plot) => plot.color,
2174 PlotElement::Quiver(plot) => plot.color,
2175 PlotElement::Pie(_plot) => Vec4::new(1.0, 1.0, 1.0, 1.0),
2176 PlotElement::Surface(_plot) => Vec4::new(1.0, 1.0, 1.0, 1.0),
2177 PlotElement::Mesh(plot) => plot.effective_face_color(),
2178 PlotElement::Patch(plot) => plot.effective_face_color(),
2179 PlotElement::Line3(plot) => plot.color,
2180 PlotElement::Scatter3(plot) => plot.colors.first().copied().unwrap_or(Vec4::ONE),
2181 PlotElement::Contour(_plot) => Vec4::new(1.0, 1.0, 1.0, 1.0),
2182 PlotElement::ContourFill(_plot) => Vec4::new(0.9, 0.9, 0.9, 1.0),
2183 PlotElement::ReferenceLine(plot) => plot.color,
2184 }
2185 }
2186
2187 pub fn plot_type(&self) -> PlotType {
2189 match self {
2190 PlotElement::Line(_) => PlotType::Line,
2191 PlotElement::Scatter(_) => PlotType::Scatter,
2192 PlotElement::Bar(_) => PlotType::Bar,
2193 PlotElement::ErrorBar(_) => PlotType::ErrorBar,
2194 PlotElement::Stairs(_) => PlotType::Stairs,
2195 PlotElement::Stem(_) => PlotType::Stem,
2196 PlotElement::Area(_) => PlotType::Area,
2197 PlotElement::Quiver(_) => PlotType::Quiver,
2198 PlotElement::Pie(_) => PlotType::Pie,
2199 PlotElement::Surface(_) => PlotType::Surface,
2200 PlotElement::Mesh(_) => PlotType::Mesh,
2201 PlotElement::Patch(_) => PlotType::Patch,
2202 PlotElement::Line3(_) => PlotType::Line3,
2203 PlotElement::Scatter3(_) => PlotType::Scatter3,
2204 PlotElement::Contour(_) => PlotType::Contour,
2205 PlotElement::ContourFill(_) => PlotType::ContourFill,
2206 PlotElement::ReferenceLine(_) => PlotType::ReferenceLine,
2207 }
2208 }
2209
2210 pub fn bounds(&mut self) -> BoundingBox {
2212 match self {
2213 PlotElement::Line(plot) => plot.bounds(),
2214 PlotElement::Scatter(plot) => plot.bounds(),
2215 PlotElement::Bar(plot) => plot.bounds(),
2216 PlotElement::ErrorBar(plot) => plot.bounds(),
2217 PlotElement::Stairs(plot) => plot.bounds(),
2218 PlotElement::Stem(plot) => plot.bounds(),
2219 PlotElement::Area(plot) => plot.bounds(),
2220 PlotElement::Quiver(plot) => plot.bounds(),
2221 PlotElement::Pie(plot) => plot.bounds(),
2222 PlotElement::Surface(plot) => plot.bounds(),
2223 PlotElement::Mesh(plot) => plot.bounds(),
2224 PlotElement::Patch(plot) => plot.bounds(),
2225 PlotElement::Line3(plot) => plot.bounds(),
2226 PlotElement::Scatter3(plot) => plot.bounds(),
2227 PlotElement::Contour(plot) => plot.bounds(),
2228 PlotElement::ContourFill(plot) => plot.bounds(),
2229 PlotElement::ReferenceLine(plot) => plot.coordinate_bounds(),
2230 }
2231 }
2232
2233 pub fn render_data(&mut self) -> RenderData {
2235 match self {
2236 PlotElement::Line(plot) => plot.render_data(),
2237 PlotElement::Scatter(plot) => plot.render_data(),
2238 PlotElement::Bar(plot) => plot.render_data(),
2239 PlotElement::ErrorBar(plot) => plot.render_data(),
2240 PlotElement::Stairs(plot) => plot.render_data(),
2241 PlotElement::Stem(plot) => plot.render_data(),
2242 PlotElement::Area(plot) => plot.render_data(),
2243 PlotElement::Quiver(plot) => plot.render_data(),
2244 PlotElement::Pie(plot) => plot.render_data(),
2245 PlotElement::Surface(plot) => plot.render_data(),
2246 PlotElement::Mesh(plot) => plot.render_data(),
2247 PlotElement::Patch(plot) => plot.render_data(),
2248 PlotElement::Line3(plot) => plot.render_data(),
2249 PlotElement::Scatter3(plot) => plot.render_data(),
2250 PlotElement::Contour(plot) => plot.render_data(),
2251 PlotElement::ContourFill(plot) => plot.render_data(),
2252 PlotElement::ReferenceLine(plot) => {
2253 plot.render_data_with_range((0.0, 1.0), (0.0, 1.0), None)
2254 }
2255 }
2256 }
2257
2258 pub fn estimated_memory_usage(&self) -> usize {
2260 match self {
2261 PlotElement::Line(plot) => plot.estimated_memory_usage(),
2262 PlotElement::Scatter(plot) => plot.estimated_memory_usage(),
2263 PlotElement::Bar(plot) => plot.estimated_memory_usage(),
2264 PlotElement::ErrorBar(plot) => plot.estimated_memory_usage(),
2265 PlotElement::Stairs(plot) => plot.estimated_memory_usage(),
2266 PlotElement::Stem(plot) => plot.estimated_memory_usage(),
2267 PlotElement::Area(plot) => plot.estimated_memory_usage(),
2268 PlotElement::Quiver(plot) => plot.estimated_memory_usage(),
2269 PlotElement::Pie(plot) => plot.estimated_memory_usage(),
2270 PlotElement::Surface(_plot) => 0,
2271 PlotElement::Mesh(plot) => plot.estimated_memory_usage(),
2272 PlotElement::Patch(plot) => plot.estimated_memory_usage(),
2273 PlotElement::Line3(plot) => plot.estimated_memory_usage(),
2274 PlotElement::Scatter3(plot) => plot.estimated_memory_usage(),
2275 PlotElement::Contour(plot) => plot.estimated_memory_usage(),
2276 PlotElement::ContourFill(plot) => plot.estimated_memory_usage(),
2277 PlotElement::ReferenceLine(plot) => plot.estimated_memory_usage(),
2278 }
2279 }
2280}
2281
2282#[derive(Debug)]
2284pub struct FigureStatistics {
2285 pub total_plots: usize,
2286 pub visible_plots: usize,
2287 pub plot_type_counts: HashMap<PlotType, usize>,
2288 pub total_memory_usage: usize,
2289 pub has_legend: bool,
2290}
2291
2292pub mod matlab_compat {
2294 use super::*;
2295 use crate::plots::{LinePlot, ScatterPlot};
2296
2297 pub fn figure() -> Figure {
2299 Figure::new()
2300 }
2301
2302 pub fn figure_with_title<S: Into<String>>(title: S) -> Figure {
2304 Figure::new().with_title(title)
2305 }
2306
2307 pub fn plot_multiple_lines(
2309 figure: &mut Figure,
2310 data_sets: Vec<(Vec<f64>, Vec<f64>, Option<String>)>,
2311 ) -> Result<Vec<usize>, String> {
2312 let mut indices = Vec::new();
2313
2314 for (i, (x, y, label)) in data_sets.into_iter().enumerate() {
2315 let mut line = LinePlot::new(x, y)?;
2316
2317 let colors = [
2319 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), ];
2327 let color = colors[i % colors.len()];
2328 line.set_color(color);
2329
2330 if let Some(label) = label {
2331 line = line.with_label(label);
2332 }
2333
2334 indices.push(figure.add_line_plot(line));
2335 }
2336
2337 Ok(indices)
2338 }
2339
2340 pub fn scatter_multiple(
2342 figure: &mut Figure,
2343 data_sets: Vec<(Vec<f64>, Vec<f64>, Option<String>)>,
2344 ) -> Result<Vec<usize>, String> {
2345 let mut indices = Vec::new();
2346
2347 for (i, (x, y, label)) in data_sets.into_iter().enumerate() {
2348 let mut scatter = ScatterPlot::new(x, y)?;
2349
2350 let colors = [
2352 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), ];
2360 let color = colors[i % colors.len()];
2361 scatter.set_color(color);
2362
2363 if let Some(label) = label {
2364 scatter = scatter.with_label(label);
2365 }
2366
2367 indices.push(figure.add_scatter_plot(scatter));
2368 }
2369
2370 Ok(indices)
2371 }
2372}
2373
2374#[cfg(test)]
2375mod tests {
2376 use super::*;
2377 use crate::plots::line::LineStyle;
2378 use glam::Vec3;
2379
2380 #[test]
2381 fn test_figure_creation() {
2382 let figure = Figure::new();
2383
2384 assert_eq!(figure.len(), 0);
2385 assert!(figure.is_empty());
2386 assert!(figure.legend_enabled);
2387 assert!(figure.grid_enabled);
2388 }
2389
2390 #[test]
2391 fn test_figure_styling() {
2392 let figure = Figure::new()
2393 .with_title("Test Figure")
2394 .with_sg_title("Overview")
2395 .with_labels("X Axis", "Y Axis")
2396 .with_legend(false)
2397 .with_grid(false);
2398
2399 assert_eq!(figure.title, Some("Test Figure".to_string()));
2400 assert_eq!(figure.sg_title, Some("Overview".to_string()));
2401 assert_eq!(figure.x_label, Some("X Axis".to_string()));
2402 assert_eq!(figure.y_label, Some("Y Axis".to_string()));
2403 assert!(!figure.legend_enabled);
2404 assert!(!figure.grid_enabled);
2405 }
2406
2407 #[test]
2408 fn test_window_title_follows_name_and_number_title() {
2409 let mut figure = Figure::new();
2410 assert_eq!(figure.window_title(Some(7)), "Figure 7");
2411
2412 figure.set_name("demo");
2413 assert_eq!(figure.window_title(Some(7)), "Figure 7: demo");
2414
2415 figure.set_number_title(false);
2416 assert_eq!(figure.window_title(Some(7)), "demo");
2417
2418 figure.set_name(" ");
2419 assert_eq!(figure.window_title(Some(7)), "RunMat Plot");
2420 }
2421
2422 #[test]
2423 fn test_has_any_titles_tracks_super_and_axes_titles() {
2424 let mut figure = Figure::new();
2425 assert!(!figure.has_any_titles());
2426
2427 figure.set_sg_title("Summary");
2428 assert!(figure.has_any_titles());
2429
2430 figure.clear_sg_title();
2431 assert!(!figure.has_any_titles());
2432
2433 figure.set_axes_title(0, "Panel");
2434 assert!(figure.has_any_titles());
2435 }
2436
2437 #[test]
2438 fn test_multiple_line_plots() {
2439 let mut figure = Figure::new();
2440
2441 let line1 = LinePlot::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 4.0])
2443 .unwrap()
2444 .with_label("Quadratic");
2445 let index1 = figure.add_line_plot(line1);
2446
2447 let line2 = LinePlot::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 2.0])
2449 .unwrap()
2450 .with_style(Vec4::new(1.0, 0.0, 0.0, 1.0), 2.0, LineStyle::Dashed)
2451 .with_label("Linear");
2452 let index2 = figure.add_line_plot(line2);
2453
2454 assert_eq!(figure.len(), 2);
2455 assert_eq!(index1, 0);
2456 assert_eq!(index2, 1);
2457
2458 let legend = figure.legend_entries();
2460 assert_eq!(legend.len(), 2);
2461 assert_eq!(legend[0].label, "Quadratic");
2462 assert_eq!(legend[1].label, "Linear");
2463 }
2464
2465 #[test]
2466 fn test_mixed_plot_types() {
2467 let mut figure = Figure::new();
2468
2469 let line = LinePlot::new(vec![0.0, 1.0, 2.0], vec![1.0, 2.0, 3.0])
2471 .unwrap()
2472 .with_label("Line");
2473 figure.add_line_plot(line);
2474
2475 let scatter = ScatterPlot::new(vec![0.5, 1.5, 2.5], vec![1.5, 2.5, 3.5])
2476 .unwrap()
2477 .with_label("Scatter");
2478 figure.add_scatter_plot(scatter);
2479
2480 let bar = BarChart::new(vec!["A".to_string(), "B".to_string()], vec![2.0, 4.0])
2481 .unwrap()
2482 .with_label("Bar");
2483 figure.add_bar_chart(bar);
2484
2485 assert_eq!(figure.len(), 3);
2486
2487 let render_data = figure.render_data();
2489 assert_eq!(render_data.len(), 3);
2490
2491 let stats = figure.statistics();
2493 assert_eq!(stats.total_plots, 3);
2494 assert_eq!(stats.visible_plots, 3);
2495 assert!(stats.has_legend);
2496 }
2497
2498 #[test]
2499 fn test_plot_visibility() {
2500 let mut figure = Figure::new();
2501
2502 let mut line = LinePlot::new(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
2503 line.set_visible(false); figure.add_line_plot(line);
2505
2506 let scatter = ScatterPlot::new(vec![0.0, 1.0], vec![1.0, 2.0]).unwrap();
2507 figure.add_scatter_plot(scatter);
2508
2509 let render_data = figure.render_data();
2511 assert_eq!(render_data.len(), 1);
2512
2513 let stats = figure.statistics();
2514 assert_eq!(stats.total_plots, 2);
2515 assert_eq!(stats.visible_plots, 1);
2516 }
2517
2518 #[test]
2519 fn test_bounds_computation() {
2520 let mut figure = Figure::new();
2521
2522 let line = LinePlot::new(vec![-1.0, 0.0, 1.0], vec![-2.0, 0.0, 2.0]).unwrap();
2524 figure.add_line_plot(line);
2525
2526 let scatter = ScatterPlot::new(vec![2.0, 3.0, 4.0], vec![1.0, 3.0, 5.0]).unwrap();
2527 figure.add_scatter_plot(scatter);
2528
2529 let bounds = figure.bounds();
2530
2531 assert!(bounds.min.x <= -1.0);
2533 assert!(bounds.max.x >= 4.0);
2534 assert!(bounds.min.y <= -2.0);
2535 assert!(bounds.max.y >= 5.0);
2536 }
2537
2538 #[test]
2539 fn test_reference_line_only_bounds_use_default_span() {
2540 let mut vertical_figure = Figure::new();
2541 vertical_figure.add_reference_line_on_axes(
2542 ReferenceLine::new(ReferenceLineOrientation::Vertical, 2.0).unwrap(),
2543 0,
2544 );
2545 let vertical_bounds = vertical_figure.bounds();
2546 assert_eq!(vertical_bounds.min.x, 1.5);
2547 assert_eq!(vertical_bounds.max.x, 2.5);
2548 assert_eq!(vertical_bounds.min.y, 0.0);
2549 assert_eq!(vertical_bounds.max.y, 1.0);
2550
2551 let mut horizontal_figure = Figure::new();
2552 horizontal_figure.add_reference_line_on_axes(
2553 ReferenceLine::new(ReferenceLineOrientation::Horizontal, 3.0).unwrap(),
2554 0,
2555 );
2556 let horizontal_bounds = horizontal_figure.bounds();
2557 assert_eq!(horizontal_bounds.min.x, 0.0);
2558 assert_eq!(horizontal_bounds.max.x, 1.0);
2559 assert_eq!(horizontal_bounds.min.y, 2.5);
2560 assert_eq!(horizontal_bounds.max.y, 3.5);
2561 }
2562
2563 #[test]
2564 fn test_reference_line_render_data_prefers_figure_limits() {
2565 let mut horizontal_figure = Figure::new().with_limits((-2.0, 8.0), (-10.0, 10.0));
2566 horizontal_figure.axes_metadata[0].x_limits = Some((0.0, 1.0));
2567 horizontal_figure.add_reference_line_on_axes(
2568 ReferenceLine::new(ReferenceLineOrientation::Horizontal, 3.0).unwrap(),
2569 0,
2570 );
2571 let horizontal_bounds = horizontal_figure.render_data()[0].bounds.unwrap();
2572 assert_eq!(horizontal_bounds.min.x, -2.0);
2573 assert_eq!(horizontal_bounds.max.x, 8.0);
2574 assert_eq!(horizontal_bounds.min.y, 3.0);
2575 assert_eq!(horizontal_bounds.max.y, 3.0);
2576
2577 let mut vertical_figure = Figure::new().with_limits((-2.0, 8.0), (-10.0, 10.0));
2578 vertical_figure.axes_metadata[0].y_limits = Some((0.0, 1.0));
2579 vertical_figure.add_reference_line_on_axes(
2580 ReferenceLine::new(ReferenceLineOrientation::Vertical, 4.0).unwrap(),
2581 0,
2582 );
2583 let vertical_bounds = vertical_figure.render_data()[0].bounds.unwrap();
2584 assert_eq!(vertical_bounds.min.x, 4.0);
2585 assert_eq!(vertical_bounds.max.x, 4.0);
2586 assert_eq!(vertical_bounds.min.y, -10.0);
2587 assert_eq!(vertical_bounds.max.y, 10.0);
2588 }
2589
2590 #[test]
2591 fn test_matlab_compat_multiple_lines() {
2592 use super::matlab_compat::*;
2593
2594 let mut figure = figure_with_title("Multiple Lines Test");
2595
2596 let data_sets = vec![
2597 (
2598 vec![0.0, 1.0, 2.0],
2599 vec![0.0, 1.0, 4.0],
2600 Some("Quadratic".to_string()),
2601 ),
2602 (
2603 vec![0.0, 1.0, 2.0],
2604 vec![0.0, 1.0, 2.0],
2605 Some("Linear".to_string()),
2606 ),
2607 (
2608 vec![0.0, 1.0, 2.0],
2609 vec![1.0, 1.0, 1.0],
2610 Some("Constant".to_string()),
2611 ),
2612 ];
2613
2614 let indices = plot_multiple_lines(&mut figure, data_sets).unwrap();
2615
2616 assert_eq!(indices.len(), 3);
2617 assert_eq!(figure.len(), 3);
2618
2619 let legend = figure.legend_entries();
2621 assert_eq!(legend.len(), 3);
2622 assert_ne!(legend[0].color, legend[1].color);
2623 assert_ne!(legend[1].color, legend[2].color);
2624 }
2625
2626 #[test]
2627 fn axes_metadata_and_labels_are_isolated_per_subplot() {
2628 let mut figure = Figure::new();
2629 figure.set_subplot_grid(1, 2);
2630 figure.set_axes_title(0, "Left Title");
2631 figure.set_axes_xlabel(0, "Left X");
2632 figure.set_axes_ylabel(0, "Left Y");
2633 figure.set_axes_title(1, "Right Title");
2634 figure.set_axes_style(
2635 1,
2636 TextStyle {
2637 font_size: Some(14.0),
2638 ..Default::default()
2639 },
2640 );
2641 figure.set_axes_legend_enabled(0, false);
2642 figure.set_axes_legend_style(
2643 1,
2644 LegendStyle {
2645 location: Some("southwest".into()),
2646 ..Default::default()
2647 },
2648 );
2649
2650 assert_eq!(
2651 figure.axes_metadata(0).and_then(|m| m.title.as_deref()),
2652 Some("Left Title")
2653 );
2654 assert_eq!(
2655 figure.axes_metadata(1).and_then(|m| m.title.as_deref()),
2656 Some("Right Title")
2657 );
2658 assert_eq!(
2659 figure.axes_metadata(0).and_then(|m| m.x_label.as_deref()),
2660 Some("Left X")
2661 );
2662 assert_eq!(
2663 figure.axes_metadata(0).and_then(|m| m.y_label.as_deref()),
2664 Some("Left Y")
2665 );
2666 assert!(!figure.axes_metadata(0).unwrap().legend_enabled);
2667 assert_eq!(
2668 figure
2669 .axes_metadata(1)
2670 .unwrap()
2671 .legend_style
2672 .location
2673 .as_deref(),
2674 Some("southwest")
2675 );
2676 assert_eq!(figure.axes_metadata(0).unwrap().axes_style.font_size, None);
2677 assert_eq!(
2678 figure.axes_metadata(1).unwrap().axes_style.font_size,
2679 Some(14.0)
2680 );
2681 }
2682
2683 #[test]
2684 fn set_labels_for_axes_only_updates_target_subplot() {
2685 let mut figure = Figure::new();
2686 figure.set_subplot_grid(1, 2);
2687 figure.add_line_plot_on_axes(
2688 LinePlot::new(vec![0.0, 1.0], vec![1.0, 2.0])
2689 .unwrap()
2690 .with_label("L0"),
2691 0,
2692 );
2693 figure.add_line_plot_on_axes(
2694 LinePlot::new(vec![0.0, 1.0], vec![2.0, 3.0])
2695 .unwrap()
2696 .with_label("R0"),
2697 1,
2698 );
2699 figure.set_labels_for_axes(1, &["Right Only".into()]);
2700
2701 let left_entries = figure.legend_entries_for_axes(0);
2702 let right_entries = figure.legend_entries_for_axes(1);
2703 assert_eq!(left_entries[0].label, "L0");
2704 assert_eq!(right_entries[0].label, "Right Only");
2705 }
2706
2707 #[test]
2708 fn axes_log_modes_are_isolated_per_subplot() {
2709 let mut figure = Figure::new();
2710 figure.set_subplot_grid(1, 2);
2711 figure.set_axes_log_modes(1, true, false);
2712
2713 assert!(!figure.axes_metadata(0).unwrap().x_log);
2714 assert!(!figure.axes_metadata(0).unwrap().y_log);
2715 assert!(figure.axes_metadata(1).unwrap().x_log);
2716 assert!(!figure.axes_metadata(1).unwrap().y_log);
2717
2718 figure.set_active_axes_index(1);
2719 assert!(figure.x_log);
2720 assert!(!figure.y_log);
2721 }
2722
2723 #[test]
2724 fn z_label_and_view_state_are_isolated_per_subplot() {
2725 let mut figure = Figure::new();
2726 figure.set_subplot_grid(1, 2);
2727 figure.set_axes_zlabel(1, "Height");
2728 figure.set_axes_view(1, 45.0, 20.0);
2729
2730 assert_eq!(figure.axes_metadata(0).unwrap().z_label, None);
2731 assert_eq!(
2732 figure.axes_metadata(1).unwrap().z_label.as_deref(),
2733 Some("Height")
2734 );
2735 assert_eq!(
2736 figure.axes_metadata(1).unwrap().view_azimuth_deg,
2737 Some(45.0)
2738 );
2739 assert_eq!(
2740 figure.axes_metadata(1).unwrap().view_elevation_deg,
2741 Some(20.0)
2742 );
2743 }
2744
2745 #[test]
2746 fn axes_view_revision_advances_for_each_explicit_view_update() {
2747 let mut figure = Figure::new();
2748
2749 assert_eq!(figure.axes_metadata(0).unwrap().view_revision, 0);
2750
2751 figure.set_axes_view(0, 45.0, 20.0);
2752 assert_eq!(figure.axes_metadata(0).unwrap().view_revision, 1);
2753
2754 figure.set_axes_view(0, 45.0, 20.0);
2755 assert_eq!(figure.axes_metadata(0).unwrap().view_revision, 2);
2756 }
2757
2758 #[test]
2759 fn pie_legend_entries_are_slice_based() {
2760 let mut figure = Figure::new();
2761 let pie = PieChart::new(vec![1.0, 2.0], None)
2762 .unwrap()
2763 .with_slice_labels(vec!["A".into(), "B".into()]);
2764 figure.add_pie_chart(pie);
2765 let entries = figure.legend_entries_for_axes(0);
2766 assert_eq!(entries.len(), 2);
2767 assert_eq!(entries[0].label, "A");
2768 assert_eq!(entries[1].label, "B");
2769 }
2770
2771 #[test]
2772 fn histogram_bars_do_not_use_categorical_axis_labels() {
2773 let mut figure = Figure::new();
2774 let mut bar = BarChart::new(vec!["a".into(), "b".into()], vec![2.0, 3.0]).unwrap();
2775 bar.set_histogram_bin_edges(vec![0.0, 0.5, 1.0]);
2776 figure.add_bar_chart(bar);
2777
2778 assert!(figure.categorical_axis_labels().is_none());
2779 assert_eq!(
2780 figure.histogram_axis_edges_for_axes(0),
2781 Some((true, vec![0.0, 0.5, 1.0]))
2782 );
2783 }
2784
2785 #[test]
2786 fn plain_bar_charts_keep_categorical_axis_labels() {
2787 let mut figure = Figure::new();
2788 let bar = BarChart::new(vec!["A".into(), "B".into()], vec![1.0, 2.0]).unwrap();
2789 figure.add_bar_chart(bar);
2790
2791 assert_eq!(
2792 figure.categorical_axis_labels(),
2793 Some((true, vec!["A".to_string(), "B".to_string()]))
2794 );
2795 }
2796
2797 #[test]
2798 fn line3_contributes_to_3d_bounds_and_metadata() {
2799 let mut figure = Figure::new();
2800 let line3 = Line3Plot::new(vec![0.0, 1.0], vec![1.0, 2.0], vec![2.0, 4.0])
2801 .unwrap()
2802 .with_label("Trajectory");
2803 figure.add_line3_plot(line3);
2804 let bounds = figure.bounds();
2805 assert_eq!(bounds.min.z, 2.0);
2806 assert_eq!(bounds.max.z, 4.0);
2807 let entries = figure.legend_entries_for_axes(0);
2808 assert_eq!(entries[0].plot_type, PlotType::Line3);
2809 }
2810
2811 #[test]
2812 fn hidden_line_removal_off_uses_no_depth_pipeline_for_mesh_edges() {
2813 let mut figure = Figure::new();
2814 let mesh = MeshPlot::new(
2815 vec![
2816 Vec3::new(0.0, 0.0, 0.0),
2817 Vec3::new(1.0, 0.0, 0.0),
2818 Vec3::new(0.0, 1.0, 1.0),
2819 ],
2820 vec![[0, 1, 2]],
2821 )
2822 .unwrap();
2823 figure.add_mesh_plot(mesh);
2824 figure.set_axes_hidden_line_removal(0, false);
2825
2826 let render_data = figure.render_data();
2827 assert_eq!(render_data.len(), 2);
2828 assert_eq!(
2829 render_data[0].pipeline_type,
2830 crate::core::PipelineType::Triangles
2831 );
2832 assert_eq!(
2833 render_data[1].pipeline_type,
2834 crate::core::PipelineType::LinesNoDepth
2835 );
2836 }
2837
2838 #[test]
2839 fn hidden_line_removal_off_uses_no_depth_pipeline_for_wireframe_surfaces() {
2840 let mut figure = Figure::new();
2841 let surface = SurfacePlot::new(
2842 vec![0.0, 1.0],
2843 vec![0.0, 1.0],
2844 vec![vec![0.0, 1.0], vec![1.0, 0.0]],
2845 )
2846 .unwrap()
2847 .with_wireframe(true);
2848 figure.add_surface_plot(surface);
2849 figure.set_axes_hidden_line_removal(0, false);
2850
2851 let render_data = figure.render_data();
2852 assert_eq!(render_data.len(), 1);
2853 assert_eq!(
2854 render_data[0].pipeline_type,
2855 crate::core::PipelineType::LinesNoDepth
2856 );
2857 }
2858
2859 #[test]
2860 fn stem_render_data_includes_marker_pass() {
2861 let mut figure = Figure::new();
2862 figure.add_stem_plot(StemPlot::new(vec![0.0, 1.0], vec![1.0, 2.0]).unwrap());
2863
2864 let render_data = figure.render_data();
2865 assert_eq!(render_data.len(), 2);
2866 assert_eq!(
2867 render_data[0].pipeline_type,
2868 crate::core::PipelineType::Lines
2869 );
2870 assert_eq!(
2871 render_data[1].pipeline_type,
2872 crate::core::PipelineType::Points
2873 );
2874 }
2875
2876 #[test]
2877 fn errorbar_render_data_includes_marker_pass() {
2878 let mut figure = Figure::new();
2879 figure.add_errorbar(
2880 ErrorBar::new_vertical(
2881 vec![0.0, 1.0],
2882 vec![1.0, 2.0],
2883 vec![0.1, 0.2],
2884 vec![0.1, 0.2],
2885 )
2886 .unwrap(),
2887 );
2888
2889 let render_data = figure.render_data();
2890 assert_eq!(render_data.len(), 2);
2891 assert_eq!(
2892 render_data[0].pipeline_type,
2893 crate::core::PipelineType::Lines
2894 );
2895 assert_eq!(
2896 render_data[1].pipeline_type,
2897 crate::core::PipelineType::Points
2898 );
2899 }
2900
2901 #[test]
2902 fn subplot_sensitive_axes_state_is_isolated_per_subplot() {
2903 let mut figure = Figure::new();
2904 figure.set_subplot_grid(1, 2);
2905 figure.set_axes_limits(1, Some((1.0, 2.0)), Some((3.0, 4.0)));
2906 figure.set_axes_z_limits(1, Some((5.0, 6.0)));
2907 figure.set_axes_grid_enabled(1, false);
2908 figure.set_axes_minor_grid_enabled(1, true);
2909 figure.set_axes_box_enabled(1, false);
2910 figure.set_axes_axis_equal(1, true);
2911 figure.set_axes_colorbar_enabled(1, true);
2912 figure.set_axes_colormap(1, ColorMap::Hot);
2913 figure.set_axes_color_limits(1, Some((0.0, 10.0)));
2914
2915 let left = figure.axes_metadata(0).unwrap();
2916 let right = figure.axes_metadata(1).unwrap();
2917 assert_eq!(left.x_limits, None);
2918 assert_eq!(right.x_limits, Some((1.0, 2.0)));
2919 assert!(!left.minor_grid_enabled);
2920 assert!(!left.minor_grid_explicit);
2921 assert!(!right.grid_enabled);
2922 assert!(right.minor_grid_enabled);
2923 assert!(right.minor_grid_explicit);
2924 assert!(!right.box_enabled);
2925 assert!(right.axis_equal);
2926 assert!(right.colorbar_enabled);
2927 assert_eq!(format!("{:?}", right.colormap), "Hot");
2928 assert_eq!(right.color_limits, Some((0.0, 10.0)));
2929 }
2930
2931 #[test]
2932 fn subplot_grid_expansion_reclaims_overlay_axes_slots() {
2933 let mut figure = Figure::new();
2934 let overlay_axes = figure.ensure_overlay_axes(0);
2935 assert_eq!(overlay_axes, 1);
2936 figure.add_line_plot_on_axes(
2937 LinePlot::new(vec![0.0, 1.0], vec![10.0, 20.0]).unwrap(),
2938 overlay_axes,
2939 );
2940 assert_eq!(figure.axes_overlay_parent(overlay_axes), Some(0));
2941 assert_eq!(figure.axes_count(), 2);
2942
2943 figure.set_subplot_grid(2, 1);
2944
2945 let second_subplot = figure.axes_metadata(1).unwrap();
2946 assert_eq!(second_subplot.overlay_parent, None);
2947 assert_eq!(second_subplot.y_axis_location, "left");
2948 assert!(figure.plot_axes_indices().is_empty());
2949 assert_eq!(figure.axes_count(), 2);
2950 }
2951
2952 #[test]
2953 fn active_axes_sync_does_not_clobber_figure_minor_grid_default() {
2954 let mut figure = Figure::new();
2955 figure.set_subplot_grid(1, 2);
2956 figure.minor_grid_enabled = true;
2957
2958 assert!(figure.minor_grid_enabled_for_axes(0));
2959 assert!(figure.minor_grid_enabled_for_axes(1));
2960
2961 figure.set_active_axes_index(1);
2962
2963 assert!(figure.minor_grid_enabled);
2964 assert!(figure.minor_grid_enabled_for_axes(0));
2965 assert!(figure.minor_grid_enabled_for_axes(1));
2966
2967 figure.set_axes_minor_grid_enabled(1, false);
2968
2969 assert!(figure.minor_grid_enabled);
2970 assert!(figure.minor_grid_enabled_for_axes(0));
2971 assert!(!figure.minor_grid_enabled_for_axes(1));
2972 }
2973}