1use glam::Vec4;
2use once_cell::sync::OnceCell;
3use runmat_builtins::Tensor;
4use runmat_plot::plots::{
5 surface::ColorMap, surface::ShadingMode, AxesKind, Figure, LegendStyle, LineStyle, PlotElement,
6 TextStyle,
7};
8use runmat_thread_local::runmat_thread_local;
9use std::cell::RefCell;
10use std::collections::{hash_map::Entry, HashMap, HashSet};
11use std::ops::{Deref, DerefMut};
12#[cfg(not(target_arch = "wasm32"))]
13use std::sync::MutexGuard;
14use std::sync::{Arc, Mutex};
15use thiserror::Error;
16
17use super::common::{default_figure, ERR_PLOTTING_UNAVAILABLE};
18#[cfg(not(all(target_arch = "wasm32", feature = "plot-web")))]
19use super::engine::render_figure;
20use super::web::current_plot_theme_config;
21use super::{plotting_error, plotting_error_with_source};
22
23use crate::builtins::common::map_control_flow_with_builtin;
24use crate::{BuiltinResult, RuntimeError};
25
26type AxisLimitSnapshot = (Option<(f64, f64)>, Option<(f64, f64)>);
27type AxisTickSnapshot = (Option<Vec<f64>>, Option<Vec<f64>>);
28type AxisTickLabelSnapshot = (Option<Vec<String>>, Option<Vec<String>>);
29type AxisTickFormatSnapshot = (Option<String>, Option<String>);
30type AxisTickAngleSnapshot = (Option<f64>, Option<f64>);
31type AxisDisplayBoundsSnapshot = Option<(f64, f64, f64, f64)>;
32const DEFAULT_COLORMAP_LENGTH: usize = 256;
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum LinkAxesMode {
36 X,
37 Y,
38 XY,
39}
40
41impl LinkAxesMode {
42 fn links_x(self) -> bool {
43 matches!(self, Self::X | Self::XY)
44 }
45
46 fn links_y(self) -> bool {
47 matches!(self, Self::Y | Self::XY)
48 }
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52enum LinkAxesAxis {
53 X,
54 Y,
55}
56
57#[derive(Clone, Debug)]
58struct LinkAxesGroup {
59 axis: LinkAxesAxis,
60 targets: Vec<(FigureHandle, usize)>,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
64pub struct FigureHandle(u32);
65
66impl FigureHandle {
67 pub fn as_u32(self) -> u32 {
68 self.0
69 }
70
71 fn next(self) -> FigureHandle {
72 FigureHandle(self.0 + 1)
73 }
74}
75
76impl From<u32> for FigureHandle {
77 fn from(value: u32) -> Self {
78 FigureHandle(value.max(1))
79 }
80}
81
82impl Default for FigureHandle {
83 fn default() -> Self {
84 FigureHandle(1)
85 }
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum ZoomMotion {
90 Both,
91 Horizontal,
92 Vertical,
93}
94
95impl ZoomMotion {
96 pub fn as_str(self) -> &'static str {
97 match self {
98 Self::Both => "both",
99 Self::Horizontal => "horizontal",
100 Self::Vertical => "vertical",
101 }
102 }
103}
104
105impl Default for ZoomMotion {
106 fn default() -> Self {
107 Self::Both
108 }
109}
110
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112pub enum ZoomDirection {
113 In,
114 Out,
115}
116
117impl ZoomDirection {
118 pub fn as_str(self) -> &'static str {
119 match self {
120 Self::In => "in",
121 Self::Out => "out",
122 }
123 }
124}
125
126#[derive(Clone, Copy, Debug, PartialEq, Eq)]
127pub enum ZoomRightClickAction {
128 PostContextMenu,
129 InverseZoom,
130}
131
132impl ZoomRightClickAction {
133 pub fn as_str(self) -> &'static str {
134 match self {
135 Self::PostContextMenu => "PostContextMenu",
136 Self::InverseZoom => "InverseZoom",
137 }
138 }
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
142pub struct ZoomModeState {
143 pub enabled: bool,
144 pub motion: ZoomMotion,
145 pub direction: ZoomDirection,
146 pub right_click_action: ZoomRightClickAction,
147 pub use_legacy_exploration_modes: bool,
148}
149
150impl Default for ZoomModeState {
151 fn default() -> Self {
152 Self {
153 enabled: false,
154 motion: ZoomMotion::Both,
155 direction: ZoomDirection::In,
156 right_click_action: ZoomRightClickAction::PostContextMenu,
157 use_legacy_exploration_modes: false,
158 }
159 }
160}
161
162#[derive(Clone, Copy, Debug, PartialEq, Eq)]
163pub enum ZoomModeCommand {
164 On,
165 Off,
166 Toggle,
167 XOn,
168 YOn,
169}
170
171#[derive(Clone, Copy, Debug, PartialEq)]
172pub struct ZoomStateSnapshot {
173 pub figure: FigureHandle,
174 pub axes_index: Option<usize>,
175 pub mode: ZoomModeState,
176}
177
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
179pub struct PanModeState {
180 pub enabled: bool,
181 pub motion: ZoomMotion,
182}
183
184impl Default for PanModeState {
185 fn default() -> Self {
186 Self {
187 enabled: false,
188 motion: ZoomMotion::Both,
189 }
190 }
191}
192
193#[derive(Clone, Copy, Debug, PartialEq)]
194pub struct PanStateSnapshot {
195 pub figure: FigureHandle,
196 pub axes_index: Option<usize>,
197 pub mode: PanModeState,
198}
199
200#[derive(Clone, Copy, Debug, PartialEq, Eq)]
201pub enum PanModeCommand {
202 On,
203 Off,
204 Toggle,
205 XOn,
206 YOn,
207}
208
209#[derive(Clone, Debug, PartialEq, Eq)]
210pub struct DataCursorModeState {
211 pub enabled: bool,
212 pub snap_to_data_vertex: bool,
213 pub display_style: String,
214}
215
216impl Default for DataCursorModeState {
217 fn default() -> Self {
218 Self {
219 enabled: false,
220 snap_to_data_vertex: true,
221 display_style: "datatip".into(),
222 }
223 }
224}
225
226#[derive(Clone, Debug, PartialEq, Eq)]
227pub struct DataCursorStateSnapshot {
228 pub figure: FigureHandle,
229 pub mode: DataCursorModeState,
230}
231
232#[derive(Clone, Debug, PartialEq)]
233pub struct WaitbarState {
234 pub progress: f64,
235 pub message: String,
236}
237
238const DEFAULT_LINE_STYLE_ORDER: [LineStyle; 1] = [LineStyle::Solid];
239
240#[derive(Clone)]
241struct LineStyleCycle {
242 order: Vec<LineStyle>,
243 cursor: usize,
244}
245
246impl Default for LineStyleCycle {
247 fn default() -> Self {
248 Self {
249 order: DEFAULT_LINE_STYLE_ORDER.to_vec(),
250 cursor: 0,
251 }
252 }
253}
254
255impl LineStyleCycle {
256 fn next(&mut self) -> LineStyle {
257 if self.order.is_empty() {
258 self.order = DEFAULT_LINE_STYLE_ORDER.to_vec();
259 }
260 let style = self.order[self.cursor % self.order.len()];
261 self.cursor = (self.cursor + 1) % self.order.len();
262 style
263 }
264
265 fn set_order(&mut self, order: &[LineStyle]) {
266 if order.is_empty() {
267 self.order = DEFAULT_LINE_STYLE_ORDER.to_vec();
268 } else {
269 self.order = order.to_vec();
270 }
271 self.cursor = 0;
272 }
273
274 fn reset_cursor(&mut self) {
275 self.cursor = 0;
276 }
277}
278
279#[derive(Clone, Default)]
280struct LineColorCycle {
281 order: Option<Vec<Vec4>>,
282 cursor: usize,
283}
284
285impl LineColorCycle {
286 fn next(&mut self) -> Vec4 {
287 let color = match self.order.as_deref() {
288 Some(order) if !order.is_empty() => order[self.cursor % order.len()],
289 _ => line_color_for_series_index(self.cursor),
290 };
291 self.cursor = self.cursor.saturating_add(1);
292 color
293 }
294
295 fn color_at(&self, index: usize) -> Vec4 {
296 match self.order.as_deref() {
297 Some(order) if !order.is_empty() => order[index % order.len()],
298 _ => line_color_for_series_index(index),
299 }
300 }
301
302 fn set_order(&mut self, order: &[Vec4]) {
303 self.order = if order.is_empty() {
304 None
305 } else {
306 Some(order.to_vec())
307 };
308 self.cursor = 0;
309 }
310
311 fn reset_cursor(&mut self) {
312 self.cursor = 0;
313 }
314}
315
316#[derive(Default)]
317struct FigureState {
318 figure: Figure,
319 active_axes: usize,
320 tag: String,
321 hold_per_axes: HashMap<usize, bool>,
322 line_style_cycles: HashMap<usize, LineStyleCycle>,
323 line_color_cycles: HashMap<usize, LineColorCycle>,
324 figure_color_order: Option<Vec<Vec4>>,
325 colormap_lengths: HashMap<usize, usize>,
326 zoom_mode: ZoomModeState,
327 last_enabled_zoom_motion: ZoomMotion,
328 zoom_axes_modes: HashMap<usize, ZoomModeState>,
329 zoom_baselines: HashMap<usize, AxisLimitSnapshot>,
330 pan_mode: PanModeState,
331 last_enabled_pan_motion: ZoomMotion,
332 pan_axes_modes: HashMap<usize, PanModeState>,
333 data_cursor_mode: DataCursorModeState,
334 waitbar: Option<WaitbarState>,
335 revision: u64,
336}
337
338impl FigureState {
339 fn new(handle: FigureHandle) -> Self {
340 let title = format!("Figure {}", handle.as_u32());
341 let figure = default_figure(&title, "X", "Y");
342 Self {
343 figure,
344 active_axes: 0,
345 tag: String::new(),
346 hold_per_axes: HashMap::new(),
347 line_style_cycles: HashMap::new(),
348 line_color_cycles: HashMap::new(),
349 figure_color_order: None,
350 colormap_lengths: HashMap::new(),
351 zoom_mode: ZoomModeState::default(),
352 last_enabled_zoom_motion: ZoomMotion::Both,
353 zoom_axes_modes: HashMap::new(),
354 zoom_baselines: HashMap::new(),
355 pan_mode: PanModeState::default(),
356 last_enabled_pan_motion: ZoomMotion::Both,
357 pan_axes_modes: HashMap::new(),
358 data_cursor_mode: DataCursorModeState::default(),
359 waitbar: None,
360 revision: 0,
361 }
362 }
363
364 fn hold(&self) -> bool {
365 *self.hold_per_axes.get(&self.active_axes).unwrap_or(&false)
366 }
367
368 fn set_hold(&mut self, hold: bool) {
369 self.hold_per_axes.insert(self.active_axes, hold);
370 }
371
372 fn cycle_for_axes_mut(&mut self, axes_index: usize) -> &mut LineStyleCycle {
373 self.line_style_cycles.entry(axes_index).or_default()
374 }
375
376 fn color_cycle_for_axes_mut(&mut self, axes_index: usize) -> &mut LineColorCycle {
377 match self.line_color_cycles.entry(axes_index) {
378 Entry::Occupied(entry) => entry.into_mut(),
379 Entry::Vacant(entry) => {
380 let mut cycle = LineColorCycle::default();
381 if let Some(order) = self.figure_color_order.as_deref() {
382 cycle.set_order(order);
383 }
384 entry.insert(cycle)
385 }
386 }
387 }
388
389 fn reset_cycle(&mut self, axes_index: usize) {
390 if let Some(cycle) = self.line_style_cycles.get_mut(&axes_index) {
391 cycle.reset_cursor();
392 }
393 if let Some(cycle) = self.line_color_cycles.get_mut(&axes_index) {
394 cycle.reset_cursor();
395 }
396 }
397}
398
399struct ActiveAxesContext {
400 axes_index: usize,
401 style_cycle_ptr: *mut LineStyleCycle,
402 color_cycle_ptr: *mut LineColorCycle,
403}
404
405struct AxesContextGuard {
406 _private: (),
407}
408
409impl AxesContextGuard {
410 fn install(state: &mut FigureState, axes_index: usize) -> Self {
411 let style_cycle_ptr = state.cycle_for_axes_mut(axes_index) as *mut LineStyleCycle;
412 let color_cycle_ptr = state.color_cycle_for_axes_mut(axes_index) as *mut LineColorCycle;
413 ACTIVE_AXES_CONTEXT.with(|ctx| {
414 debug_assert!(
415 ctx.borrow().is_none(),
416 "plot axes context already installed"
417 );
418 ctx.borrow_mut().replace(ActiveAxesContext {
419 axes_index,
420 style_cycle_ptr,
421 color_cycle_ptr,
422 });
423 });
424 Self { _private: () }
425 }
426}
427
428impl Drop for AxesContextGuard {
429 fn drop(&mut self) {
430 ACTIVE_AXES_CONTEXT.with(|ctx| {
431 ctx.borrow_mut().take();
432 });
433 }
434}
435
436fn with_active_style_cycle<R>(
437 axes_index: usize,
438 f: impl FnOnce(&mut LineStyleCycle) -> R,
439) -> Option<R> {
440 ACTIVE_AXES_CONTEXT.with(|ctx| {
441 let guard = ctx.borrow();
442 let active = guard.as_ref()?;
443 if active.axes_index != axes_index {
444 return None;
445 }
446 let cycle = unsafe { &mut *active.style_cycle_ptr };
447 Some(f(cycle))
448 })
449}
450
451fn with_active_color_cycle<R>(
452 axes_index: usize,
453 f: impl FnOnce(&mut LineColorCycle) -> R,
454) -> Option<R> {
455 ACTIVE_AXES_CONTEXT.with(|ctx| {
456 let guard = ctx.borrow();
457 let active = guard.as_ref()?;
458 if active.axes_index != axes_index {
459 return None;
460 }
461 let cycle = unsafe { &mut *active.color_cycle_ptr };
462 Some(f(cycle))
463 })
464}
465
466struct PlotRegistry {
467 current: FigureHandle,
468 next_handle: FigureHandle,
469 figures: HashMap<FigureHandle, FigureState>,
470 root_defaults: HashMap<String, RootPropertyEntry>,
471 root_units: String,
472 root_show_hidden_handles: bool,
473 next_plot_child_handle: u64,
474 plot_children: HashMap<u64, PlotChildHandleState>,
475 link_axes_groups: Vec<LinkAxesGroup>,
476}
477
478#[derive(Clone, Debug)]
479pub struct RootPropertyEntry {
480 pub display_name: String,
481 pub value: RootPropertyValue,
482}
483
484#[derive(Clone, Debug)]
485pub enum RootPropertyValue {
486 Bool(bool),
487 Num(f64),
488 String(String),
489 Tensor(Tensor),
490 StringArray {
491 rows: usize,
492 cols: usize,
493 shape: Vec<usize>,
494 data: Vec<String>,
495 },
496}
497
498#[derive(Clone, Debug)]
499pub struct HistogramHandleState {
500 pub figure: FigureHandle,
501 pub axes_index: usize,
502 pub plot_index: usize,
503 pub bin_edges: Vec<f64>,
504 pub raw_counts: Vec<f64>,
505 pub normalization: String,
506 pub display_name: Option<String>,
507 pub metadata: HistogramHandleMetadata,
508}
509
510#[derive(Clone, Debug)]
511pub struct HistogramHandleMetadata {
512 pub data: Option<Vec<f64>>,
513 pub display_style: String,
514 pub face_color: String,
515 pub face_alpha: f64,
516 pub edge_color: String,
517 pub bin_width: f64,
518 pub bin_limits: (f64, f64),
519 pub is_polar: bool,
520}
521
522impl HistogramHandleMetadata {
523 pub fn new(bin_edges: &[f64]) -> Self {
524 let bin_width = bin_edges
525 .windows(2)
526 .next()
527 .map(|pair| pair[1] - pair[0])
528 .unwrap_or(0.0);
529 let bin_limits = (
530 bin_edges.first().copied().unwrap_or(0.0),
531 bin_edges.last().copied().unwrap_or(0.0),
532 );
533 Self {
534 data: None,
535 display_style: "bar".into(),
536 face_color: "auto".into(),
537 face_alpha: 1.0,
538 edge_color: "auto".into(),
539 bin_width,
540 bin_limits,
541 is_polar: false,
542 }
543 }
544}
545
546#[derive(Clone, Debug)]
547pub struct Histogram2HandleState {
548 pub figure: FigureHandle,
549 pub axes_index: usize,
550 pub plot_index: usize,
551 pub values: Tensor,
552 pub raw_counts: Tensor,
553 pub x_bin_edges: Vec<f64>,
554 pub y_bin_edges: Vec<f64>,
555 pub normalization: String,
556 pub display_style: crate::builtins::plotting::histogram2::Histogram2DisplayStyle,
557 pub show_empty_bins: bool,
558 pub face_alpha: f64,
559 pub display_name: Option<String>,
560}
561
562#[derive(Clone, Debug)]
563pub struct StemHandleState {
564 pub figure: FigureHandle,
565 pub axes_index: usize,
566 pub plot_index: usize,
567}
568
569#[derive(Clone, Debug)]
570pub struct SimplePlotHandleState {
571 pub figure: FigureHandle,
572 pub axes_index: usize,
573 pub plot_index: usize,
574}
575
576#[derive(Clone, Debug)]
577pub struct AnimatedLineHandleState {
578 pub figure: FigureHandle,
579 pub axes_index: usize,
580 pub plot_index: usize,
581 pub is_3d: bool,
582 pub maximum_num_points: Option<usize>,
583}
584
585#[derive(Clone, Debug)]
586pub struct ErrorBarHandleState {
587 pub figure: FigureHandle,
588 pub axes_index: usize,
589 pub plot_index: usize,
590}
591
592#[derive(Clone, Debug)]
593pub struct QuiverHandleState {
594 pub figure: FigureHandle,
595 pub axes_index: usize,
596 pub plot_index: usize,
597 pub is_3d: bool,
598}
599
600#[derive(Clone, Debug)]
601pub struct ImageHandleState {
602 pub figure: FigureHandle,
603 pub axes_index: usize,
604 pub plot_index: usize,
605}
606
607#[derive(Clone, Debug)]
608pub struct HeatmapHandleState {
609 pub figure: FigureHandle,
610 pub axes_index: usize,
611 pub plot_index: usize,
612 pub x_labels: Vec<String>,
613 pub y_labels: Vec<String>,
614 pub color_data: Tensor,
615}
616
617#[derive(Clone, Debug)]
618pub struct BinscatterHandleState {
619 pub figure: FigureHandle,
620 pub axes_index: usize,
621 pub plot_index: usize,
622 pub values: Tensor,
623 pub x_bin_edges: Vec<f64>,
624 pub y_bin_edges: Vec<f64>,
625 pub x_data: Vec<f64>,
626 pub y_data: Vec<f64>,
627 pub num_bins: [usize; 2],
628 pub auto_bins: bool,
629 pub x_limits_option: Option<(f64, f64)>,
630 pub y_limits_option: Option<(f64, f64)>,
631 pub x_limits: (f64, f64),
632 pub y_limits: (f64, f64),
633 pub show_empty_bins: bool,
634 pub face_alpha: f64,
635 pub display_name: Option<String>,
636}
637
638#[derive(Clone, Debug)]
639pub struct FunctionSurfaceHandleState {
640 pub figure: FigureHandle,
641 pub axes_index: usize,
642 pub plot_index: usize,
643 pub mesh_density: usize,
644 pub x_range: (f64, f64),
645 pub y_range: (f64, f64),
646 pub function: FunctionSurfaceFunctionState,
647}
648
649#[derive(Clone, Debug)]
650pub enum FunctionSurfaceFunctionState {
651 Explicit(FunctionSurfaceFunctionRef),
652 Parametric {
653 x: FunctionSurfaceFunctionRef,
654 y: FunctionSurfaceFunctionRef,
655 z: FunctionSurfaceFunctionRef,
656 },
657}
658
659#[derive(Clone, Debug)]
660pub enum FunctionSurfaceFunctionRef {
661 FunctionHandle(String),
662 ExternalFunctionHandle(String),
663 MethodFunctionHandle(String),
664 BoundFunctionHandle {
665 name: String,
666 function: usize,
667 },
668 ClosureSummary {
669 function_name: String,
670 bound_function: Option<usize>,
671 },
672}
673
674#[derive(Clone, Debug)]
675pub struct FunctionContourHandleState {
676 pub figure: FigureHandle,
677 pub axes_index: usize,
678 pub plot_index: usize,
679 pub mesh_density: usize,
680 pub x_range: (f64, f64),
681 pub y_range: (f64, f64),
682 pub function: FunctionSurfaceFunctionRef,
683}
684
685#[derive(Clone, Debug)]
686pub struct AreaHandleState {
687 pub figure: FigureHandle,
688 pub axes_index: usize,
689 pub plot_index: usize,
690}
691
692#[derive(Clone, Debug)]
693pub struct TextAnnotationHandleState {
694 pub figure: FigureHandle,
695 pub axes_index: usize,
696 pub annotation_index: usize,
697}
698
699#[derive(Clone, Debug)]
700pub struct TextScatterHandleState {
701 pub figure: FigureHandle,
702 pub axes_index: usize,
703 pub annotation_indices: Vec<usize>,
704 pub marker_plot_index: Option<usize>,
705 pub is_3d: bool,
706 pub text_data: Vec<String>,
707 pub text_density_percentage: f64,
708 pub max_text_length: usize,
709 pub marker_color: TextScatterMarkerColor,
710 pub marker_size: f64,
711 pub color_data: Option<Vec<glam::Vec4>>,
712 pub colors: Vec<glam::Vec4>,
713 pub visible: bool,
714 pub base_style: runmat_plot::plots::TextStyle,
715}
716
717#[derive(Clone, Debug, PartialEq)]
718pub enum TextScatterMarkerColor {
719 Auto,
720 None,
721 Color(glam::Vec4),
722}
723
724#[derive(Clone, Debug)]
725pub struct WordCloudHandleState {
726 pub figure: FigureHandle,
727 pub axes_index: usize,
728 pub annotation_indices: Vec<usize>,
729 pub word_data: Vec<String>,
730 pub size_data: Vec<f64>,
731 pub word_variable: String,
732 pub size_variable: String,
733 pub max_display_words: usize,
734 pub color: Vec<glam::Vec4>,
735 pub highlight_color: glam::Vec4,
736 pub shape: String,
737 pub layout_num: usize,
738 pub size_power: f64,
739 pub title: String,
740 pub title_font_name: String,
741 pub visible: bool,
742 pub font_name: String,
743 pub box_visible: bool,
744 pub units: String,
745 pub position: [f64; 4],
746 pub handle_visibility: String,
747 pub display_name: String,
748 pub tag: String,
749}
750
751#[derive(Clone, Debug)]
752pub struct StackedSourceTableSnapshot {
753 pub classes: Vec<String>,
754 pub variable_names: Vec<Vec<String>>,
755}
756
757#[derive(Clone, Debug)]
758pub struct StackedPlotHandleState {
759 pub figure: FigureHandle,
760 pub axes_indices: Vec<usize>,
761 pub line_plot_indices: Vec<usize>,
762 pub line_group_counts: Vec<usize>,
763 pub line_labels: Vec<String>,
764 pub x_data: Vec<f64>,
765 pub y_data: Vec<Vec<f64>>,
766 pub display_variables: Vec<String>,
767 pub source_table: Option<StackedSourceTableSnapshot>,
768 pub x_variable: Vec<String>,
769 pub combine_matching_names: bool,
770 pub x_label: String,
771 pub title: String,
772 pub appearance: crate::builtins::plotting::style::LineAppearance,
773 pub visible: bool,
774 pub grid_visible: bool,
775 pub x_limits: Option<(f64, f64)>,
776}
777
778#[derive(Clone, Debug)]
779pub enum PlotChildHandleState {
780 Histogram(HistogramHandleState),
781 Histogram2(Histogram2HandleState),
782 Line(SimplePlotHandleState),
783 AnimatedLine(AnimatedLineHandleState),
784 Scatter(SimplePlotHandleState),
785 Bar(SimplePlotHandleState),
786 Stem(StemHandleState),
787 ErrorBar(ErrorBarHandleState),
788 Stairs(SimplePlotHandleState),
789 Quiver(QuiverHandleState),
790 Image(ImageHandleState),
791 Heatmap(HeatmapHandleState),
792 Binscatter(BinscatterHandleState),
793 FunctionSurface(FunctionSurfaceHandleState),
794 FunctionContour(FunctionContourHandleState),
795 Area(AreaHandleState),
796 Surface(SimplePlotHandleState),
797 Patch(SimplePlotHandleState),
798 Line3(SimplePlotHandleState),
799 Scatter3(SimplePlotHandleState),
800 Contour(SimplePlotHandleState),
801 ContourFill(SimplePlotHandleState),
802 ReferenceLine(SimplePlotHandleState),
803 Pie(SimplePlotHandleState),
804 Text(TextAnnotationHandleState),
805 TextScatter(TextScatterHandleState),
806 WordCloud(WordCloudHandleState),
807 StackedPlot(StackedPlotHandleState),
808}
809
810impl PlotChildHandleState {
811 pub fn figure_axes(&self) -> (FigureHandle, usize) {
812 match self {
813 Self::Histogram(state) => (state.figure, state.axes_index),
814 Self::Histogram2(state) => (state.figure, state.axes_index),
815 Self::Line(state)
816 | Self::Scatter(state)
817 | Self::Bar(state)
818 | Self::Stairs(state)
819 | Self::Surface(state)
820 | Self::Patch(state)
821 | Self::Line3(state)
822 | Self::Scatter3(state)
823 | Self::Contour(state)
824 | Self::ContourFill(state)
825 | Self::ReferenceLine(state)
826 | Self::Pie(state) => (state.figure, state.axes_index),
827 Self::AnimatedLine(state) => (state.figure, state.axes_index),
828 Self::Stem(state) => (state.figure, state.axes_index),
829 Self::ErrorBar(state) => (state.figure, state.axes_index),
830 Self::Quiver(state) => (state.figure, state.axes_index),
831 Self::Image(state) => (state.figure, state.axes_index),
832 Self::Heatmap(state) => (state.figure, state.axes_index),
833 Self::Binscatter(state) => (state.figure, state.axes_index),
834 Self::FunctionSurface(state) => (state.figure, state.axes_index),
835 Self::FunctionContour(state) => (state.figure, state.axes_index),
836 Self::Area(state) => (state.figure, state.axes_index),
837 Self::Text(state) => (state.figure, state.axes_index),
838 Self::TextScatter(state) => (state.figure, state.axes_index),
839 Self::WordCloud(state) => (state.figure, state.axes_index),
840 Self::StackedPlot(state) => (
841 state.figure,
842 state.axes_indices.first().copied().unwrap_or(0),
843 ),
844 }
845 }
846
847 pub fn plot_index(&self) -> Option<usize> {
848 Some(match self {
849 Self::Histogram(state) => state.plot_index,
850 Self::Histogram2(state) => state.plot_index,
851 Self::Line(state)
852 | Self::Scatter(state)
853 | Self::Bar(state)
854 | Self::Stairs(state)
855 | Self::Surface(state)
856 | Self::Patch(state)
857 | Self::Line3(state)
858 | Self::Scatter3(state)
859 | Self::Contour(state)
860 | Self::ContourFill(state)
861 | Self::ReferenceLine(state)
862 | Self::Pie(state) => state.plot_index,
863 Self::AnimatedLine(state) => state.plot_index,
864 Self::Stem(state) => state.plot_index,
865 Self::ErrorBar(state) => state.plot_index,
866 Self::Quiver(state) => state.plot_index,
867 Self::Image(state) => state.plot_index,
868 Self::Heatmap(state) => state.plot_index,
869 Self::Binscatter(state) => state.plot_index,
870 Self::FunctionSurface(state) => state.plot_index,
871 Self::FunctionContour(state) => state.plot_index,
872 Self::Area(state) => state.plot_index,
873 Self::Text(_) => return None,
874 Self::TextScatter(state) => return state.marker_plot_index,
875 Self::WordCloud(_) => return None,
876 Self::StackedPlot(state) => return state.line_plot_indices.first().copied(),
877 })
878 }
879
880 pub fn with_plot_location(
881 &self,
882 figure: FigureHandle,
883 axes_index: usize,
884 plot_index: usize,
885 ) -> Option<Self> {
886 Some(match self {
887 Self::Histogram(state) => Self::Histogram(HistogramHandleState {
888 figure,
889 axes_index,
890 plot_index,
891 bin_edges: state.bin_edges.clone(),
892 raw_counts: state.raw_counts.clone(),
893 normalization: state.normalization.clone(),
894 display_name: state.display_name.clone(),
895 metadata: state.metadata.clone(),
896 }),
897 Self::Histogram2(state) => Self::Histogram2(Histogram2HandleState {
898 figure,
899 axes_index,
900 plot_index,
901 values: state.values.clone(),
902 raw_counts: state.raw_counts.clone(),
903 x_bin_edges: state.x_bin_edges.clone(),
904 y_bin_edges: state.y_bin_edges.clone(),
905 normalization: state.normalization.clone(),
906 display_style: state.display_style,
907 show_empty_bins: state.show_empty_bins,
908 face_alpha: state.face_alpha,
909 display_name: state.display_name.clone(),
910 }),
911 Self::Line(_) => Self::Line(SimplePlotHandleState {
912 figure,
913 axes_index,
914 plot_index,
915 }),
916 Self::Scatter(_) => Self::Scatter(SimplePlotHandleState {
917 figure,
918 axes_index,
919 plot_index,
920 }),
921 Self::Bar(_) => Self::Bar(SimplePlotHandleState {
922 figure,
923 axes_index,
924 plot_index,
925 }),
926 Self::Stairs(_) => Self::Stairs(SimplePlotHandleState {
927 figure,
928 axes_index,
929 plot_index,
930 }),
931 Self::Surface(_) => Self::Surface(SimplePlotHandleState {
932 figure,
933 axes_index,
934 plot_index,
935 }),
936 Self::Patch(_) => Self::Patch(SimplePlotHandleState {
937 figure,
938 axes_index,
939 plot_index,
940 }),
941 Self::Line3(_) => Self::Line3(SimplePlotHandleState {
942 figure,
943 axes_index,
944 plot_index,
945 }),
946 Self::Scatter3(_) => Self::Scatter3(SimplePlotHandleState {
947 figure,
948 axes_index,
949 plot_index,
950 }),
951 Self::Contour(_) => Self::Contour(SimplePlotHandleState {
952 figure,
953 axes_index,
954 plot_index,
955 }),
956 Self::ContourFill(_) => Self::ContourFill(SimplePlotHandleState {
957 figure,
958 axes_index,
959 plot_index,
960 }),
961 Self::ReferenceLine(_) => Self::ReferenceLine(SimplePlotHandleState {
962 figure,
963 axes_index,
964 plot_index,
965 }),
966 Self::Pie(_) => Self::Pie(SimplePlotHandleState {
967 figure,
968 axes_index,
969 plot_index,
970 }),
971 Self::AnimatedLine(state) => Self::AnimatedLine(AnimatedLineHandleState {
972 figure,
973 axes_index,
974 plot_index,
975 is_3d: state.is_3d,
976 maximum_num_points: state.maximum_num_points,
977 }),
978 Self::Stem(_) => Self::Stem(StemHandleState {
979 figure,
980 axes_index,
981 plot_index,
982 }),
983 Self::ErrorBar(_) => Self::ErrorBar(ErrorBarHandleState {
984 figure,
985 axes_index,
986 plot_index,
987 }),
988 Self::Quiver(state) => Self::Quiver(QuiverHandleState {
989 figure,
990 axes_index,
991 plot_index,
992 is_3d: state.is_3d,
993 }),
994 Self::Image(_) => Self::Image(ImageHandleState {
995 figure,
996 axes_index,
997 plot_index,
998 }),
999 Self::Heatmap(state) => Self::Heatmap(HeatmapHandleState {
1000 figure,
1001 axes_index,
1002 plot_index,
1003 x_labels: state.x_labels.clone(),
1004 y_labels: state.y_labels.clone(),
1005 color_data: state.color_data.clone(),
1006 }),
1007 Self::Binscatter(state) => Self::Binscatter(BinscatterHandleState {
1008 figure,
1009 axes_index,
1010 plot_index,
1011 values: state.values.clone(),
1012 x_bin_edges: state.x_bin_edges.clone(),
1013 y_bin_edges: state.y_bin_edges.clone(),
1014 x_data: state.x_data.clone(),
1015 y_data: state.y_data.clone(),
1016 num_bins: state.num_bins,
1017 auto_bins: state.auto_bins,
1018 x_limits_option: state.x_limits_option,
1019 y_limits_option: state.y_limits_option,
1020 x_limits: state.x_limits,
1021 y_limits: state.y_limits,
1022 show_empty_bins: state.show_empty_bins,
1023 face_alpha: state.face_alpha,
1024 display_name: state.display_name.clone(),
1025 }),
1026 Self::FunctionSurface(state) => Self::FunctionSurface(FunctionSurfaceHandleState {
1027 figure,
1028 axes_index,
1029 plot_index,
1030 mesh_density: state.mesh_density,
1031 x_range: state.x_range,
1032 y_range: state.y_range,
1033 function: state.function.clone(),
1034 }),
1035 Self::FunctionContour(state) => Self::FunctionContour(FunctionContourHandleState {
1036 figure,
1037 axes_index,
1038 plot_index,
1039 mesh_density: state.mesh_density,
1040 x_range: state.x_range,
1041 y_range: state.y_range,
1042 function: state.function.clone(),
1043 }),
1044 Self::Area(_) => Self::Area(AreaHandleState {
1045 figure,
1046 axes_index,
1047 plot_index,
1048 }),
1049 Self::Text(_) => return None,
1050 Self::TextScatter(_) => return None,
1051 Self::WordCloud(_) => return None,
1052 Self::StackedPlot(_) => return None,
1053 })
1054 }
1055
1056 pub fn type_name(&self) -> &'static str {
1057 match self {
1058 Self::Histogram(_) => "histogram",
1059 Self::Histogram2(_) => "histogram2",
1060 Self::Line(_) | Self::Line3(_) => "line",
1061 Self::AnimatedLine(_) => "animatedline",
1062 Self::Scatter(_) | Self::Scatter3(_) => "scatter",
1063 Self::Bar(_) => "bar",
1064 Self::Stem(_) => "stem",
1065 Self::ErrorBar(_) => "errorbar",
1066 Self::Stairs(_) => "stairs",
1067 Self::Quiver(_) => "quiver",
1068 Self::Image(_) => "image",
1069 Self::Heatmap(_) => "heatmap",
1070 Self::Binscatter(_) => "binscatter",
1071 Self::FunctionSurface(_) => "functionsurface",
1072 Self::FunctionContour(_) => "functioncontour",
1073 Self::Area(_) => "area",
1074 Self::Surface(_) => "surface",
1075 Self::Patch(_) => "patch",
1076 Self::Contour(_) | Self::ContourFill(_) => "contour",
1077 Self::ReferenceLine(_) => "constantline",
1078 Self::Pie(_) => "pie",
1079 Self::Text(_) => "text",
1080 Self::TextScatter(_) => "textscatter",
1081 Self::WordCloud(_) => "wordcloud",
1082 Self::StackedPlot(_) => "stackedplot",
1083 }
1084 }
1085}
1086
1087impl Default for PlotRegistry {
1088 fn default() -> Self {
1089 Self {
1090 current: FigureHandle::default(),
1091 next_handle: FigureHandle::default().next(),
1092 figures: HashMap::new(),
1093 root_defaults: HashMap::new(),
1094 root_units: "pixels".to_string(),
1095 root_show_hidden_handles: false,
1096 next_plot_child_handle: 1u64 << 40,
1097 plot_children: HashMap::new(),
1098 link_axes_groups: Vec::new(),
1099 }
1100 }
1101}
1102
1103#[cfg(not(target_arch = "wasm32"))]
1104static REGISTRY: OnceCell<Mutex<PlotRegistry>> = OnceCell::new();
1105
1106static TEST_PLOT_REGISTRY_LOCK: Mutex<()> = Mutex::new(());
1107
1108thread_local! {
1109 static TEST_PLOT_OUTER_LOCK_HELD: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1110}
1111
1112#[doc(hidden)]
1113pub struct PlotTestLockGuard {
1114 _guard: std::sync::MutexGuard<'static, ()>,
1115 disable_previous: Option<std::ffi::OsString>,
1116 host_previous: Option<std::ffi::OsString>,
1117}
1118
1119impl Drop for PlotTestLockGuard {
1120 fn drop(&mut self) {
1121 restore_env_var(
1122 "RUNMAT_DISABLE_INTERACTIVE_PLOTS",
1123 self.disable_previous.take(),
1124 );
1125 restore_env_var("RUNMAT_HOST_MANAGED_PLOTS", self.host_previous.take());
1126 TEST_PLOT_OUTER_LOCK_HELD.with(|flag| flag.set(false));
1127 }
1128}
1129
1130impl PlotTestLockGuard {
1131 #[doc(hidden)]
1132 pub fn disable_host_managed_plot_env(&self) -> HostManagedPlotEnvGuard<'_> {
1133 let previous = std::env::var_os("RUNMAT_HOST_MANAGED_PLOTS");
1136 unsafe {
1137 std::env::remove_var("RUNMAT_HOST_MANAGED_PLOTS");
1138 }
1139 HostManagedPlotEnvGuard {
1140 _plot_guard: self,
1141 previous,
1142 }
1143 }
1144}
1145
1146#[doc(hidden)]
1147pub fn lock_plot_test_registry() -> PlotTestLockGuard {
1148 let guard = TEST_PLOT_REGISTRY_LOCK
1149 .lock()
1150 .unwrap_or_else(|e| e.into_inner());
1151 TEST_PLOT_OUTER_LOCK_HELD.with(|flag| flag.set(true));
1152 let disable_previous = std::env::var_os("RUNMAT_DISABLE_INTERACTIVE_PLOTS");
1153 let host_previous = std::env::var_os("RUNMAT_HOST_MANAGED_PLOTS");
1154 set_plot_test_env_vars();
1155 PlotTestLockGuard {
1156 _guard: guard,
1157 disable_previous,
1158 host_previous,
1159 }
1160}
1161
1162#[doc(hidden)]
1163pub struct HostManagedPlotEnvGuard<'a> {
1164 _plot_guard: &'a PlotTestLockGuard,
1165 previous: Option<std::ffi::OsString>,
1166}
1167
1168impl Drop for HostManagedPlotEnvGuard<'_> {
1169 fn drop(&mut self) {
1170 restore_env_var("RUNMAT_HOST_MANAGED_PLOTS", self.previous.take());
1171 }
1172}
1173
1174fn set_plot_test_env_vars() {
1175 unsafe {
1176 std::env::set_var("RUNMAT_DISABLE_INTERACTIVE_PLOTS", "1");
1177 std::env::set_var("RUNMAT_HOST_MANAGED_PLOTS", "1");
1178 }
1179}
1180
1181fn restore_env_var(key: &'static str, previous: Option<std::ffi::OsString>) {
1182 unsafe {
1183 if let Some(previous) = previous {
1184 std::env::set_var(key, previous);
1185 } else {
1186 std::env::remove_var(key);
1187 }
1188 }
1189}
1190
1191#[cfg(target_arch = "wasm32")]
1192runmat_thread_local! {
1193 static REGISTRY: RefCell<PlotRegistry> = RefCell::new(PlotRegistry::default());
1194}
1195
1196#[cfg(not(target_arch = "wasm32"))]
1197type RegistryBackendGuard<'a> = MutexGuard<'a, PlotRegistry>;
1198#[cfg(target_arch = "wasm32")]
1199type RegistryBackendGuard<'a> = std::cell::RefMut<'a, PlotRegistry>;
1200
1201struct PlotRegistryGuard<'a> {
1202 inner: RegistryBackendGuard<'a>,
1203 #[cfg(test)]
1204 _test_lock: Option<std::sync::MutexGuard<'static, ()>>,
1205}
1206
1207impl<'a> PlotRegistryGuard<'a> {
1208 #[cfg(test)]
1209 fn new(
1210 inner: RegistryBackendGuard<'a>,
1211 _test_lock: Option<std::sync::MutexGuard<'static, ()>>,
1212 ) -> Self {
1213 Self { inner, _test_lock }
1214 }
1215
1216 #[cfg(not(test))]
1217 fn new(inner: RegistryBackendGuard<'a>) -> Self {
1218 Self { inner }
1219 }
1220}
1221
1222impl<'a> Deref for PlotRegistryGuard<'a> {
1223 type Target = PlotRegistry;
1224
1225 fn deref(&self) -> &Self::Target {
1226 &self.inner
1227 }
1228}
1229
1230impl<'a> DerefMut for PlotRegistryGuard<'a> {
1231 fn deref_mut(&mut self) -> &mut Self::Target {
1232 &mut self.inner
1233 }
1234}
1235
1236const AXES_INDEX_BITS: u32 = 20;
1237const AXES_INDEX_MASK: u64 = (1 << AXES_INDEX_BITS) - 1;
1238const OBJECT_KIND_BITS: u32 = 4;
1239const OBJECT_KIND_MASK: u64 = (1 << OBJECT_KIND_BITS) - 1;
1240
1241#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1242pub enum PlotObjectKind {
1243 Title = 1,
1244 XLabel = 2,
1245 YLabel = 3,
1246 ZLabel = 4,
1247 Legend = 5,
1248 SuperTitle = 6,
1249 Subtitle = 7,
1250 XAxis = 8,
1251 YAxis = 9,
1252}
1253
1254impl PlotObjectKind {
1255 fn from_u64(value: u64) -> Option<Self> {
1256 match value {
1257 1 => Some(Self::Title),
1258 2 => Some(Self::XLabel),
1259 3 => Some(Self::YLabel),
1260 4 => Some(Self::ZLabel),
1261 5 => Some(Self::Legend),
1262 6 => Some(Self::SuperTitle),
1263 7 => Some(Self::Subtitle),
1264 8 => Some(Self::XAxis),
1265 9 => Some(Self::YAxis),
1266 _ => None,
1267 }
1268 }
1269}
1270
1271#[derive(Debug, Error)]
1272pub enum FigureError {
1273 #[error("figure handle {0} does not exist")]
1274 InvalidHandle(u32),
1275 #[error("subplot grid dimensions must be positive (rows={rows}, cols={cols})")]
1276 InvalidSubplotGrid { rows: usize, cols: usize },
1277 #[error("subplot index {index} is out of range for a {rows}x{cols} grid")]
1278 InvalidSubplotIndex {
1279 rows: usize,
1280 cols: usize,
1281 index: usize,
1282 },
1283 #[error("invalid axes handle")]
1284 InvalidAxesHandle,
1285 #[error("invalid plot object handle")]
1286 InvalidPlotObjectHandle,
1287 #[error("failed to render figure snapshot: {source}")]
1288 RenderFailure {
1289 #[source]
1290 source: Box<dyn std::error::Error + Send + Sync>,
1291 },
1292}
1293
1294fn map_figure_error(builtin: &'static str, err: FigureError) -> RuntimeError {
1295 let message = format!("{builtin}: {err}");
1296 plotting_error_with_source(builtin, message, err)
1297}
1298
1299pub(crate) fn clear_figure_with_builtin(
1300 builtin: &'static str,
1301 target: Option<FigureHandle>,
1302) -> BuiltinResult<FigureHandle> {
1303 clear_figure(target).map_err(|err| map_figure_error(builtin, err))
1304}
1305
1306pub(crate) fn close_figure_with_builtin(
1307 builtin: &'static str,
1308 target: Option<FigureHandle>,
1309) -> BuiltinResult<FigureHandle> {
1310 close_figure(target).map_err(|err| map_figure_error(builtin, err))
1311}
1312
1313pub fn set_grid_enabled(enabled: bool) {
1314 let (handle, figure_clone) = {
1315 let mut reg = registry();
1316 let handle = reg.current;
1317 let state = get_state_mut(&mut reg, handle);
1318 let axes = state.active_axes;
1319 state.figure.set_axes_grid_enabled(axes, enabled);
1320 state.revision = state.revision.wrapping_add(1);
1321 (handle, state.figure.clone())
1322 };
1323 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1324}
1325
1326pub fn set_grid_and_minor_grid_enabled(grid_enabled: bool, minor_grid_enabled: Option<bool>) {
1327 let (handle, figure_clone) = {
1328 let mut reg = registry();
1329 let handle = reg.current;
1330 let state = get_state_mut(&mut reg, handle);
1331 let axes = state.active_axes;
1332 state.figure.set_axes_grid_enabled(axes, grid_enabled);
1333 if let Some(minor_enabled) = minor_grid_enabled {
1334 state
1335 .figure
1336 .set_axes_minor_grid_enabled(axes, minor_enabled);
1337 }
1338 state.revision = state.revision.wrapping_add(1);
1339 (handle, state.figure.clone())
1340 };
1341 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1342}
1343
1344pub fn set_grid_enabled_for_axes(
1345 handle: FigureHandle,
1346 axes_index: usize,
1347 enabled: bool,
1348) -> Result<(), FigureError> {
1349 let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1350 state.figure.set_axes_grid_enabled(axes_index, enabled);
1351 })?;
1352 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1353 Ok(())
1354}
1355
1356pub fn set_minor_grid_enabled_for_axes(
1357 handle: FigureHandle,
1358 axes_index: usize,
1359 enabled: bool,
1360) -> Result<(), FigureError> {
1361 let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1362 state
1363 .figure
1364 .set_axes_minor_grid_enabled(axes_index, enabled);
1365 })?;
1366 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1367 Ok(())
1368}
1369
1370pub fn toggle_grid() -> bool {
1371 let (handle, figure_clone, enabled) = {
1372 let mut reg = registry();
1373 let handle = reg.current;
1374 let state = get_state_mut(&mut reg, handle);
1375 let axes = state.active_axes;
1376 let next = !state
1377 .figure
1378 .axes_metadata(axes)
1379 .map(|m| m.grid_enabled)
1380 .unwrap_or(true);
1381 state.figure.set_axes_grid_enabled(axes, next);
1382 state.revision = state.revision.wrapping_add(1);
1383 (handle, state.figure.clone(), next)
1384 };
1385 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1386 enabled
1387}
1388
1389pub fn toggle_minor_grid() -> bool {
1390 let (handle, figure_clone, enabled) = {
1391 let mut reg = registry();
1392 let handle = reg.current;
1393 let state = get_state_mut(&mut reg, handle);
1394 let axes = state.active_axes;
1395 let next = !state.figure.minor_grid_enabled_for_axes(axes);
1396 state.figure.set_axes_minor_grid_enabled(axes, next);
1397 state.revision = state.revision.wrapping_add(1);
1398 (handle, state.figure.clone(), next)
1399 };
1400 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1401 enabled
1402}
1403
1404pub fn set_box_enabled(enabled: bool) {
1405 let (handle, figure_clone) = {
1406 let mut reg = registry();
1407 let handle = reg.current;
1408 let state = get_state_mut(&mut reg, handle);
1409 let axes = state.active_axes;
1410 state.figure.set_axes_box_enabled(axes, enabled);
1411 state.revision = state.revision.wrapping_add(1);
1412 (handle, state.figure.clone())
1413 };
1414 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1415}
1416
1417pub fn set_box_enabled_for_axes(
1418 handle: FigureHandle,
1419 axes_index: usize,
1420 enabled: bool,
1421) -> Result<(), FigureError> {
1422 let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1423 state.figure.set_axes_box_enabled(axes_index, enabled);
1424 })?;
1425 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1426 Ok(())
1427}
1428
1429pub fn set_hidden_line_removal_for_axes(
1430 handle: FigureHandle,
1431 axes_index: usize,
1432 enabled: bool,
1433) -> Result<(), FigureError> {
1434 let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1435 state
1436 .figure
1437 .set_axes_hidden_line_removal(axes_index, enabled);
1438 })?;
1439 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1440 Ok(())
1441}
1442
1443pub fn set_axes_style_for_axes(
1444 handle: FigureHandle,
1445 axes_index: usize,
1446 style: TextStyle,
1447) -> Result<(), FigureError> {
1448 let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1449 state.figure.set_axes_style(axes_index, style);
1450 })?;
1451 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1452 Ok(())
1453}
1454
1455pub fn default_color_order() -> Vec<Vec4> {
1456 let theme = current_plot_theme_config().build_theme();
1457 (0..8).map(|idx| theme.get_data_color(idx)).collect()
1458}
1459
1460pub fn color_order_for_axes(
1461 handle: FigureHandle,
1462 axes_index: usize,
1463) -> Result<Vec<Vec4>, FigureError> {
1464 let mut reg = registry();
1465 let state = get_state_mut(&mut reg, handle);
1466 let total_axes = axes_count(state);
1467 if axes_index >= total_axes {
1468 return Err(FigureError::InvalidSubplotIndex {
1469 rows: state.figure.axes_rows.max(1),
1470 cols: state.figure.axes_cols.max(1),
1471 index: axes_index,
1472 });
1473 }
1474 if let Some(order) = state
1475 .figure
1476 .axes_metadata(axes_index)
1477 .and_then(|meta| meta.color_order.clone())
1478 {
1479 return Ok(order);
1480 }
1481 Ok(state
1482 .figure_color_order
1483 .clone()
1484 .unwrap_or_else(default_color_order))
1485}
1486
1487pub fn color_order_for_figure(handle: FigureHandle) -> Result<Vec<Vec4>, FigureError> {
1488 let mut reg = registry();
1489 let state = get_state_mut(&mut reg, handle);
1490 Ok(state
1491 .figure_color_order
1492 .clone()
1493 .unwrap_or_else(default_color_order))
1494}
1495
1496pub fn set_color_order_for_axes(
1497 handle: FigureHandle,
1498 axes_index: usize,
1499 colors: &[Vec4],
1500) -> Result<(), FigureError> {
1501 let figure_clone = {
1502 let mut reg = registry();
1503 let state = get_state_mut(&mut reg, handle);
1504 let total_axes = axes_count(state);
1505 if axes_index >= total_axes {
1506 return Err(FigureError::InvalidSubplotIndex {
1507 rows: state.figure.axes_rows.max(1),
1508 cols: state.figure.axes_cols.max(1),
1509 index: axes_index,
1510 });
1511 }
1512 state.color_cycle_for_axes_mut(axes_index).set_order(colors);
1513 state
1514 .figure
1515 .set_axes_color_order(axes_index, colors.to_vec());
1516 state.revision = state.revision.wrapping_add(1);
1517 state.figure.clone()
1518 };
1519 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1520 Ok(())
1521}
1522
1523pub fn set_color_order_for_figure(
1524 handle: FigureHandle,
1525 colors: &[Vec4],
1526) -> Result<(), FigureError> {
1527 let figure_clone = {
1528 let mut reg = registry();
1529 let state = get_state_mut(&mut reg, handle);
1530 state.figure_color_order = Some(colors.to_vec());
1531 let total_axes = axes_count(state);
1532 for axes_index in 0..total_axes {
1533 state.color_cycle_for_axes_mut(axes_index).set_order(colors);
1534 }
1535 state.figure.set_all_axes_color_order(colors.to_vec());
1536 state.revision = state.revision.wrapping_add(1);
1537 state.figure.clone()
1538 };
1539 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1540 Ok(())
1541}
1542
1543pub fn set_figure_title_for_axes(
1544 handle: FigureHandle,
1545 axes_index: usize,
1546 title: &str,
1547 style: TextStyle,
1548) -> Result<f64, FigureError> {
1549 let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1550 state.figure.set_axes_title(axes_index, title.to_string());
1551 state.figure.set_axes_title_style(axes_index, style);
1552 encode_plot_object_handle(handle, axes_index, PlotObjectKind::Title)
1553 })?;
1554 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1555 Ok(object_handle)
1556}
1557
1558pub fn set_figure_subtitle_for_axes(
1559 handle: FigureHandle,
1560 axes_index: usize,
1561 subtitle: &str,
1562 style: TextStyle,
1563) -> Result<f64, FigureError> {
1564 let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1565 state
1566 .figure
1567 .set_axes_subtitle(axes_index, subtitle.to_string());
1568 state.figure.set_axes_subtitle_style(axes_index, style);
1569 encode_plot_object_handle(handle, axes_index, PlotObjectKind::Subtitle)
1570 })?;
1571 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1572 Ok(object_handle)
1573}
1574
1575pub fn set_sg_title_for_figure(
1576 handle: FigureHandle,
1577 title: &str,
1578 style: TextStyle,
1579) -> Result<f64, FigureError> {
1580 let (object_handle, figure_clone) = with_figure_mut(handle, |state| {
1581 state.figure.set_sg_title(title.to_string());
1582 state.figure.set_sg_title_style(style);
1583 encode_plot_object_handle(handle, 0, PlotObjectKind::SuperTitle)
1584 })?;
1585 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1586 Ok(object_handle)
1587}
1588
1589pub fn set_sg_title_properties_for_figure(
1590 handle: FigureHandle,
1591 text: Option<String>,
1592 style: Option<TextStyle>,
1593) -> Result<f64, FigureError> {
1594 let (object_handle, figure_clone) = with_figure_mut(handle, |state| {
1595 if let Some(text) = text {
1596 state.figure.set_sg_title(text);
1597 }
1598 if let Some(style) = style {
1599 state.figure.set_sg_title_style(style);
1600 }
1601 encode_plot_object_handle(handle, 0, PlotObjectKind::SuperTitle)
1602 })?;
1603 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1604 Ok(object_handle)
1605}
1606
1607pub fn set_figure_name(handle: FigureHandle, name: String) -> Result<(), FigureError> {
1608 let ((), figure_clone) = with_figure_mut(handle, |state| {
1609 state.figure.set_name(name);
1610 })?;
1611 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1612 Ok(())
1613}
1614
1615pub fn set_figure_tag(handle: FigureHandle, tag: String) -> Result<(), FigureError> {
1616 let ((), figure_clone) = with_figure_mut(handle, |state| {
1617 state.tag = tag;
1618 })?;
1619 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1620 Ok(())
1621}
1622
1623pub fn set_figure_number_title(handle: FigureHandle, enabled: bool) -> Result<(), FigureError> {
1624 let ((), figure_clone) = with_figure_mut(handle, |state| {
1625 state.figure.set_number_title(enabled);
1626 })?;
1627 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1628 Ok(())
1629}
1630
1631pub fn set_figure_visible(
1632 handle: FigureHandle,
1633 visible: bool,
1634) -> Result<(bool, Figure), FigureError> {
1635 let ((was_visible, now_visible), figure_clone) = with_figure_mut(handle, |state| {
1636 let was_visible = state.figure.visible;
1637 state.figure.set_visible(visible);
1638 (was_visible, state.figure.visible)
1639 })?;
1640 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1641 Ok((!was_visible && now_visible, figure_clone))
1642}
1643
1644pub fn set_figure_position(handle: FigureHandle, position: [f64; 4]) -> Result<(), FigureError> {
1645 let ((), figure_clone) = with_figure_mut(handle, |state| {
1646 state.figure.set_position(position);
1647 })?;
1648 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1649 Ok(())
1650}
1651
1652pub fn set_figure_background_color(handle: FigureHandle, color: Vec4) -> Result<(), FigureError> {
1653 let ((), figure_clone) = with_figure_mut(handle, |state| {
1654 state.figure.set_background_color(color);
1655 })?;
1656 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1657 Ok(())
1658}
1659
1660pub fn set_text_properties_for_axes(
1661 handle: FigureHandle,
1662 axes_index: usize,
1663 kind: PlotObjectKind,
1664 text: Option<String>,
1665 style: Option<TextStyle>,
1666) -> Result<f64, FigureError> {
1667 let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1668 if let Some(text) = text {
1669 match kind {
1670 PlotObjectKind::Title => state.figure.set_axes_title(axes_index, text),
1671 PlotObjectKind::Subtitle => state.figure.set_axes_subtitle(axes_index, text),
1672 PlotObjectKind::XLabel => state.figure.set_axes_xlabel(axes_index, text),
1673 PlotObjectKind::YLabel => state.figure.set_axes_ylabel(axes_index, text),
1674 PlotObjectKind::ZLabel => state.figure.set_axes_zlabel(axes_index, text),
1675 PlotObjectKind::Legend | PlotObjectKind::XAxis | PlotObjectKind::YAxis => {}
1676 PlotObjectKind::SuperTitle => state.figure.set_sg_title(text),
1677 }
1678 }
1679 if let Some(style) = style {
1680 match kind {
1681 PlotObjectKind::Title => state.figure.set_axes_title_style(axes_index, style),
1682 PlotObjectKind::Subtitle => state.figure.set_axes_subtitle_style(axes_index, style),
1683 PlotObjectKind::XLabel => state.figure.set_axes_xlabel_style(axes_index, style),
1684 PlotObjectKind::YLabel => state.figure.set_axes_ylabel_style(axes_index, style),
1685 PlotObjectKind::ZLabel => state.figure.set_axes_zlabel_style(axes_index, style),
1686 PlotObjectKind::Legend | PlotObjectKind::XAxis | PlotObjectKind::YAxis => {}
1687 PlotObjectKind::SuperTitle => state.figure.set_sg_title_style(style),
1688 }
1689 }
1690 encode_plot_object_handle(handle, axes_index, kind)
1691 })?;
1692 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1693 Ok(object_handle)
1694}
1695
1696pub fn set_xlabel_for_axes(
1697 handle: FigureHandle,
1698 axes_index: usize,
1699 label: &str,
1700 style: TextStyle,
1701) -> Result<f64, FigureError> {
1702 let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1703 state.figure.set_axes_xlabel(axes_index, label.to_string());
1704 state.figure.set_axes_xlabel_style(axes_index, style);
1705 encode_plot_object_handle(handle, axes_index, PlotObjectKind::XLabel)
1706 })?;
1707 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1708 Ok(object_handle)
1709}
1710
1711pub fn set_ylabel_for_axes(
1712 handle: FigureHandle,
1713 axes_index: usize,
1714 label: &str,
1715 style: TextStyle,
1716) -> Result<f64, FigureError> {
1717 let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1718 state.figure.set_axes_ylabel(axes_index, label.to_string());
1719 state.figure.set_axes_ylabel_style(axes_index, style);
1720 encode_plot_object_handle(handle, axes_index, PlotObjectKind::YLabel)
1721 })?;
1722 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1723 Ok(object_handle)
1724}
1725
1726pub fn set_zlabel_for_axes(
1727 handle: FigureHandle,
1728 axes_index: usize,
1729 label: &str,
1730 style: TextStyle,
1731) -> Result<f64, FigureError> {
1732 let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1733 state.figure.set_axes_zlabel(axes_index, label.to_string());
1734 state.figure.set_axes_zlabel_style(axes_index, style);
1735 encode_plot_object_handle(handle, axes_index, PlotObjectKind::ZLabel)
1736 })?;
1737 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1738 Ok(object_handle)
1739}
1740
1741pub fn add_text_annotation_for_axes(
1742 handle: FigureHandle,
1743 axes_index: usize,
1744 position: glam::Vec3,
1745 text: &str,
1746 style: TextStyle,
1747) -> Result<f64, FigureError> {
1748 let (annotation_index, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1749 state
1750 .figure
1751 .add_axes_text_annotation(axes_index, position, text.to_string(), style)
1752 })?;
1753 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1754 Ok(register_text_annotation_handle(
1755 handle,
1756 axes_index,
1757 annotation_index,
1758 ))
1759}
1760
1761pub fn set_text_annotation_properties_for_axes(
1762 handle: FigureHandle,
1763 axes_index: usize,
1764 annotation_index: usize,
1765 text: Option<String>,
1766 position: Option<glam::Vec3>,
1767 style: Option<TextStyle>,
1768) -> Result<f64, FigureError> {
1769 let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1770 if let Some(text) = text {
1771 state
1772 .figure
1773 .set_axes_text_annotation_text(axes_index, annotation_index, text);
1774 }
1775 if let Some(position) = position {
1776 state
1777 .figure
1778 .set_axes_text_annotation_position(axes_index, annotation_index, position);
1779 }
1780 if let Some(style) = style {
1781 state
1782 .figure
1783 .set_axes_text_annotation_style(axes_index, annotation_index, style);
1784 }
1785 register_text_annotation_handle(handle, axes_index, annotation_index)
1786 })?;
1787 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1788 Ok(object_handle)
1789}
1790
1791pub fn toggle_box() -> bool {
1792 let (handle, figure_clone, enabled) = {
1793 let mut reg = registry();
1794 let handle = reg.current;
1795 let state = get_state_mut(&mut reg, handle);
1796 let axes = state.active_axes;
1797 let next = !state
1798 .figure
1799 .axes_metadata(axes)
1800 .map(|m| m.box_enabled)
1801 .unwrap_or(true);
1802 state.figure.set_axes_box_enabled(axes, next);
1803 state.revision = state.revision.wrapping_add(1);
1804 (handle, state.figure.clone(), next)
1805 };
1806 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1807 enabled
1808}
1809
1810pub fn set_axis_equal(enabled: bool) {
1811 let (handle, figure_clone) = {
1812 let mut reg = registry();
1813 let handle = reg.current;
1814 let state = get_state_mut(&mut reg, handle);
1815 let axes = state.active_axes;
1816 state.figure.set_axes_axis_equal(axes, enabled);
1817 state.revision = state.revision.wrapping_add(1);
1818 (handle, state.figure.clone())
1819 };
1820 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1821}
1822
1823pub fn set_axis_equal_and_limits(enabled: bool, x: Option<(f64, f64)>, y: Option<(f64, f64)>) {
1824 let updates = {
1825 let mut reg = registry();
1826 let handle = reg.current;
1827 let state = get_state_mut(&mut reg, handle);
1828 let axes = state.active_axes;
1829 state.figure.set_axes_axis_equal(axes, enabled);
1830 set_axes_limits_with_links(&mut reg, handle, axes, x, y)
1831 .expect("active axes target should be valid")
1832 };
1833 notify_figure_updates(updates);
1834}
1835
1836pub fn set_axis_equal_for_axes(
1837 handle: FigureHandle,
1838 axes_index: usize,
1839 enabled: bool,
1840) -> Result<(), FigureError> {
1841 let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1842 state.figure.set_axes_axis_equal(axes_index, enabled);
1843 })?;
1844 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1845 Ok(())
1846}
1847
1848pub fn data_aspect_ratio_snapshot() -> ([f64; 3], String) {
1849 let mut reg = registry();
1850 let handle = reg.current;
1851 let state = get_state_mut(&mut reg, handle);
1852 let axes = state.active_axes;
1853 state
1854 .figure
1855 .axes_metadata(axes)
1856 .map(|meta| (meta.data_aspect_ratio, meta.data_aspect_ratio_mode.clone()))
1857 .unwrap_or(([1.0, 1.0, 1.0], "auto".into()))
1858}
1859
1860pub fn data_aspect_ratio_snapshot_for_axes(
1861 handle: FigureHandle,
1862 axes_index: usize,
1863) -> Result<([f64; 3], String), FigureError> {
1864 let mut reg = registry();
1865 let state = get_state_mut(&mut reg, handle);
1866 validate_axes_index(state, axes_index)?;
1867 Ok(state
1868 .figure
1869 .axes_metadata(axes_index)
1870 .map(|meta| (meta.data_aspect_ratio, meta.data_aspect_ratio_mode.clone()))
1871 .unwrap_or(([1.0, 1.0, 1.0], "auto".into())))
1872}
1873
1874pub fn set_data_aspect_ratio(ratio: [f64; 3], mode: &str) {
1875 let (handle, figure_clone) = {
1876 let mut reg = registry();
1877 let handle = reg.current;
1878 let state = get_state_mut(&mut reg, handle);
1879 let axes = state.active_axes;
1880 state
1881 .figure
1882 .set_axes_data_aspect_ratio(axes, ratio, mode.to_string());
1883 state.revision = state.revision.wrapping_add(1);
1884 (handle, state.figure.clone())
1885 };
1886 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1887}
1888
1889pub fn set_data_aspect_ratio_for_axes(
1890 handle: FigureHandle,
1891 axes_index: usize,
1892 ratio: [f64; 3],
1893 mode: &str,
1894) -> Result<(), FigureError> {
1895 let figure_clone = {
1896 let mut reg = registry();
1897 let state = get_state_mut(&mut reg, handle);
1898 validate_axes_index(state, axes_index)?;
1899 state
1900 .figure
1901 .set_axes_data_aspect_ratio(axes_index, ratio, mode.to_string());
1902 state.revision = state.revision.wrapping_add(1);
1903 state.figure.clone()
1904 };
1905 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1906 Ok(())
1907}
1908
1909fn clone_touched_figures(
1910 registry: &mut PlotRegistry,
1911 touched: HashSet<FigureHandle>,
1912) -> Vec<(FigureHandle, Figure)> {
1913 touched
1914 .into_iter()
1915 .filter_map(|handle| {
1916 let state = registry.figures.get_mut(&handle)?;
1917 state.revision = state.revision.wrapping_add(1);
1918 Some((handle, state.figure.clone()))
1919 })
1920 .collect()
1921}
1922
1923fn set_axes_limits_with_links(
1924 registry: &mut PlotRegistry,
1925 source_handle: FigureHandle,
1926 source_axes: usize,
1927 x: Option<(f64, f64)>,
1928 y: Option<(f64, f64)>,
1929) -> Result<Vec<(FigureHandle, Figure)>, FigureError> {
1930 let source_state = registry
1931 .figures
1932 .get(&source_handle)
1933 .ok_or_else(|| FigureError::InvalidHandle(source_handle.as_u32()))?;
1934 if source_axes >= axes_count(source_state) {
1935 return Err(FigureError::InvalidSubplotIndex {
1936 rows: source_state.figure.axes_rows.max(1),
1937 cols: source_state.figure.axes_cols.max(1),
1938 index: source_axes,
1939 });
1940 }
1941
1942 let x_group = registry
1943 .link_axes_groups
1944 .iter()
1945 .find(|group| {
1946 group.axis == LinkAxesAxis::X && group.targets.contains(&(source_handle, source_axes))
1947 })
1948 .cloned();
1949 let y_group = registry
1950 .link_axes_groups
1951 .iter()
1952 .find(|group| {
1953 group.axis == LinkAxesAxis::Y && group.targets.contains(&(source_handle, source_axes))
1954 })
1955 .cloned();
1956 if x_group.is_none() && y_group.is_none() {
1957 let state = registry
1958 .figures
1959 .get_mut(&source_handle)
1960 .expect("validated source axes target should exist");
1961 state.figure.set_axes_limits(source_axes, x, y);
1962 state.revision = state.revision.wrapping_add(1);
1963 return Ok(vec![(source_handle, state.figure.clone())]);
1964 };
1965
1966 let mut updates = HashMap::new();
1967 updates.insert((source_handle, source_axes), (x, y));
1968 if let Some(group) = x_group {
1969 for &(handle, axes_index) in &group.targets {
1970 let Some(state) = registry.figures.get(&handle) else {
1971 continue;
1972 };
1973 if axes_index >= axes_count(state) {
1974 continue;
1975 }
1976 let Some(meta) = state.figure.axes_metadata(axes_index) else {
1977 continue;
1978 };
1979 updates
1980 .entry((handle, axes_index))
1981 .or_insert((meta.x_limits, meta.y_limits))
1982 .0 = x;
1983 }
1984 }
1985 if let Some(group) = y_group {
1986 for &(handle, axes_index) in &group.targets {
1987 let Some(state) = registry.figures.get(&handle) else {
1988 continue;
1989 };
1990 if axes_index >= axes_count(state) {
1991 continue;
1992 }
1993 let Some(meta) = state.figure.axes_metadata(axes_index) else {
1994 continue;
1995 };
1996 updates
1997 .entry((handle, axes_index))
1998 .or_insert((meta.x_limits, meta.y_limits))
1999 .1 = y;
2000 }
2001 }
2002
2003 let mut touched = HashSet::new();
2004 for ((handle, axes_index), (x_limits, y_limits)) in updates {
2005 let Some(state) = registry.figures.get_mut(&handle) else {
2006 continue;
2007 };
2008 if axes_index >= axes_count(state) {
2009 continue;
2010 }
2011 state.figure.set_axes_limits(axes_index, x_limits, y_limits);
2012 touched.insert(handle);
2013 }
2014 Ok(clone_touched_figures(registry, touched))
2015}
2016
2017fn notify_figure_updates(updates: Vec<(FigureHandle, Figure)>) {
2018 for (handle, figure) in updates {
2019 notify_with_figure(handle, &figure, FigureEventKind::Updated);
2020 }
2021}
2022
2023pub fn set_axis_limits(x: Option<(f64, f64)>, y: Option<(f64, f64)>) {
2024 let updates = {
2025 let mut reg = registry();
2026 let handle = reg.current;
2027 let state = get_state_mut(&mut reg, handle);
2028 let axes = state.active_axes;
2029 set_axes_limits_with_links(&mut reg, handle, axes, x, y)
2030 .expect("active axes target should be valid")
2031 };
2032 notify_figure_updates(updates);
2033}
2034
2035pub fn set_axis_limits_for_axes(
2036 handle: FigureHandle,
2037 axes_index: usize,
2038 x: Option<(f64, f64)>,
2039 y: Option<(f64, f64)>,
2040) -> Result<(), FigureError> {
2041 let updates = {
2042 let mut reg = registry();
2043 let state = get_state_mut(&mut reg, handle);
2044 let total_axes = axes_count(state);
2045 if axes_index >= total_axes {
2046 return Err(FigureError::InvalidSubplotIndex {
2047 rows: state.figure.axes_rows.max(1),
2048 cols: state.figure.axes_cols.max(1),
2049 index: axes_index,
2050 });
2051 }
2052 state.active_axes = axes_index;
2053 state.figure.set_active_axes_index(axes_index);
2054 set_axes_limits_with_links(&mut reg, handle, axes_index, x, y)?
2055 };
2056 notify_figure_updates(updates);
2057 Ok(())
2058}
2059
2060fn axis_limits_for_link_axis(
2061 state: &mut FigureState,
2062 axes_index: usize,
2063 axis: LinkAxesAxis,
2064) -> Option<(f64, f64)> {
2065 if let Some(meta) = state.figure.axes_metadata(axes_index) {
2066 let explicit = match axis {
2067 LinkAxesAxis::X => meta.x_limits,
2068 LinkAxesAxis::Y => meta.y_limits,
2069 };
2070 if let Some((lo, hi)) = explicit {
2071 return Some((lo.min(hi), lo.max(hi)));
2072 }
2073 }
2074 let display = display_bounds_for_state_axes(state, axes_index)?;
2075 let limits = match axis {
2076 LinkAxesAxis::X => (display.0, display.1),
2077 LinkAxesAxis::Y => (display.2, display.3),
2078 };
2079 if limits.0.is_finite() && limits.1.is_finite() {
2080 Some((limits.0.min(limits.1), limits.0.max(limits.1)))
2081 } else {
2082 None
2083 }
2084}
2085
2086fn union_link_axis_limits(
2087 registry: &mut PlotRegistry,
2088 targets: &[(FigureHandle, usize)],
2089 axis: LinkAxesAxis,
2090) -> Option<(f64, f64)> {
2091 let mut union = None;
2092 for &(handle, axes_index) in targets {
2093 let Some(state) = registry.figures.get_mut(&handle) else {
2094 continue;
2095 };
2096 if axes_index >= axes_count(state) {
2097 continue;
2098 }
2099 let Some((lo, hi)) = axis_limits_for_link_axis(state, axes_index, axis) else {
2100 continue;
2101 };
2102 union = Some(match union {
2103 Some((current_lo, current_hi)) => (f64::min(current_lo, lo), f64::max(current_hi, hi)),
2104 None => (lo, hi),
2105 });
2106 }
2107 union
2108}
2109
2110fn remove_link_axes_targets(
2111 registry: &mut PlotRegistry,
2112 target_set: &HashSet<(FigureHandle, usize)>,
2113 axis: Option<LinkAxesAxis>,
2114) {
2115 for group in &mut registry.link_axes_groups {
2116 if axis.is_none_or(|axis| group.axis == axis) {
2117 group.targets.retain(|target| !target_set.contains(target));
2118 }
2119 }
2120 registry
2121 .link_axes_groups
2122 .retain(|group| group.targets.len() >= 2);
2123}
2124
2125fn purge_link_axes_for_figure(registry: &mut PlotRegistry, handle: FigureHandle) {
2126 for group in &mut registry.link_axes_groups {
2127 group
2128 .targets
2129 .retain(|(target_handle, _)| *target_handle != handle);
2130 }
2131 registry
2132 .link_axes_groups
2133 .retain(|group| group.targets.len() >= 2);
2134}
2135
2136pub fn link_axes(
2137 targets: Vec<(FigureHandle, usize)>,
2138 mode: Option<LinkAxesMode>,
2139) -> Result<(), FigureError> {
2140 let updates = {
2141 let mut reg = registry();
2142 let mut seen = HashSet::new();
2143 let targets = targets
2144 .into_iter()
2145 .filter(|target| seen.insert(*target))
2146 .collect::<Vec<_>>();
2147
2148 for &(handle, axes_index) in &targets {
2149 let state = reg
2150 .figures
2151 .get(&handle)
2152 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2153 if axes_index >= axes_count(state) {
2154 return Err(FigureError::InvalidSubplotIndex {
2155 rows: state.figure.axes_rows.max(1),
2156 cols: state.figure.axes_cols.max(1),
2157 index: axes_index,
2158 });
2159 }
2160 }
2161
2162 let target_set = targets.iter().copied().collect::<HashSet<_>>();
2163 let Some(mode) = mode else {
2164 remove_link_axes_targets(&mut reg, &target_set, None);
2165 return Ok(());
2166 };
2167 if mode.links_x() {
2168 remove_link_axes_targets(&mut reg, &target_set, Some(LinkAxesAxis::X));
2169 }
2170 if mode.links_y() {
2171 remove_link_axes_targets(&mut reg, &target_set, Some(LinkAxesAxis::Y));
2172 }
2173 if targets.len() < 2 {
2174 return Ok(());
2175 }
2176
2177 let (source_handle, source_axes) = targets[0];
2178 let x_sync = if mode.links_x() {
2179 union_link_axis_limits(&mut reg, &targets, LinkAxesAxis::X)
2180 } else {
2181 None
2182 };
2183 let y_sync = if mode.links_y() {
2184 union_link_axis_limits(&mut reg, &targets, LinkAxesAxis::Y)
2185 } else {
2186 None
2187 };
2188 let source_meta = reg
2189 .figures
2190 .get(&source_handle)
2191 .and_then(|state| state.figure.axes_metadata(source_axes))
2192 .cloned()
2193 .ok_or(FigureError::InvalidAxesHandle)?;
2194 if mode.links_x() {
2195 reg.link_axes_groups.push(LinkAxesGroup {
2196 axis: LinkAxesAxis::X,
2197 targets: targets.clone(),
2198 });
2199 }
2200 if mode.links_y() {
2201 reg.link_axes_groups.push(LinkAxesGroup {
2202 axis: LinkAxesAxis::Y,
2203 targets,
2204 });
2205 }
2206 set_axes_limits_with_links(
2207 &mut reg,
2208 source_handle,
2209 source_axes,
2210 if mode.links_x() {
2211 x_sync
2212 } else {
2213 source_meta.x_limits
2214 },
2215 if mode.links_y() {
2216 y_sync
2217 } else {
2218 source_meta.y_limits
2219 },
2220 )?
2221 };
2222 notify_figure_updates(updates);
2223 Ok(())
2224}
2225
2226pub fn set_axis_ticks(x: Option<Vec<f64>>, y: Option<Vec<f64>>) {
2227 let (handle, figure_clone) = {
2228 let mut reg = registry();
2229 let handle = reg.current;
2230 let state = get_state_mut(&mut reg, handle);
2231 let axes = state.active_axes;
2232 state.figure.set_axes_ticks(axes, x, y);
2233 state.revision = state.revision.wrapping_add(1);
2234 (handle, state.figure.clone())
2235 };
2236 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2237}
2238
2239pub fn set_axis_ticks_for_axes(
2240 handle: FigureHandle,
2241 axes_index: usize,
2242 x: Option<Vec<f64>>,
2243 y: Option<Vec<f64>>,
2244) -> Result<(), FigureError> {
2245 let figure_clone = {
2246 let mut reg = registry();
2247 let state = reg
2248 .figures
2249 .get_mut(&handle)
2250 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2251 let total_axes = axes_count(state);
2252 if axes_index >= total_axes {
2253 return Err(FigureError::InvalidSubplotIndex {
2254 rows: state.figure.axes_rows.max(1),
2255 cols: state.figure.axes_cols.max(1),
2256 index: axes_index,
2257 });
2258 }
2259 state.figure.set_axes_ticks(axes_index, x, y);
2260 state.revision = state.revision.wrapping_add(1);
2261 state.figure.clone()
2262 };
2263 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2264 Ok(())
2265}
2266
2267pub fn set_axis_tick_labels(x: Option<Vec<String>>, y: Option<Vec<String>>) {
2268 let (handle, figure_clone) = {
2269 let mut reg = registry();
2270 let handle = reg.current;
2271 let state = get_state_mut(&mut reg, handle);
2272 let axes = state.active_axes;
2273 state.figure.set_axes_tick_labels(axes, x, y);
2274 state.revision = state.revision.wrapping_add(1);
2275 (handle, state.figure.clone())
2276 };
2277 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2278}
2279
2280pub fn set_axis_tick_labels_for_axes(
2281 handle: FigureHandle,
2282 axes_index: usize,
2283 x: Option<Vec<String>>,
2284 y: Option<Vec<String>>,
2285) -> Result<(), FigureError> {
2286 let figure_clone = {
2287 let mut reg = registry();
2288 let state = reg
2289 .figures
2290 .get_mut(&handle)
2291 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2292 let total_axes = axes_count(state);
2293 if axes_index >= total_axes {
2294 return Err(FigureError::InvalidSubplotIndex {
2295 rows: state.figure.axes_rows.max(1),
2296 cols: state.figure.axes_cols.max(1),
2297 index: axes_index,
2298 });
2299 }
2300 state.figure.set_axes_tick_labels(axes_index, x, y);
2301 state.revision = state.revision.wrapping_add(1);
2302 state.figure.clone()
2303 };
2304 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2305 Ok(())
2306}
2307
2308pub fn set_axis_tick_formats(x: Option<String>, y: Option<String>) {
2309 let (handle, figure_clone) = {
2310 let mut reg = registry();
2311 let handle = reg.current;
2312 let state = get_state_mut(&mut reg, handle);
2313 let axes = state.active_axes;
2314 state.figure.set_axes_tick_formats(axes, x, y);
2315 state.revision = state.revision.wrapping_add(1);
2316 (handle, state.figure.clone())
2317 };
2318 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2319}
2320
2321pub fn set_axis_tick_formats_for_axes(
2322 handle: FigureHandle,
2323 axes_index: usize,
2324 x: Option<String>,
2325 y: Option<String>,
2326) -> Result<(), FigureError> {
2327 let figure_clone = {
2328 let mut reg = registry();
2329 let state = reg
2330 .figures
2331 .get_mut(&handle)
2332 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2333 let total_axes = axes_count(state);
2334 if axes_index >= total_axes {
2335 return Err(FigureError::InvalidSubplotIndex {
2336 rows: state.figure.axes_rows.max(1),
2337 cols: state.figure.axes_cols.max(1),
2338 index: axes_index,
2339 });
2340 }
2341 state.figure.set_axes_tick_formats(axes_index, x, y);
2342 state.revision = state.revision.wrapping_add(1);
2343 state.figure.clone()
2344 };
2345 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2346 Ok(())
2347}
2348
2349pub fn set_axis_tick_angles(x: Option<f64>, y: Option<f64>) {
2350 let (handle, figure_clone) = {
2351 let mut reg = registry();
2352 let handle = reg.current;
2353 let state = get_state_mut(&mut reg, handle);
2354 let axes = state.active_axes;
2355 state.figure.set_axes_tick_label_rotations(axes, x, y);
2356 state.revision = state.revision.wrapping_add(1);
2357 (handle, state.figure.clone())
2358 };
2359 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2360}
2361
2362pub fn set_axis_tick_angles_for_axes(
2363 handle: FigureHandle,
2364 axes_index: usize,
2365 x: Option<f64>,
2366 y: Option<f64>,
2367) -> Result<(), FigureError> {
2368 let figure_clone = {
2369 let mut reg = registry();
2370 let state = reg
2371 .figures
2372 .get_mut(&handle)
2373 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2374 let total_axes = axes_count(state);
2375 if axes_index >= total_axes {
2376 return Err(FigureError::InvalidSubplotIndex {
2377 rows: state.figure.axes_rows.max(1),
2378 cols: state.figure.axes_cols.max(1),
2379 index: axes_index,
2380 });
2381 }
2382 state.figure.set_axes_tick_label_rotations(axes_index, x, y);
2383 state.revision = state.revision.wrapping_add(1);
2384 state.figure.clone()
2385 };
2386 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2387 Ok(())
2388}
2389
2390pub fn axis_limits_snapshot() -> AxisLimitSnapshot {
2391 let mut reg = registry();
2392 let handle = reg.current;
2393 let state = get_state_mut(&mut reg, handle);
2394 let axes = state.active_axes;
2395 let meta = state
2396 .figure
2397 .axes_metadata(axes)
2398 .cloned()
2399 .unwrap_or_default();
2400 (meta.x_limits, meta.y_limits)
2401}
2402
2403pub fn axis_ticks_snapshot() -> AxisTickSnapshot {
2404 let mut reg = registry();
2405 let handle = reg.current;
2406 let state = get_state_mut(&mut reg, handle);
2407 let axes = state.active_axes;
2408 let meta = state
2409 .figure
2410 .axes_metadata(axes)
2411 .cloned()
2412 .unwrap_or_default();
2413 (meta.x_ticks, meta.y_ticks)
2414}
2415
2416pub fn axis_tick_labels_snapshot() -> AxisTickLabelSnapshot {
2417 let mut reg = registry();
2418 let handle = reg.current;
2419 let state = get_state_mut(&mut reg, handle);
2420 let axes = state.active_axes;
2421 let meta = state
2422 .figure
2423 .axes_metadata(axes)
2424 .cloned()
2425 .unwrap_or_default();
2426 (meta.x_tick_labels, meta.y_tick_labels)
2427}
2428
2429pub fn axis_tick_formats_snapshot() -> AxisTickFormatSnapshot {
2430 let mut reg = registry();
2431 let handle = reg.current;
2432 let state = get_state_mut(&mut reg, handle);
2433 let axes = state.active_axes;
2434 let meta = state
2435 .figure
2436 .axes_metadata(axes)
2437 .cloned()
2438 .unwrap_or_default();
2439 (meta.x_tick_format, meta.y_tick_format)
2440}
2441
2442pub fn axis_tick_angles_snapshot() -> AxisTickAngleSnapshot {
2443 let mut reg = registry();
2444 let handle = reg.current;
2445 let state = get_state_mut(&mut reg, handle);
2446 let axes = state.active_axes;
2447 let meta = state
2448 .figure
2449 .axes_metadata(axes)
2450 .cloned()
2451 .unwrap_or_default();
2452 (meta.x_tick_label_rotation, meta.y_tick_label_rotation)
2453}
2454
2455pub fn axis_ticks_snapshot_for_axes(
2456 handle: FigureHandle,
2457 axes_index: usize,
2458) -> Result<AxisTickSnapshot, FigureError> {
2459 let reg = registry();
2460 let state = reg
2461 .figures
2462 .get(&handle)
2463 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2464 let total_axes = axes_count(state);
2465 if axes_index >= total_axes {
2466 return Err(FigureError::InvalidSubplotIndex {
2467 rows: state.figure.axes_rows.max(1),
2468 cols: state.figure.axes_cols.max(1),
2469 index: axes_index,
2470 });
2471 }
2472 let meta = state
2473 .figure
2474 .axes_metadata(axes_index)
2475 .cloned()
2476 .unwrap_or_default();
2477 Ok((meta.x_ticks, meta.y_ticks))
2478}
2479
2480pub fn axis_tick_labels_snapshot_for_axes(
2481 handle: FigureHandle,
2482 axes_index: usize,
2483) -> Result<AxisTickLabelSnapshot, FigureError> {
2484 let reg = registry();
2485 let state = reg
2486 .figures
2487 .get(&handle)
2488 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2489 let total_axes = axes_count(state);
2490 if axes_index >= total_axes {
2491 return Err(FigureError::InvalidSubplotIndex {
2492 rows: state.figure.axes_rows.max(1),
2493 cols: state.figure.axes_cols.max(1),
2494 index: axes_index,
2495 });
2496 }
2497 let meta = state
2498 .figure
2499 .axes_metadata(axes_index)
2500 .cloned()
2501 .unwrap_or_default();
2502 Ok((meta.x_tick_labels, meta.y_tick_labels))
2503}
2504
2505pub fn axis_tick_formats_snapshot_for_axes(
2506 handle: FigureHandle,
2507 axes_index: usize,
2508) -> Result<AxisTickFormatSnapshot, FigureError> {
2509 let reg = registry();
2510 let state = reg
2511 .figures
2512 .get(&handle)
2513 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2514 let total_axes = axes_count(state);
2515 if axes_index >= total_axes {
2516 return Err(FigureError::InvalidSubplotIndex {
2517 rows: state.figure.axes_rows.max(1),
2518 cols: state.figure.axes_cols.max(1),
2519 index: axes_index,
2520 });
2521 }
2522 let meta = state
2523 .figure
2524 .axes_metadata(axes_index)
2525 .cloned()
2526 .unwrap_or_default();
2527 Ok((meta.x_tick_format, meta.y_tick_format))
2528}
2529
2530pub fn axis_tick_angles_snapshot_for_axes(
2531 handle: FigureHandle,
2532 axes_index: usize,
2533) -> Result<AxisTickAngleSnapshot, FigureError> {
2534 let reg = registry();
2535 let state = reg
2536 .figures
2537 .get(&handle)
2538 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2539 let total_axes = axes_count(state);
2540 if axes_index >= total_axes {
2541 return Err(FigureError::InvalidSubplotIndex {
2542 rows: state.figure.axes_rows.max(1),
2543 cols: state.figure.axes_cols.max(1),
2544 index: axes_index,
2545 });
2546 }
2547 let meta = state
2548 .figure
2549 .axes_metadata(axes_index)
2550 .cloned()
2551 .unwrap_or_default();
2552 Ok((meta.x_tick_label_rotation, meta.y_tick_label_rotation))
2553}
2554
2555pub fn axis_display_bounds_snapshot() -> AxisDisplayBoundsSnapshot {
2556 let mut reg = registry();
2557 let handle = reg.current;
2558 let state = get_state_mut(&mut reg, handle);
2559 let axes_index = state.active_axes;
2560 display_bounds_for_state_axes(state, axes_index)
2561}
2562
2563pub fn axis_display_bounds_snapshot_for_axes(
2564 handle: FigureHandle,
2565 axes_index: usize,
2566) -> Result<AxisDisplayBoundsSnapshot, FigureError> {
2567 let mut reg = registry();
2568 let state = reg
2569 .figures
2570 .get_mut(&handle)
2571 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2572 let total_axes = axes_count(state);
2573 if axes_index >= total_axes {
2574 return Err(FigureError::InvalidSubplotIndex {
2575 rows: state.figure.axes_rows.max(1),
2576 cols: state.figure.axes_cols.max(1),
2577 index: axes_index,
2578 });
2579 }
2580 Ok(display_bounds_for_state_axes(state, axes_index))
2581}
2582
2583fn display_bounds_for_state_axes(
2584 state: &mut FigureState,
2585 axes_index: usize,
2586) -> AxisDisplayBoundsSnapshot {
2587 let bounds = state.figure.data_bounds_for_axes(axes_index);
2588 if !(bounds.min.x.is_finite()
2589 && bounds.max.x.is_finite()
2590 && bounds.min.y.is_finite()
2591 && bounds.max.y.is_finite())
2592 {
2593 return None;
2594 }
2595
2596 let mut x_min = bounds.min.x as f64;
2597 let mut x_max = bounds.max.x as f64;
2598 let mut y_min = bounds.min.y as f64;
2599 let mut y_max = bounds.max.y as f64;
2600
2601 if let Some(meta) = state.figure.axes_metadata(axes_index) {
2602 if let Some((lo, hi)) = meta.x_limits {
2603 x_min = lo;
2604 x_max = hi;
2605 }
2606 if let Some((lo, hi)) = meta.y_limits {
2607 y_min = lo;
2608 y_max = hi;
2609 }
2610 if meta.axis_equal || meta.data_aspect_ratio_mode == "manual" {
2611 (x_min, x_max, y_min, y_max) =
2612 data_aspect_adjusted_bounds(x_min, x_max, y_min, y_max, meta.data_aspect_ratio);
2613 }
2614 }
2615
2616 Some((x_min, x_max, y_min, y_max))
2617}
2618
2619fn data_aspect_adjusted_bounds(
2620 x_min: f64,
2621 x_max: f64,
2622 y_min: f64,
2623 y_max: f64,
2624 ratio: [f64; 3],
2625) -> (f64, f64, f64, f64) {
2626 let x_ratio = ratio[0].abs().max(1.0e-12);
2627 let y_ratio = ratio[1].abs().max(1.0e-12);
2628 let cx = (x_min + x_max) * 0.5;
2629 let cy = (y_min + y_max) * 0.5;
2630 let x_span = (x_max - x_min).abs().max(0.1);
2631 let y_span = (y_max - y_min).abs().max(0.1);
2632 let units = (x_span / x_ratio).max(y_span / y_ratio).max(0.1);
2633 let next_x = units * x_ratio;
2634 let next_y = units * y_ratio;
2635 (
2636 cx - next_x * 0.5,
2637 cx + next_x * 0.5,
2638 cy - next_y * 0.5,
2639 cy + next_y * 0.5,
2640 )
2641}
2642
2643fn axes_count(state: &FigureState) -> usize {
2644 state.figure.axes_count()
2645}
2646
2647fn validate_axes_index(state: &FigureState, axes_index: usize) -> Result<(), FigureError> {
2648 let total_axes = axes_count(state);
2649 if axes_index >= total_axes {
2650 return Err(FigureError::InvalidSubplotIndex {
2651 rows: state.figure.axes_rows.max(1),
2652 cols: state.figure.axes_cols.max(1),
2653 index: axes_index,
2654 });
2655 }
2656 Ok(())
2657}
2658
2659fn zoom_mode_for_target(state: &FigureState, axes_index: Option<usize>) -> ZoomModeState {
2660 axes_index
2661 .and_then(|index| state.zoom_axes_modes.get(&index).copied())
2662 .unwrap_or(state.zoom_mode)
2663}
2664
2665fn apply_zoom_mode_command(
2666 mode: &mut ZoomModeState,
2667 last_enabled_motion: &mut ZoomMotion,
2668 command: ZoomModeCommand,
2669) {
2670 match command {
2671 ZoomModeCommand::On => {
2672 mode.enabled = true;
2673 mode.motion = ZoomMotion::Both;
2674 *last_enabled_motion = ZoomMotion::Both;
2675 }
2676 ZoomModeCommand::Off => {
2677 mode.enabled = false;
2678 }
2679 ZoomModeCommand::Toggle => {
2680 mode.enabled = !mode.enabled;
2681 if mode.enabled {
2682 mode.motion = *last_enabled_motion;
2683 }
2684 }
2685 ZoomModeCommand::XOn => {
2686 mode.enabled = true;
2687 mode.motion = ZoomMotion::Horizontal;
2688 *last_enabled_motion = ZoomMotion::Horizontal;
2689 }
2690 ZoomModeCommand::YOn => {
2691 mode.enabled = true;
2692 mode.motion = ZoomMotion::Vertical;
2693 *last_enabled_motion = ZoomMotion::Vertical;
2694 }
2695 }
2696}
2697
2698fn axis_limit_basis_for_zoom(state: &mut FigureState, axes_index: usize) -> AxisLimitSnapshot {
2699 let meta = state
2700 .figure
2701 .axes_metadata(axes_index)
2702 .cloned()
2703 .unwrap_or_default();
2704 let bounds = display_bounds_for_state_axes(state, axes_index);
2705 let x = meta
2706 .x_limits
2707 .or_else(|| bounds.map(|(x_min, x_max, _, _)| (x_min, x_max)))
2708 .or(Some((0.0, 1.0)));
2709 let y = meta
2710 .y_limits
2711 .or_else(|| bounds.map(|(_, _, y_min, y_max)| (y_min, y_max)))
2712 .or(Some((0.0, 1.0)));
2713 (x, y)
2714}
2715
2716fn zoom_interval(limits: (f64, f64), factor: f64) -> (f64, f64) {
2717 let (lo, hi) = limits;
2718 let center = (lo + hi) * 0.5;
2719 let span = (hi - lo).abs().max(f64::EPSILON) / factor;
2720 (center - span * 0.5, center + span * 0.5)
2721}
2722
2723fn zoom_factor_limits_for_state(
2724 state: &mut FigureState,
2725 axes_index: usize,
2726 factor: f64,
2727) -> AxisLimitSnapshot {
2728 let mode = zoom_mode_for_target(state, Some(axes_index));
2729 let meta = state
2730 .figure
2731 .axes_metadata(axes_index)
2732 .cloned()
2733 .unwrap_or_default();
2734 let (basis_x, basis_y) = axis_limit_basis_for_zoom(state, axes_index);
2735 let x_limits = match (mode.motion, basis_x) {
2736 (ZoomMotion::Vertical, _) => meta.x_limits,
2737 (_, Some(limits)) => Some(zoom_interval(limits, factor)),
2738 (_, None) => meta.x_limits,
2739 };
2740 let y_limits = match (mode.motion, basis_y) {
2741 (ZoomMotion::Horizontal, _) => meta.y_limits,
2742 (_, Some(limits)) => Some(zoom_interval(limits, factor)),
2743 (_, None) => meta.y_limits,
2744 };
2745 (x_limits, y_limits)
2746}
2747
2748fn zoom_baseline_limits_for_state(state: &FigureState, axes_index: usize) -> AxisLimitSnapshot {
2749 state
2750 .zoom_baselines
2751 .get(&axes_index)
2752 .copied()
2753 .unwrap_or((None, None))
2754}
2755
2756pub fn zoom_state_snapshot(
2757 handle: FigureHandle,
2758 axes_index: Option<usize>,
2759) -> Result<ZoomStateSnapshot, FigureError> {
2760 let reg = registry();
2761 let state = reg
2762 .figures
2763 .get(&handle)
2764 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2765 if let Some(index) = axes_index {
2766 validate_axes_index(state, index)?;
2767 }
2768 Ok(ZoomStateSnapshot {
2769 figure: handle,
2770 axes_index,
2771 mode: zoom_mode_for_target(state, axes_index),
2772 })
2773}
2774
2775pub fn set_zoom_mode_for_figure(
2776 handle: FigureHandle,
2777 command: ZoomModeCommand,
2778) -> Result<(), FigureError> {
2779 let figure_clone = {
2780 let mut reg = registry();
2781 let state = reg
2782 .figures
2783 .get_mut(&handle)
2784 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2785 apply_zoom_mode_command(
2786 &mut state.zoom_mode,
2787 &mut state.last_enabled_zoom_motion,
2788 command,
2789 );
2790 state.revision = state.revision.wrapping_add(1);
2791 state.figure.clone()
2792 };
2793 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2794 Ok(())
2795}
2796
2797pub fn set_zoom_mode_for_axes(
2798 handle: FigureHandle,
2799 axes_index: usize,
2800 command: ZoomModeCommand,
2801) -> Result<(), FigureError> {
2802 let figure_clone = {
2803 let mut reg = registry();
2804 let state = reg
2805 .figures
2806 .get_mut(&handle)
2807 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2808 validate_axes_index(state, axes_index)?;
2809 let mut mode = zoom_mode_for_target(state, Some(axes_index));
2810 apply_zoom_mode_command(&mut mode, &mut state.last_enabled_zoom_motion, command);
2811 state.zoom_axes_modes.insert(axes_index, mode);
2812 state.revision = state.revision.wrapping_add(1);
2813 state.figure.clone()
2814 };
2815 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2816 Ok(())
2817}
2818
2819pub fn set_zoom_motion_for_figure(
2820 handle: FigureHandle,
2821 motion: ZoomMotion,
2822) -> Result<(), FigureError> {
2823 let figure_clone = {
2824 let mut reg = registry();
2825 let state = reg
2826 .figures
2827 .get_mut(&handle)
2828 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2829 state.zoom_mode.motion = motion;
2830 if state.zoom_mode.enabled {
2831 state.last_enabled_zoom_motion = motion;
2832 }
2833 state.revision = state.revision.wrapping_add(1);
2834 state.figure.clone()
2835 };
2836 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2837 Ok(())
2838}
2839
2840pub fn set_zoom_enabled_for_figure(handle: FigureHandle, enabled: bool) -> Result<(), FigureError> {
2841 let figure_clone = {
2842 let mut reg = registry();
2843 let state = reg
2844 .figures
2845 .get_mut(&handle)
2846 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2847 state.zoom_mode.enabled = enabled;
2848 state.revision = state.revision.wrapping_add(1);
2849 state.figure.clone()
2850 };
2851 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2852 Ok(())
2853}
2854
2855pub fn set_zoom_direction_for_figure(
2856 handle: FigureHandle,
2857 direction: ZoomDirection,
2858) -> Result<(), FigureError> {
2859 let figure_clone = {
2860 let mut reg = registry();
2861 let state = reg
2862 .figures
2863 .get_mut(&handle)
2864 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2865 state.zoom_mode.direction = direction;
2866 state.revision = state.revision.wrapping_add(1);
2867 state.figure.clone()
2868 };
2869 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2870 Ok(())
2871}
2872
2873pub fn set_zoom_right_click_action_for_figure(
2874 handle: FigureHandle,
2875 action: ZoomRightClickAction,
2876) -> Result<(), FigureError> {
2877 let figure_clone = {
2878 let mut reg = registry();
2879 let state = reg
2880 .figures
2881 .get_mut(&handle)
2882 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2883 state.zoom_mode.right_click_action = action;
2884 state.revision = state.revision.wrapping_add(1);
2885 state.figure.clone()
2886 };
2887 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2888 Ok(())
2889}
2890
2891pub fn set_zoom_legacy_for_figure(handle: FigureHandle, enabled: bool) -> Result<(), FigureError> {
2892 let figure_clone = {
2893 let mut reg = registry();
2894 let state = reg
2895 .figures
2896 .get_mut(&handle)
2897 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2898 state.zoom_mode.use_legacy_exploration_modes = enabled;
2899 state.revision = state.revision.wrapping_add(1);
2900 state.figure.clone()
2901 };
2902 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2903 Ok(())
2904}
2905
2906pub fn reset_zoom_baseline_for_figure(handle: FigureHandle) -> Result<(), FigureError> {
2907 let figure_clone = {
2908 let mut reg = registry();
2909 let state = reg
2910 .figures
2911 .get_mut(&handle)
2912 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2913 for axes_index in 0..axes_count(state) {
2914 let snapshot = axis_limit_basis_for_zoom(state, axes_index);
2915 state.zoom_baselines.insert(axes_index, snapshot);
2916 }
2917 state.revision = state.revision.wrapping_add(1);
2918 state.figure.clone()
2919 };
2920 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2921 Ok(())
2922}
2923
2924pub fn reset_zoom_baseline_for_axes(
2925 handle: FigureHandle,
2926 axes_index: usize,
2927) -> Result<(), FigureError> {
2928 let figure_clone = {
2929 let mut reg = registry();
2930 let state = reg
2931 .figures
2932 .get_mut(&handle)
2933 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2934 validate_axes_index(state, axes_index)?;
2935 let snapshot = axis_limit_basis_for_zoom(state, axes_index);
2936 state.zoom_baselines.insert(axes_index, snapshot);
2937 state.revision = state.revision.wrapping_add(1);
2938 state.figure.clone()
2939 };
2940 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2941 Ok(())
2942}
2943
2944pub fn restore_zoom_baseline_for_figure(handle: FigureHandle) -> Result<(), FigureError> {
2945 let updates = {
2946 let mut reg = registry();
2947 let limits = {
2948 let state = reg
2949 .figures
2950 .get(&handle)
2951 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2952 (0..axes_count(state))
2953 .map(|axes_index| {
2954 (
2955 axes_index,
2956 zoom_baseline_limits_for_state(state, axes_index),
2957 )
2958 })
2959 .collect::<Vec<_>>()
2960 };
2961 let mut updates = Vec::new();
2962 for (axes_index, (x_limits, y_limits)) in limits {
2963 updates.extend(set_axes_limits_with_links(
2964 &mut reg, handle, axes_index, x_limits, y_limits,
2965 )?);
2966 }
2967 updates
2968 };
2969 notify_figure_updates(updates);
2970 Ok(())
2971}
2972
2973pub fn restore_zoom_baseline_for_axes(
2974 handle: FigureHandle,
2975 axes_index: usize,
2976) -> Result<(), FigureError> {
2977 let updates = {
2978 let mut reg = registry();
2979 let (x_limits, y_limits) = {
2980 let state = reg
2981 .figures
2982 .get(&handle)
2983 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2984 validate_axes_index(state, axes_index)?;
2985 zoom_baseline_limits_for_state(state, axes_index)
2986 };
2987 set_axes_limits_with_links(&mut reg, handle, axes_index, x_limits, y_limits)?
2988 };
2989 notify_figure_updates(updates);
2990 Ok(())
2991}
2992
2993pub fn apply_zoom_factor_for_figure(handle: FigureHandle, factor: f64) -> Result<(), FigureError> {
2994 let updates = {
2995 let mut reg = registry();
2996 let (axes_index, x_limits, y_limits) = {
2997 let state = reg
2998 .figures
2999 .get_mut(&handle)
3000 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
3001 let axes_index = state.active_axes;
3002 let (x_limits, y_limits) = zoom_factor_limits_for_state(state, axes_index, factor);
3003 (axes_index, x_limits, y_limits)
3004 };
3005 set_axes_limits_with_links(&mut reg, handle, axes_index, x_limits, y_limits)?
3006 };
3007 notify_figure_updates(updates);
3008 Ok(())
3009}
3010
3011pub fn apply_zoom_factor_for_axes(
3012 handle: FigureHandle,
3013 axes_index: usize,
3014 factor: f64,
3015) -> Result<(), FigureError> {
3016 let updates = {
3017 let mut reg = registry();
3018 let (x_limits, y_limits) = {
3019 let state = reg
3020 .figures
3021 .get_mut(&handle)
3022 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
3023 validate_axes_index(state, axes_index)?;
3024 zoom_factor_limits_for_state(state, axes_index, factor)
3025 };
3026 set_axes_limits_with_links(&mut reg, handle, axes_index, x_limits, y_limits)?
3027 };
3028 notify_figure_updates(updates);
3029 Ok(())
3030}
3031
3032fn pan_mode_for_target(state: &FigureState, axes_index: Option<usize>) -> PanModeState {
3033 axes_index
3034 .and_then(|index| state.pan_axes_modes.get(&index).copied())
3035 .unwrap_or(state.pan_mode)
3036}
3037
3038fn apply_pan_mode_command(
3039 mode: &mut PanModeState,
3040 last_enabled_motion: &mut ZoomMotion,
3041 command: PanModeCommand,
3042) {
3043 match command {
3044 PanModeCommand::On => {
3045 mode.enabled = true;
3046 mode.motion = *last_enabled_motion;
3047 }
3048 PanModeCommand::Off => mode.enabled = false,
3049 PanModeCommand::Toggle => {
3050 mode.enabled = !mode.enabled;
3051 if mode.enabled {
3052 mode.motion = *last_enabled_motion;
3053 }
3054 }
3055 PanModeCommand::XOn => {
3056 mode.enabled = true;
3057 mode.motion = ZoomMotion::Horizontal;
3058 *last_enabled_motion = ZoomMotion::Horizontal;
3059 }
3060 PanModeCommand::YOn => {
3061 mode.enabled = true;
3062 mode.motion = ZoomMotion::Vertical;
3063 *last_enabled_motion = ZoomMotion::Vertical;
3064 }
3065 }
3066}
3067
3068pub fn pan_state_snapshot(
3069 handle: FigureHandle,
3070 axes_index: Option<usize>,
3071) -> Result<PanStateSnapshot, FigureError> {
3072 let mut reg = registry();
3073 let state = get_state_mut(&mut reg, handle);
3074 if let Some(index) = axes_index {
3075 validate_axes_index(state, index)?;
3076 }
3077 Ok(PanStateSnapshot {
3078 figure: handle,
3079 axes_index,
3080 mode: pan_mode_for_target(state, axes_index),
3081 })
3082}
3083
3084pub fn set_pan_mode_for_figure(
3085 handle: FigureHandle,
3086 command: PanModeCommand,
3087) -> Result<(), FigureError> {
3088 let ((), figure_clone) = with_figure_mut(handle, |state| {
3089 apply_pan_mode_command(
3090 &mut state.pan_mode,
3091 &mut state.last_enabled_pan_motion,
3092 command,
3093 );
3094 })?;
3095 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3096 Ok(())
3097}
3098
3099pub fn set_pan_mode_for_axes(
3100 handle: FigureHandle,
3101 axes_index: usize,
3102 command: PanModeCommand,
3103) -> Result<(), FigureError> {
3104 let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
3105 let mut mode = pan_mode_for_target(state, Some(axes_index));
3106 apply_pan_mode_command(&mut mode, &mut state.last_enabled_pan_motion, command);
3107 state.pan_axes_modes.insert(axes_index, mode);
3108 })?;
3109 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3110 Ok(())
3111}
3112
3113pub fn set_pan_enabled_for_figure(handle: FigureHandle, enabled: bool) -> Result<(), FigureError> {
3114 let ((), figure_clone) = with_figure_mut(handle, |state| {
3115 state.pan_mode.enabled = enabled;
3116 })?;
3117 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3118 Ok(())
3119}
3120
3121pub fn set_pan_motion_for_figure(
3122 handle: FigureHandle,
3123 motion: ZoomMotion,
3124) -> Result<(), FigureError> {
3125 let ((), figure_clone) = with_figure_mut(handle, |state| {
3126 state.pan_mode.motion = motion;
3127 state.last_enabled_pan_motion = motion;
3128 })?;
3129 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3130 Ok(())
3131}
3132
3133pub fn data_cursor_state_snapshot(
3134 handle: FigureHandle,
3135) -> Result<DataCursorStateSnapshot, FigureError> {
3136 let mut reg = registry();
3137 let state = get_state_mut(&mut reg, handle);
3138 Ok(DataCursorStateSnapshot {
3139 figure: handle,
3140 mode: state.data_cursor_mode.clone(),
3141 })
3142}
3143
3144pub fn set_data_cursor_enabled_for_figure(
3145 handle: FigureHandle,
3146 enabled: bool,
3147) -> Result<(), FigureError> {
3148 let ((), figure_clone) = with_figure_mut(handle, |state| {
3149 state.data_cursor_mode.enabled = enabled;
3150 })?;
3151 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3152 Ok(())
3153}
3154
3155pub fn set_data_cursor_snap_to_data_vertex_for_figure(
3156 handle: FigureHandle,
3157 enabled: bool,
3158) -> Result<(), FigureError> {
3159 let ((), figure_clone) = with_figure_mut(handle, |state| {
3160 state.data_cursor_mode.snap_to_data_vertex = enabled;
3161 })?;
3162 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3163 Ok(())
3164}
3165
3166pub fn set_data_cursor_display_style_for_figure(
3167 handle: FigureHandle,
3168 style: String,
3169) -> Result<(), FigureError> {
3170 let ((), figure_clone) = with_figure_mut(handle, |state| {
3171 state.data_cursor_mode.display_style = style;
3172 })?;
3173 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3174 Ok(())
3175}
3176
3177pub fn set_waitbar_state(
3178 handle: FigureHandle,
3179 progress: f64,
3180 message: String,
3181) -> Result<(), FigureError> {
3182 let ((), figure_clone) = with_figure_mut(handle, |state| {
3183 if state.tag.is_empty() {
3184 state.tag = "TMWWaitbar".into();
3185 }
3186 state.waitbar = Some(WaitbarState { progress, message });
3187 })?;
3188 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3189 Ok(())
3190}
3191
3192pub fn waitbar_state_snapshot(handle: FigureHandle) -> Result<Option<WaitbarState>, FigureError> {
3193 let mut reg = registry();
3194 let state = get_state_mut(&mut reg, handle);
3195 Ok(state.waitbar.clone())
3196}
3197
3198pub fn current_or_any_waitbar_handle() -> Option<FigureHandle> {
3199 let reg = registry();
3200 if let Some(state) = reg.figures.get(®.current) {
3201 if state.waitbar.is_some() {
3202 return Some(reg.current);
3203 }
3204 }
3205 reg.figures
3206 .iter()
3207 .find_map(|(handle, state)| state.waitbar.as_ref().map(|_| *handle))
3208}
3209
3210pub fn z_limits_snapshot() -> Option<(f64, f64)> {
3211 let mut reg = registry();
3212 let handle = reg.current;
3213 let state = get_state_mut(&mut reg, handle);
3214 let axes = state.active_axes;
3215 state.figure.axes_metadata(axes).and_then(|m| m.z_limits)
3216}
3217
3218pub fn color_limits_snapshot() -> Option<(f64, f64)> {
3219 let mut reg = registry();
3220 let handle = reg.current;
3221 let state = get_state_mut(&mut reg, handle);
3222 let axes = state.active_axes;
3223 state
3224 .figure
3225 .axes_metadata(axes)
3226 .and_then(|m| m.color_limits)
3227}
3228
3229pub fn set_z_limits(limits: Option<(f64, f64)>) {
3230 let (handle, figure_clone) = {
3231 let mut reg = registry();
3232 let handle = reg.current;
3233 let state = get_state_mut(&mut reg, handle);
3234 let axes = state.active_axes;
3235 state.figure.set_axes_z_limits(axes, limits);
3236 state.revision = state.revision.wrapping_add(1);
3237 (handle, state.figure.clone())
3238 };
3239 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3240}
3241
3242pub fn set_z_limits_for_axes(
3243 handle: FigureHandle,
3244 axes_index: usize,
3245 limits: Option<(f64, f64)>,
3246) -> Result<(), FigureError> {
3247 let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
3248 state.figure.set_axes_z_limits(axes_index, limits);
3249 })?;
3250 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3251 Ok(())
3252}
3253
3254pub fn set_color_limits_runtime(limits: Option<(f64, f64)>) {
3255 let (handle, figure_clone) = {
3256 let mut reg = registry();
3257 let handle = reg.current;
3258 let state = get_state_mut(&mut reg, handle);
3259 let axes = state.active_axes;
3260 state.figure.set_axes_color_limits(axes, limits);
3261 state.revision = state.revision.wrapping_add(1);
3262 (handle, state.figure.clone())
3263 };
3264 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3265}
3266
3267pub fn set_color_limits_for_axes(
3268 handle: FigureHandle,
3269 axes_index: usize,
3270 limits: Option<(f64, f64)>,
3271) -> Result<(), FigureError> {
3272 let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
3273 state.figure.set_axes_color_limits(axes_index, limits);
3274 })?;
3275 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3276 Ok(())
3277}
3278
3279pub fn clear_current_axes() {
3280 let (handle, figure_clone) = {
3281 let mut reg = registry();
3282 let handle = reg.current;
3283 let axes_index = {
3284 let state = get_state_mut(&mut reg, handle);
3285 let axes_index = state.active_axes;
3286 state.figure.clear_axes(axes_index);
3287 state.reset_cycle(axes_index);
3288 state.revision = state.revision.wrapping_add(1);
3289 axes_index
3290 };
3291 purge_plot_children_for_axes(&mut reg, handle, axes_index);
3292 let figure_clone = reg
3293 .figures
3294 .get(&handle)
3295 .expect("figure exists")
3296 .figure
3297 .clone();
3298 (handle, figure_clone)
3299 };
3300 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3301}
3302
3303pub fn set_colorbar_enabled(enabled: bool) {
3304 let (handle, figure_clone) = {
3305 let mut reg = registry();
3306 let handle = reg.current;
3307 let state = get_state_mut(&mut reg, handle);
3308 let axes = state.active_axes;
3309 state.figure.set_axes_colorbar_enabled(axes, enabled);
3310 state.revision = state.revision.wrapping_add(1);
3311 (handle, state.figure.clone())
3312 };
3313 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3314}
3315
3316pub fn set_colorbar_enabled_for_axes(
3317 handle: FigureHandle,
3318 axes_index: usize,
3319 enabled: bool,
3320) -> Result<(), FigureError> {
3321 let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
3322 state.figure.set_axes_colorbar_enabled(axes_index, enabled);
3323 })?;
3324 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3325 Ok(())
3326}
3327
3328pub fn set_legend_for_axes(
3329 handle: FigureHandle,
3330 axes_index: usize,
3331 enabled: bool,
3332 labels: Option<&[String]>,
3333 style: Option<LegendStyle>,
3334) -> Result<f64, FigureError> {
3335 let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
3336 state.figure.set_axes_legend_enabled(axes_index, enabled);
3337 if let Some(labels) = labels {
3338 state.figure.set_labels_for_axes(axes_index, labels);
3339 }
3340 if let Some(style) = style {
3341 state.figure.set_axes_legend_style(axes_index, style);
3342 }
3343 encode_plot_object_handle(handle, axes_index, PlotObjectKind::Legend)
3344 })?;
3345 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3346 Ok(object_handle)
3347}
3348
3349pub fn set_log_modes_for_axes(
3350 handle: FigureHandle,
3351 axes_index: usize,
3352 x_log: bool,
3353 y_log: bool,
3354) -> Result<(), FigureError> {
3355 let figure_clone = {
3356 let mut reg = registry();
3357 let state = reg
3358 .figures
3359 .get_mut(&handle)
3360 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
3361 let total_axes = axes_count(state);
3362 if axes_index >= total_axes {
3363 return Err(FigureError::InvalidSubplotIndex {
3364 rows: state.figure.axes_rows.max(1),
3365 cols: state.figure.axes_cols.max(1),
3366 index: axes_index,
3367 });
3368 }
3369 state.figure.set_axes_log_modes(axes_index, x_log, y_log);
3370 state.revision = state.revision.wrapping_add(1);
3371 state.figure.clone()
3372 };
3373 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3374 Ok(())
3375}
3376
3377pub fn set_y_axis_location_for_axes(
3378 handle: FigureHandle,
3379 axes_index: usize,
3380 location: String,
3381) -> Result<(), FigureError> {
3382 let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
3383 state.figure.set_axes_y_axis_location(axes_index, location);
3384 })?;
3385 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3386 Ok(())
3387}
3388
3389pub fn set_axes_position_for_axes(
3390 handle: FigureHandle,
3391 axes_index: usize,
3392 position: [f64; 4],
3393) -> Result<(), FigureError> {
3394 let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
3395 state.figure.set_axes_position(axes_index, position);
3396 })?;
3397 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3398 Ok(())
3399}
3400
3401pub fn set_axes_units_for_axes(
3402 handle: FigureHandle,
3403 axes_index: usize,
3404 units: String,
3405) -> Result<(), FigureError> {
3406 let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
3407 state.figure.set_axes_units(axes_index, units);
3408 })?;
3409 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3410 Ok(())
3411}
3412
3413pub fn set_view_for_axes(
3414 handle: FigureHandle,
3415 axes_index: usize,
3416 azimuth_deg: f32,
3417 elevation_deg: f32,
3418) -> Result<(), FigureError> {
3419 let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
3420 state
3421 .figure
3422 .set_axes_view(axes_index, azimuth_deg, elevation_deg);
3423 })?;
3424 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3425 Ok(())
3426}
3427
3428pub fn legend_entries_snapshot(
3429 handle: FigureHandle,
3430 axes_index: usize,
3431) -> Result<Vec<runmat_plot::plots::LegendEntry>, FigureError> {
3432 let mut reg = registry();
3433 let state = get_state_mut(&mut reg, handle);
3434 let total_axes = axes_count(state);
3435 if axes_index >= total_axes {
3436 return Err(FigureError::InvalidSubplotIndex {
3437 rows: state.figure.axes_rows.max(1),
3438 cols: state.figure.axes_cols.max(1),
3439 index: axes_index,
3440 });
3441 }
3442 Ok(state.figure.legend_entries_for_axes(axes_index))
3443}
3444
3445pub fn toggle_colorbar() -> bool {
3446 let (handle, figure_clone, enabled) = {
3447 let mut reg = registry();
3448 let handle = reg.current;
3449 let state = get_state_mut(&mut reg, handle);
3450 let axes = state.active_axes;
3451 let next = !state
3452 .figure
3453 .axes_metadata(axes)
3454 .map(|m| m.colorbar_enabled)
3455 .unwrap_or(false);
3456 state.figure.set_axes_colorbar_enabled(axes, next);
3457 state.revision = state.revision.wrapping_add(1);
3458 (handle, state.figure.clone(), next)
3459 };
3460 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3461 enabled
3462}
3463
3464pub fn set_colormap(colormap: ColorMap) {
3465 set_colormap_with_length(colormap, DEFAULT_COLORMAP_LENGTH);
3466}
3467
3468pub fn set_colormap_with_length(colormap: ColorMap, length: usize) {
3469 let (handle, figure_clone) = {
3470 let mut reg = registry();
3471 let handle = reg.current;
3472 let state = get_state_mut(&mut reg, handle);
3473 let axes = state.active_axes;
3474 state.figure.set_axes_colormap(axes, colormap);
3475 state.colormap_lengths.insert(axes, length);
3476 state.revision = state.revision.wrapping_add(1);
3477 (handle, state.figure.clone())
3478 };
3479 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3480}
3481
3482pub fn set_colormap_for_axes(
3483 handle: FigureHandle,
3484 axes_index: usize,
3485 colormap: ColorMap,
3486) -> Result<(), FigureError> {
3487 let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
3488 state.figure.set_axes_colormap(axes_index, colormap);
3489 state
3490 .colormap_lengths
3491 .insert(axes_index, DEFAULT_COLORMAP_LENGTH);
3492 })?;
3493 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3494 Ok(())
3495}
3496
3497pub fn current_colormap_length() -> usize {
3498 let reg = registry();
3499 reg.figures
3500 .get(®.current)
3501 .and_then(|state| state.colormap_lengths.get(&state.active_axes).copied())
3502 .unwrap_or(DEFAULT_COLORMAP_LENGTH)
3503}
3504
3505pub fn set_surface_shading(mode: ShadingMode) {
3506 let (handle, figure_clone) = {
3507 let mut reg = registry();
3508 let handle = reg.current;
3509 let state = get_state_mut(&mut reg, handle);
3510 let plot_count = state.figure.len();
3511 for idx in 0..plot_count {
3512 if let Some(PlotElement::Surface(surface)) = state.figure.get_plot_mut(idx) {
3513 *surface = surface.clone().with_shading(mode);
3514 }
3515 }
3516 state.revision = state.revision.wrapping_add(1);
3517 (handle, state.figure.clone())
3518 };
3519 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
3520}
3521
3522#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3523pub enum FigureEventKind {
3524 Created,
3525 Updated,
3526 Cleared,
3527 Closed,
3528}
3529
3530#[derive(Clone, Copy)]
3531pub struct FigureEventView<'a> {
3532 pub handle: FigureHandle,
3533 pub kind: FigureEventKind,
3534 pub revision: Option<u64>,
3535 pub figure: Option<&'a Figure>,
3536}
3537
3538type FigureObserver = dyn for<'a> Fn(FigureEventView<'a>) + Send + Sync + 'static;
3539
3540struct FigureObserverRegistry {
3541 observers: Mutex<Vec<Arc<FigureObserver>>>,
3542}
3543
3544impl FigureObserverRegistry {
3545 fn new() -> Self {
3546 Self {
3547 observers: Mutex::new(Vec::new()),
3548 }
3549 }
3550
3551 fn install(&self, observer: Arc<FigureObserver>) {
3552 let mut guard = self.observers.lock().expect("figure observers poisoned");
3553 guard.push(observer);
3554 }
3555
3556 fn notify(&self, view: FigureEventView<'_>) {
3557 let snapshot = {
3558 let guard = self.observers.lock().expect("figure observers poisoned");
3559 guard.clone()
3560 };
3561 for observer in snapshot {
3562 observer(view);
3563 }
3564 }
3565
3566 fn is_empty(&self) -> bool {
3567 self.observers
3568 .lock()
3569 .map(|guard| guard.is_empty())
3570 .unwrap_or(true)
3571 }
3572}
3573
3574static FIGURE_OBSERVERS: OnceCell<FigureObserverRegistry> = OnceCell::new();
3575
3576runmat_thread_local! {
3577 static RECENT_FIGURES: RefCell<HashSet<FigureHandle>> = RefCell::new(HashSet::new());
3578 static ACTIVE_AXES_CONTEXT: RefCell<Option<ActiveAxesContext>> = const { RefCell::new(None) };
3579}
3580
3581#[derive(Clone, Copy, Debug)]
3582pub struct FigureAxesState {
3583 pub handle: FigureHandle,
3584 pub rows: usize,
3585 pub cols: usize,
3586 pub active_index: usize,
3587}
3588
3589pub fn encode_axes_handle(handle: FigureHandle, axes_index: usize) -> f64 {
3590 let encoded =
3591 ((handle.as_u32() as u64) << AXES_INDEX_BITS) | ((axes_index as u64) & AXES_INDEX_MASK);
3592 encoded as f64
3593}
3594
3595pub fn encode_plot_object_handle(
3596 handle: FigureHandle,
3597 axes_index: usize,
3598 kind: PlotObjectKind,
3599) -> f64 {
3600 let encoded = (((handle.as_u32() as u64) << AXES_INDEX_BITS)
3601 | ((axes_index as u64) & AXES_INDEX_MASK))
3602 << OBJECT_KIND_BITS
3603 | ((kind as u64) & OBJECT_KIND_MASK);
3604 encoded as f64
3605}
3606
3607pub fn decode_plot_object_handle(
3608 value: f64,
3609) -> Result<(FigureHandle, usize, PlotObjectKind), FigureError> {
3610 if !value.is_finite() || value <= 0.0 {
3611 return Err(FigureError::InvalidPlotObjectHandle);
3612 }
3613 let encoded = value.round() as u64;
3614 let kind = PlotObjectKind::from_u64(encoded & OBJECT_KIND_MASK)
3615 .ok_or(FigureError::InvalidPlotObjectHandle)?;
3616 let base = encoded >> OBJECT_KIND_BITS;
3617 let figure_id = base >> AXES_INDEX_BITS;
3618 if figure_id == 0 {
3619 return Err(FigureError::InvalidPlotObjectHandle);
3620 }
3621 let axes_index = (base & AXES_INDEX_MASK) as usize;
3622 Ok((FigureHandle::from(figure_id as u32), axes_index, kind))
3623}
3624
3625pub fn register_histogram_handle(
3626 figure: FigureHandle,
3627 axes_index: usize,
3628 plot_index: usize,
3629 bin_edges: Vec<f64>,
3630 raw_counts: Vec<f64>,
3631 normalization: String,
3632 metadata: HistogramHandleMetadata,
3633) -> f64 {
3634 let mut reg = registry();
3635 let id = reg.next_plot_child_handle;
3636 reg.next_plot_child_handle += 1;
3637 reg.plot_children.insert(
3638 id,
3639 PlotChildHandleState::Histogram(HistogramHandleState {
3640 figure,
3641 axes_index,
3642 plot_index,
3643 bin_edges,
3644 raw_counts,
3645 normalization,
3646 display_name: None,
3647 metadata,
3648 }),
3649 );
3650 id as f64
3651}
3652
3653#[allow(clippy::too_many_arguments)]
3654pub fn register_histogram2_handle(
3655 figure: FigureHandle,
3656 axes_index: usize,
3657 plot_index: usize,
3658 values: Tensor,
3659 raw_counts: Tensor,
3660 x_bin_edges: Vec<f64>,
3661 y_bin_edges: Vec<f64>,
3662 normalization: String,
3663 display_style: crate::builtins::plotting::histogram2::Histogram2DisplayStyle,
3664 show_empty_bins: bool,
3665 face_alpha: f64,
3666 display_name: Option<String>,
3667) -> f64 {
3668 let mut reg = registry();
3669 let id = reg.next_plot_child_handle;
3670 reg.next_plot_child_handle += 1;
3671 reg.plot_children.insert(
3672 id,
3673 PlotChildHandleState::Histogram2(Histogram2HandleState {
3674 figure,
3675 axes_index,
3676 plot_index,
3677 values,
3678 raw_counts,
3679 x_bin_edges,
3680 y_bin_edges,
3681 normalization,
3682 display_style,
3683 show_empty_bins,
3684 face_alpha,
3685 display_name,
3686 }),
3687 );
3688 id as f64
3689}
3690
3691fn register_simple_plot_handle(
3692 figure: FigureHandle,
3693 axes_index: usize,
3694 plot_index: usize,
3695 constructor: fn(SimplePlotHandleState) -> PlotChildHandleState,
3696) -> f64 {
3697 let mut reg = registry();
3698 let id = reg.next_plot_child_handle;
3699 reg.next_plot_child_handle += 1;
3700 reg.plot_children.insert(
3701 id,
3702 constructor(SimplePlotHandleState {
3703 figure,
3704 axes_index,
3705 plot_index,
3706 }),
3707 );
3708 id as f64
3709}
3710
3711pub fn register_line_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
3712 register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Line)
3713}
3714
3715pub fn register_animated_line_handle(
3716 figure: FigureHandle,
3717 axes_index: usize,
3718 plot_index: usize,
3719 is_3d: bool,
3720 maximum_num_points: Option<usize>,
3721) -> f64 {
3722 let mut reg = registry();
3723 let id = reg.next_plot_child_handle;
3724 reg.next_plot_child_handle += 1;
3725 reg.plot_children.insert(
3726 id,
3727 PlotChildHandleState::AnimatedLine(AnimatedLineHandleState {
3728 figure,
3729 axes_index,
3730 plot_index,
3731 is_3d,
3732 maximum_num_points,
3733 }),
3734 );
3735 id as f64
3736}
3737
3738pub fn register_reference_line_handle(
3739 figure: FigureHandle,
3740 axes_index: usize,
3741 plot_index: usize,
3742) -> f64 {
3743 register_simple_plot_handle(
3744 figure,
3745 axes_index,
3746 plot_index,
3747 PlotChildHandleState::ReferenceLine,
3748 )
3749}
3750
3751pub fn register_scatter_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
3752 register_simple_plot_handle(
3753 figure,
3754 axes_index,
3755 plot_index,
3756 PlotChildHandleState::Scatter,
3757 )
3758}
3759
3760pub fn register_bar_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
3761 register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Bar)
3762}
3763
3764pub fn register_stem_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
3765 register_simple_plot_handle(figure, axes_index, plot_index, |state| {
3766 PlotChildHandleState::Stem(StemHandleState {
3767 figure: state.figure,
3768 axes_index: state.axes_index,
3769 plot_index: state.plot_index,
3770 })
3771 })
3772}
3773
3774pub fn register_errorbar_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
3775 register_simple_plot_handle(figure, axes_index, plot_index, |state| {
3776 PlotChildHandleState::ErrorBar(ErrorBarHandleState {
3777 figure: state.figure,
3778 axes_index: state.axes_index,
3779 plot_index: state.plot_index,
3780 })
3781 })
3782}
3783
3784pub fn register_stairs_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
3785 register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Stairs)
3786}
3787
3788pub fn register_quiver_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
3789 register_simple_plot_handle(figure, axes_index, plot_index, |state| {
3790 PlotChildHandleState::Quiver(QuiverHandleState {
3791 figure: state.figure,
3792 axes_index: state.axes_index,
3793 plot_index: state.plot_index,
3794 is_3d: false,
3795 })
3796 })
3797}
3798
3799pub fn register_quiver3_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
3800 register_simple_plot_handle(figure, axes_index, plot_index, |state| {
3801 PlotChildHandleState::Quiver(QuiverHandleState {
3802 figure: state.figure,
3803 axes_index: state.axes_index,
3804 plot_index: state.plot_index,
3805 is_3d: true,
3806 })
3807 })
3808}
3809
3810pub fn register_image_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
3811 register_simple_plot_handle(figure, axes_index, plot_index, |state| {
3812 PlotChildHandleState::Image(ImageHandleState {
3813 figure: state.figure,
3814 axes_index: state.axes_index,
3815 plot_index: state.plot_index,
3816 })
3817 })
3818}
3819
3820pub fn register_heatmap_handle(
3821 figure: FigureHandle,
3822 axes_index: usize,
3823 plot_index: usize,
3824 x_labels: Vec<String>,
3825 y_labels: Vec<String>,
3826 color_data: Tensor,
3827) -> f64 {
3828 let mut reg = registry();
3829 let id = reg.next_plot_child_handle;
3830 reg.next_plot_child_handle += 1;
3831 reg.plot_children.insert(
3832 id,
3833 PlotChildHandleState::Heatmap(HeatmapHandleState {
3834 figure,
3835 axes_index,
3836 plot_index,
3837 x_labels,
3838 y_labels,
3839 color_data,
3840 }),
3841 );
3842 id as f64
3843}
3844
3845pub fn register_binscatter_handle(
3846 figure: FigureHandle,
3847 axes_index: usize,
3848 plot_index: usize,
3849 values: Tensor,
3850 x_bin_edges: Vec<f64>,
3851 y_bin_edges: Vec<f64>,
3852 x_data: Vec<f64>,
3853 y_data: Vec<f64>,
3854 num_bins: [usize; 2],
3855 auto_bins: bool,
3856 x_limits_option: Option<(f64, f64)>,
3857 y_limits_option: Option<(f64, f64)>,
3858 show_empty_bins: bool,
3859 face_alpha: f64,
3860 display_name: Option<String>,
3861) -> f64 {
3862 let x_limits = (
3863 *x_bin_edges.first().unwrap_or(&0.0),
3864 *x_bin_edges.last().unwrap_or(&1.0),
3865 );
3866 let y_limits = (
3867 *y_bin_edges.first().unwrap_or(&0.0),
3868 *y_bin_edges.last().unwrap_or(&1.0),
3869 );
3870 let mut reg = registry();
3871 let id = reg.next_plot_child_handle;
3872 reg.next_plot_child_handle += 1;
3873 reg.plot_children.insert(
3874 id,
3875 PlotChildHandleState::Binscatter(BinscatterHandleState {
3876 figure,
3877 axes_index,
3878 plot_index,
3879 values,
3880 x_bin_edges,
3881 y_bin_edges,
3882 x_data,
3883 y_data,
3884 num_bins,
3885 auto_bins,
3886 x_limits_option,
3887 y_limits_option,
3888 x_limits,
3889 y_limits,
3890 show_empty_bins,
3891 face_alpha,
3892 display_name,
3893 }),
3894 );
3895 id as f64
3896}
3897
3898pub fn register_function_surface_handle(
3899 figure: FigureHandle,
3900 axes_index: usize,
3901 plot_index: usize,
3902 mesh_density: usize,
3903 x_range: (f64, f64),
3904 y_range: (f64, f64),
3905 function: FunctionSurfaceFunctionState,
3906) -> f64 {
3907 let mut reg = registry();
3908 let id = reg.next_plot_child_handle;
3909 reg.next_plot_child_handle += 1;
3910 reg.plot_children.insert(
3911 id,
3912 PlotChildHandleState::FunctionSurface(FunctionSurfaceHandleState {
3913 figure,
3914 axes_index,
3915 plot_index,
3916 mesh_density,
3917 x_range,
3918 y_range,
3919 function,
3920 }),
3921 );
3922 id as f64
3923}
3924
3925pub fn register_function_contour_handle(
3926 figure: FigureHandle,
3927 axes_index: usize,
3928 plot_index: usize,
3929 mesh_density: usize,
3930 x_range: (f64, f64),
3931 y_range: (f64, f64),
3932 function: FunctionSurfaceFunctionRef,
3933) -> f64 {
3934 let mut reg = registry();
3935 let id = reg.next_plot_child_handle;
3936 reg.next_plot_child_handle += 1;
3937 reg.plot_children.insert(
3938 id,
3939 PlotChildHandleState::FunctionContour(FunctionContourHandleState {
3940 figure,
3941 axes_index,
3942 plot_index,
3943 mesh_density,
3944 x_range,
3945 y_range,
3946 function,
3947 }),
3948 );
3949 id as f64
3950}
3951
3952pub fn update_binscatter_handle_for_plot(
3953 figure: FigureHandle,
3954 plot_index: usize,
3955 updater: impl FnOnce(&mut BinscatterHandleState),
3956) -> Result<(), FigureError> {
3957 let mut updater = Some(updater);
3958 let mut reg = registry();
3959 for state in reg.plot_children.values_mut() {
3960 if let PlotChildHandleState::Binscatter(binscatter) = state {
3961 if binscatter.figure == figure && binscatter.plot_index == plot_index {
3962 let Some(updater) = updater.take() else {
3963 return Ok(());
3964 };
3965 updater(binscatter);
3966 return Ok(());
3967 }
3968 }
3969 }
3970 Err(FigureError::InvalidPlotObjectHandle)
3971}
3972
3973pub fn register_area_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
3974 register_simple_plot_handle(figure, axes_index, plot_index, |state| {
3975 PlotChildHandleState::Area(AreaHandleState {
3976 figure: state.figure,
3977 axes_index: state.axes_index,
3978 plot_index: state.plot_index,
3979 })
3980 })
3981}
3982
3983pub fn register_surface_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
3984 register_simple_plot_handle(
3985 figure,
3986 axes_index,
3987 plot_index,
3988 PlotChildHandleState::Surface,
3989 )
3990}
3991
3992pub fn register_patch_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
3993 register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Patch)
3994}
3995
3996pub fn register_line3_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
3997 register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Line3)
3998}
3999
4000pub fn register_scatter3_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
4001 register_simple_plot_handle(
4002 figure,
4003 axes_index,
4004 plot_index,
4005 PlotChildHandleState::Scatter3,
4006 )
4007}
4008
4009pub fn register_contour_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
4010 register_simple_plot_handle(
4011 figure,
4012 axes_index,
4013 plot_index,
4014 PlotChildHandleState::Contour,
4015 )
4016}
4017
4018pub fn register_contour_fill_handle(
4019 figure: FigureHandle,
4020 axes_index: usize,
4021 plot_index: usize,
4022) -> f64 {
4023 register_simple_plot_handle(
4024 figure,
4025 axes_index,
4026 plot_index,
4027 PlotChildHandleState::ContourFill,
4028 )
4029}
4030
4031pub fn register_pie_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
4032 register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Pie)
4033}
4034
4035pub fn register_text_annotation_handle(
4036 figure: FigureHandle,
4037 axes_index: usize,
4038 annotation_index: usize,
4039) -> f64 {
4040 let mut reg = registry();
4041 let id = reg.next_plot_child_handle;
4042 reg.next_plot_child_handle += 1;
4043 reg.plot_children.insert(
4044 id,
4045 PlotChildHandleState::Text(TextAnnotationHandleState {
4046 figure,
4047 axes_index,
4048 annotation_index,
4049 }),
4050 );
4051 id as f64
4052}
4053
4054pub fn register_textscatter_handle(state: TextScatterHandleState) -> f64 {
4055 let mut reg = registry();
4056 let id = reg.next_plot_child_handle;
4057 reg.next_plot_child_handle += 1;
4058 reg.plot_children
4059 .insert(id, PlotChildHandleState::TextScatter(state));
4060 id as f64
4061}
4062
4063pub fn update_textscatter_handle_state(
4064 handle: f64,
4065 state: TextScatterHandleState,
4066) -> Result<(), FigureError> {
4067 if !handle.is_finite() || handle <= 0.0 {
4068 return Err(FigureError::InvalidPlotObjectHandle);
4069 }
4070 let mut reg = registry();
4071 let id = handle.round() as u64;
4072 match reg.plot_children.get_mut(&id) {
4073 Some(PlotChildHandleState::TextScatter(slot)) => {
4074 *slot = state;
4075 Ok(())
4076 }
4077 _ => Err(FigureError::InvalidPlotObjectHandle),
4078 }
4079}
4080
4081pub fn update_textscatter_figure(
4082 state: &TextScatterHandleState,
4083 mut apply: impl FnMut(&mut Figure) -> Result<(), FigureError>,
4084) -> Result<(), FigureError> {
4085 let (result, figure_clone) =
4086 with_axes_target_mut(state.figure, state.axes_index, |figure_state| {
4087 apply(&mut figure_state.figure)
4088 })?;
4089 result?;
4090 notify_with_figure(state.figure, &figure_clone, FigureEventKind::Updated);
4091 Ok(())
4092}
4093
4094pub fn register_wordcloud_handle(state: WordCloudHandleState) -> f64 {
4095 let mut reg = registry();
4096 let id = reg.next_plot_child_handle;
4097 reg.next_plot_child_handle += 1;
4098 reg.plot_children
4099 .insert(id, PlotChildHandleState::WordCloud(state));
4100 id as f64
4101}
4102
4103pub fn update_wordcloud_handle_state(
4104 handle: f64,
4105 state: WordCloudHandleState,
4106) -> Result<(), FigureError> {
4107 if !handle.is_finite() || handle <= 0.0 {
4108 return Err(FigureError::InvalidPlotObjectHandle);
4109 }
4110 let mut reg = registry();
4111 let id = handle.round() as u64;
4112 match reg.plot_children.get_mut(&id) {
4113 Some(PlotChildHandleState::WordCloud(slot)) => {
4114 *slot = state;
4115 Ok(())
4116 }
4117 _ => Err(FigureError::InvalidPlotObjectHandle),
4118 }
4119}
4120
4121pub fn update_wordcloud_figure(
4122 state: &WordCloudHandleState,
4123 mut apply: impl FnMut(&mut Figure) -> Result<(), FigureError>,
4124) -> Result<(), FigureError> {
4125 let (result, figure_clone) =
4126 with_axes_target_mut(state.figure, state.axes_index, |figure_state| {
4127 apply(&mut figure_state.figure)
4128 })?;
4129 result?;
4130 notify_with_figure(state.figure, &figure_clone, FigureEventKind::Updated);
4131 Ok(())
4132}
4133
4134pub fn register_stackedplot_handle(state: StackedPlotHandleState) -> f64 {
4135 let mut reg = registry();
4136 let id = reg.next_plot_child_handle;
4137 reg.next_plot_child_handle += 1;
4138 reg.plot_children
4139 .insert(id, PlotChildHandleState::StackedPlot(state));
4140 id as f64
4141}
4142
4143pub fn update_stackedplot_handle_state(
4144 handle: f64,
4145 state: StackedPlotHandleState,
4146) -> Result<(), FigureError> {
4147 if !handle.is_finite() || handle <= 0.0 {
4148 return Err(FigureError::InvalidPlotObjectHandle);
4149 }
4150 let mut reg = registry();
4151 let id = handle.round() as u64;
4152 match reg.plot_children.get_mut(&id) {
4153 Some(PlotChildHandleState::StackedPlot(slot)) => {
4154 *slot = state;
4155 Ok(())
4156 }
4157 _ => Err(FigureError::InvalidPlotObjectHandle),
4158 }
4159}
4160
4161pub fn update_stackedplot_figure(
4162 state: &StackedPlotHandleState,
4163 mut apply: impl FnMut(&mut Figure) -> Result<(), FigureError>,
4164) -> Result<(), FigureError> {
4165 let axes_index = state.axes_indices.first().copied().unwrap_or(0);
4166 let (result, figure_clone) = with_axes_target_mut(state.figure, axes_index, |figure_state| {
4167 apply(&mut figure_state.figure)
4168 })?;
4169 result?;
4170 notify_with_figure(state.figure, &figure_clone, FigureEventKind::Updated);
4171 Ok(())
4172}
4173
4174pub fn render_stackedplot_chart<F>(
4175 builtin: &'static str,
4176 target: Option<FigureHandle>,
4177 axes_count: usize,
4178 mut apply: F,
4179) -> BuiltinResult<(FigureHandle, Vec<usize>, String)>
4180where
4181 F: FnMut(&mut Figure, &[usize]) -> BuiltinResult<()>,
4182{
4183 if axes_count == 0 {
4184 return Err(crate::builtins::plotting::plotting_error(
4185 builtin,
4186 format!("{builtin}: expected at least one plotted variable"),
4187 ));
4188 }
4189 let rendering_disabled = interactive_rendering_disabled();
4190 let host_managed_rendering = host_managed_rendering_enabled();
4191 let (handle, axes_indices, figure_clone) = {
4192 let mut reg = registry();
4193 let handle = target.unwrap_or(reg.current);
4194 let axes_indices = (0..axes_count).collect::<Vec<_>>();
4195 {
4196 let state = get_state_mut(&mut reg, handle);
4197 state.figure.set_subplot_grid(axes_count, 1);
4198 for axes_index in &axes_indices {
4199 state.figure.clear_axes(*axes_index);
4200 state.figure.set_axes_kind(*axes_index, AxesKind::Cartesian);
4201 state.figure.set_axes_limits(*axes_index, None, None);
4202 state.figure.set_axes_z_limits(*axes_index, None);
4203 state.figure.set_axes_grid_enabled(*axes_index, true);
4204 state.figure.set_axes_minor_grid_enabled(*axes_index, false);
4205 state.figure.set_axes_legend_enabled(*axes_index, false);
4206 state.reset_cycle(*axes_index);
4207 }
4208 state.active_axes = *axes_indices.last().unwrap_or(&0);
4209 state.figure.set_active_axes_index(state.active_axes);
4210 }
4211 for axes_index in &axes_indices {
4212 purge_plot_children_for_axes(&mut reg, handle, *axes_index);
4213 }
4214 {
4215 let state = get_state_mut(&mut reg, handle);
4216 apply(&mut state.figure, &axes_indices)
4217 .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
4218 state.revision = state.revision.wrapping_add(1);
4219 }
4220 reg.current = handle;
4221 let figure_clone = reg
4222 .figures
4223 .get(&handle)
4224 .expect("figure exists")
4225 .figure
4226 .clone();
4227 (handle, axes_indices, figure_clone)
4228 };
4229 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
4230 let presentation = present_figure_update_with_options(
4231 builtin,
4232 handle,
4233 figure_clone,
4234 rendering_disabled,
4235 host_managed_rendering,
4236 )?;
4237 Ok((handle, axes_indices, presentation))
4238}
4239
4240#[derive(Clone, Copy, Debug)]
4241pub enum CopyParentTarget {
4242 Figure(FigureHandle),
4243 Axes(FigureHandle, usize),
4244}
4245
4246pub fn plot_child_handle_snapshot(handle: f64) -> Result<PlotChildHandleState, FigureError> {
4247 if !handle.is_finite() || handle <= 0.0 {
4248 return Err(FigureError::InvalidPlotObjectHandle);
4249 }
4250 let reg = registry();
4251 reg.plot_children
4252 .get(&(handle.round() as u64))
4253 .cloned()
4254 .ok_or(FigureError::InvalidPlotObjectHandle)
4255}
4256
4257pub fn validate_plot_child_copy_source(source_handle: f64) -> Result<(), FigureError> {
4258 if !source_handle.is_finite() || source_handle <= 0.0 {
4259 return Err(FigureError::InvalidPlotObjectHandle);
4260 }
4261 let reg = registry();
4262 let source_state = reg
4263 .plot_children
4264 .get(&(source_handle.round() as u64))
4265 .ok_or(FigureError::InvalidPlotObjectHandle)?;
4266 let source_plot_index = source_state
4267 .plot_index()
4268 .ok_or(FigureError::InvalidPlotObjectHandle)?;
4269 let (source_figure, _) = source_state.figure_axes();
4270 reg.figures
4271 .get(&source_figure)
4272 .and_then(|state| state.figure.plots().nth(source_plot_index))
4273 .ok_or(FigureError::InvalidPlotObjectHandle)?;
4274 Ok(())
4275}
4276
4277pub fn copy_plot_child_to_parent(
4278 source_handle: f64,
4279 target: CopyParentTarget,
4280) -> Result<f64, FigureError> {
4281 if !source_handle.is_finite() || source_handle <= 0.0 {
4282 return Err(FigureError::InvalidPlotObjectHandle);
4283 }
4284
4285 let (new_handle, target_figure, figure_clone) = {
4286 let mut reg = registry();
4287 let source_key = source_handle.round() as u64;
4288 let source_state = reg
4289 .plot_children
4290 .get(&source_key)
4291 .cloned()
4292 .ok_or(FigureError::InvalidPlotObjectHandle)?;
4293 let source_plot_index = source_state
4294 .plot_index()
4295 .ok_or(FigureError::InvalidPlotObjectHandle)?;
4296 let (source_figure, _) = source_state.figure_axes();
4297 let plot = reg
4298 .figures
4299 .get(&source_figure)
4300 .and_then(|state| state.figure.plots().nth(source_plot_index))
4301 .cloned()
4302 .ok_or(FigureError::InvalidPlotObjectHandle)?;
4303
4304 let (target_figure, target_axes) = match target {
4305 CopyParentTarget::Figure(figure) => {
4306 let axes_index = get_state_mut(&mut reg, figure).active_axes;
4307 (figure, axes_index)
4308 }
4309 CopyParentTarget::Axes(figure, axes_index) => (figure, axes_index),
4310 };
4311
4312 let (new_plot_index, figure_clone) = {
4313 let target_state = get_state_mut(&mut reg, target_figure);
4314 target_state.figure.ensure_axes(target_axes);
4315 target_state.figure.set_active_axes_index(target_axes);
4316 target_state.active_axes = target_axes;
4317 let new_plot_index = target_state
4318 .figure
4319 .add_plot_element_on_axes(plot, target_axes);
4320 target_state.revision = target_state.revision.wrapping_add(1);
4321 (new_plot_index, target_state.figure.clone())
4322 };
4323
4324 let remapped_state = source_state
4325 .with_plot_location(target_figure, target_axes, new_plot_index)
4326 .ok_or(FigureError::InvalidPlotObjectHandle)?;
4327 let new_key = reg.next_plot_child_handle;
4328 reg.next_plot_child_handle += 1;
4329 reg.plot_children.insert(new_key, remapped_state);
4330
4331 (new_key as f64, target_figure, figure_clone)
4332 };
4333
4334 notify_with_figure(target_figure, &figure_clone, FigureEventKind::Updated);
4335 Ok(new_handle)
4336}
4337
4338pub fn set_heatmap_display_labels(
4339 figure: FigureHandle,
4340 axes_index: usize,
4341 plot_index: usize,
4342 x_labels: Option<Vec<String>>,
4343 y_labels: Option<Vec<String>>,
4344) -> Result<(), FigureError> {
4345 let figure_clone = {
4346 let mut reg = registry();
4347 let (current_x_labels, current_y_labels) = {
4348 let state = reg.plot_children.values_mut().find(|state| match state {
4349 PlotChildHandleState::Heatmap(heatmap) => {
4350 heatmap.figure == figure
4351 && heatmap.axes_index == axes_index
4352 && heatmap.plot_index == plot_index
4353 }
4354 _ => false,
4355 });
4356 let PlotChildHandleState::Heatmap(heatmap) =
4357 state.ok_or(FigureError::InvalidPlotObjectHandle)?
4358 else {
4359 return Err(FigureError::InvalidPlotObjectHandle);
4360 };
4361 if let Some(labels) = x_labels {
4362 heatmap.x_labels = labels;
4363 }
4364 if let Some(labels) = y_labels {
4365 heatmap.y_labels = labels;
4366 }
4367 (heatmap.x_labels.clone(), heatmap.y_labels.clone())
4368 };
4369
4370 let state = get_state_mut(&mut reg, figure);
4371 let total_axes = axes_count(state);
4372 if axes_index >= total_axes {
4373 return Err(FigureError::InvalidSubplotIndex {
4374 rows: state.figure.axes_rows.max(1),
4375 cols: state.figure.axes_cols.max(1),
4376 index: axes_index,
4377 });
4378 }
4379 state.active_axes = axes_index;
4380 state.figure.set_active_axes_index(axes_index);
4381 state.figure.set_axes_tick_labels(
4382 axes_index,
4383 Some(current_x_labels),
4384 Some(current_y_labels),
4385 );
4386 state.revision = state.revision.wrapping_add(1);
4387 state.figure.clone()
4388 };
4389 notify_with_figure(figure, &figure_clone, FigureEventKind::Updated);
4390 Ok(())
4391}
4392
4393pub fn update_histogram_handle_for_plot(
4394 figure: FigureHandle,
4395 axes_index: usize,
4396 plot_index: usize,
4397 normalization: String,
4398 raw_counts: Vec<f64>,
4399) -> Result<(), FigureError> {
4400 let mut reg = registry();
4401 let state = reg.plot_children.values_mut().find(|state| match state {
4402 PlotChildHandleState::Histogram(hist) => {
4403 hist.figure == figure && hist.axes_index == axes_index && hist.plot_index == plot_index
4404 }
4405 _ => false,
4406 });
4407 match state.ok_or(FigureError::InvalidPlotObjectHandle)? {
4408 PlotChildHandleState::Histogram(hist) => {
4409 hist.normalization = normalization;
4410 hist.raw_counts = raw_counts;
4411 Ok(())
4412 }
4413 _ => Err(FigureError::InvalidPlotObjectHandle),
4414 }
4415}
4416
4417pub fn update_histogram2_handle_for_plot(
4418 figure: FigureHandle,
4419 axes_index: usize,
4420 plot_index: usize,
4421 updater: impl FnOnce(&mut Histogram2HandleState),
4422) -> Result<(), FigureError> {
4423 let mut reg = registry();
4424 let state = reg.plot_children.values_mut().find(|state| match state {
4425 PlotChildHandleState::Histogram2(hist) => {
4426 hist.figure == figure && hist.axes_index == axes_index && hist.plot_index == plot_index
4427 }
4428 _ => false,
4429 });
4430 match state.ok_or(FigureError::InvalidPlotObjectHandle)? {
4431 PlotChildHandleState::Histogram2(hist) => {
4432 updater(hist);
4433 Ok(())
4434 }
4435 _ => Err(FigureError::InvalidPlotObjectHandle),
4436 }
4437}
4438
4439pub fn set_histogram_handle_display_name(
4440 figure: FigureHandle,
4441 axes_index: usize,
4442 plot_index: usize,
4443 display_name: Option<String>,
4444) -> Result<(), FigureError> {
4445 let mut reg = registry();
4446 let state = reg.plot_children.values_mut().find(|state| match state {
4447 PlotChildHandleState::Histogram(hist) => {
4448 hist.figure == figure && hist.axes_index == axes_index && hist.plot_index == plot_index
4449 }
4450 _ => false,
4451 });
4452 match state.ok_or(FigureError::InvalidPlotObjectHandle)? {
4453 PlotChildHandleState::Histogram(hist) => {
4454 hist.display_name = display_name;
4455 Ok(())
4456 }
4457 _ => Err(FigureError::InvalidPlotObjectHandle),
4458 }
4459}
4460
4461pub fn update_histogram_handle_metadata_for_plot(
4462 figure: FigureHandle,
4463 axes_index: usize,
4464 plot_index: usize,
4465 updater: impl FnOnce(&mut HistogramHandleMetadata),
4466) -> Result<(), FigureError> {
4467 let mut reg = registry();
4468 let state = reg.plot_children.values_mut().find(|state| match state {
4469 PlotChildHandleState::Histogram(hist) => {
4470 hist.figure == figure && hist.axes_index == axes_index && hist.plot_index == plot_index
4471 }
4472 _ => false,
4473 });
4474 match state.ok_or(FigureError::InvalidPlotObjectHandle)? {
4475 PlotChildHandleState::Histogram(hist) => {
4476 updater(&mut hist.metadata);
4477 Ok(())
4478 }
4479 _ => Err(FigureError::InvalidPlotObjectHandle),
4480 }
4481}
4482
4483pub fn update_errorbar_plot(
4484 figure_handle: FigureHandle,
4485 plot_index: usize,
4486 updater: impl FnOnce(&mut runmat_plot::plots::ErrorBar),
4487) -> Result<(), FigureError> {
4488 let mut reg = registry();
4489 let state = get_state_mut(&mut reg, figure_handle);
4490 let plot = state
4491 .figure
4492 .get_plot_mut(plot_index)
4493 .ok_or(FigureError::InvalidPlotObjectHandle)?;
4494 match plot {
4495 runmat_plot::plots::figure::PlotElement::ErrorBar(errorbar) => {
4496 updater(errorbar);
4497 Ok(())
4498 }
4499 _ => Err(FigureError::InvalidPlotObjectHandle),
4500 }
4501}
4502
4503pub fn update_histogram_plot_data(
4504 figure_handle: FigureHandle,
4505 plot_index: usize,
4506 labels: Vec<String>,
4507 values: Vec<f64>,
4508) -> Result<(), FigureError> {
4509 let mut reg = registry();
4510 let state = get_state_mut(&mut reg, figure_handle);
4511 let plot = state
4512 .figure
4513 .get_plot_mut(plot_index)
4514 .ok_or(FigureError::InvalidPlotObjectHandle)?;
4515 match plot {
4516 runmat_plot::plots::figure::PlotElement::Bar(bar) => {
4517 bar.set_data(labels, values)
4518 .map_err(|_| FigureError::InvalidPlotObjectHandle)?;
4519 Ok(())
4520 }
4521 _ => Err(FigureError::InvalidPlotObjectHandle),
4522 }
4523}
4524
4525pub fn update_stem_plot(
4526 figure_handle: FigureHandle,
4527 plot_index: usize,
4528 updater: impl FnOnce(&mut runmat_plot::plots::StemPlot),
4529) -> Result<(), FigureError> {
4530 let mut reg = registry();
4531 let state = get_state_mut(&mut reg, figure_handle);
4532 let plot = state
4533 .figure
4534 .get_plot_mut(plot_index)
4535 .ok_or(FigureError::InvalidPlotObjectHandle)?;
4536 match plot {
4537 runmat_plot::plots::figure::PlotElement::Stem(stem) => {
4538 updater(stem);
4539 Ok(())
4540 }
4541 _ => Err(FigureError::InvalidPlotObjectHandle),
4542 }
4543}
4544
4545pub fn update_quiver_plot(
4546 figure_handle: FigureHandle,
4547 plot_index: usize,
4548 updater: impl FnOnce(&mut runmat_plot::plots::QuiverPlot),
4549) -> Result<(), FigureError> {
4550 let mut reg = registry();
4551 let state = get_state_mut(&mut reg, figure_handle);
4552 let plot = state
4553 .figure
4554 .get_plot_mut(plot_index)
4555 .ok_or(FigureError::InvalidPlotObjectHandle)?;
4556 match plot {
4557 runmat_plot::plots::figure::PlotElement::Quiver(quiver) => {
4558 updater(quiver);
4559 Ok(())
4560 }
4561 _ => Err(FigureError::InvalidPlotObjectHandle),
4562 }
4563}
4564
4565pub fn update_image_plot(
4566 figure_handle: FigureHandle,
4567 plot_index: usize,
4568 updater: impl FnOnce(&mut runmat_plot::plots::SurfacePlot),
4569) -> Result<(), FigureError> {
4570 let mut reg = registry();
4571 let state = get_state_mut(&mut reg, figure_handle);
4572 let plot = state
4573 .figure
4574 .get_plot_mut(plot_index)
4575 .ok_or(FigureError::InvalidPlotObjectHandle)?;
4576 match plot {
4577 runmat_plot::plots::figure::PlotElement::Surface(surface) if surface.image_mode => {
4578 updater(surface);
4579 Ok(())
4580 }
4581 _ => Err(FigureError::InvalidPlotObjectHandle),
4582 }
4583}
4584
4585pub fn update_area_plot(
4586 figure_handle: FigureHandle,
4587 plot_index: usize,
4588 updater: impl FnOnce(&mut runmat_plot::plots::AreaPlot),
4589) -> Result<(), FigureError> {
4590 let mut reg = registry();
4591 let state = get_state_mut(&mut reg, figure_handle);
4592 let plot = state
4593 .figure
4594 .get_plot_mut(plot_index)
4595 .ok_or(FigureError::InvalidPlotObjectHandle)?;
4596 match plot {
4597 runmat_plot::plots::figure::PlotElement::Area(area) => {
4598 updater(area);
4599 Ok(())
4600 }
4601 _ => Err(FigureError::InvalidPlotObjectHandle),
4602 }
4603}
4604
4605pub fn update_plot_element(
4606 figure_handle: FigureHandle,
4607 plot_index: usize,
4608 updater: impl FnOnce(&mut runmat_plot::plots::figure::PlotElement),
4609) -> Result<(), FigureError> {
4610 let mut reg = registry();
4611 let state = get_state_mut(&mut reg, figure_handle);
4612 let plot = state
4613 .figure
4614 .get_plot_mut(plot_index)
4615 .ok_or(FigureError::InvalidPlotObjectHandle)?;
4616 updater(plot);
4617 Ok(())
4618}
4619
4620pub fn append_points_to_animated_line(
4621 handle: &AnimatedLineHandleState,
4622 x: Vec<f64>,
4623 y: Vec<f64>,
4624 z: Option<Vec<f64>>,
4625) -> Result<(), String> {
4626 if x.len() != y.len() || z.as_ref().is_some_and(|z| z.len() != x.len()) {
4627 return Err("coordinate vectors must have the same length".to_string());
4628 }
4629 if x.is_empty() {
4630 return Ok(());
4631 }
4632
4633 let (figure_clone, became_3d) = {
4634 let mut reg = registry();
4635 let mut became_3d = false;
4636 let figure_clone = {
4637 let state = get_state_mut(&mut reg, handle.figure);
4638 let plot = state
4639 .figure
4640 .get_plot_mut(handle.plot_index)
4641 .ok_or_else(|| "invalid animated line handle".to_string())?;
4642
4643 match z {
4644 None => match plot {
4645 PlotElement::Line(line) => {
4646 let mut next_x = std::mem::take(&mut line.x_data);
4647 let mut next_y = std::mem::take(&mut line.y_data);
4648 next_x.extend(x);
4649 next_y.extend(y);
4650 trim_oldest_xy(handle.maximum_num_points, &mut next_x, &mut next_y);
4651 line.update_data(next_x, next_y)
4652 .map_err(|err| format!("failed to update animated line: {err}"))?;
4653 }
4654 PlotElement::Line3(_) => {
4655 return Err(
4656 "3-D animated lines require X, Y, and Z coordinates".to_string()
4657 );
4658 }
4659 _ => return Err("invalid animated line handle".to_string()),
4660 },
4661 Some(z) => match plot {
4662 PlotElement::Line(line) => {
4663 if line.marker.is_some() {
4664 return Err("3-D animated lines do not support marker properties yet"
4665 .to_string());
4666 }
4667 let mut next_x = std::mem::take(&mut line.x_data);
4668 let mut next_y = std::mem::take(&mut line.y_data);
4669 let mut next_z = vec![0.0; next_x.len()];
4670 next_x.extend(x);
4671 next_y.extend(y);
4672 next_z.extend(z);
4673 trim_oldest_xyz(
4674 handle.maximum_num_points,
4675 &mut next_x,
4676 &mut next_y,
4677 &mut next_z,
4678 );
4679 let mut line3 = runmat_plot::plots::Line3Plot::new(next_x, next_y, next_z)
4680 .map_err(|err| format!("failed to update animated line: {err}"))?;
4681 line3.color = line.color;
4682 line3.line_width = line.line_width;
4683 line3.line_style = line.line_style;
4684 line3.label = line.label.clone();
4685 line3.visible = line.visible;
4686 *plot = PlotElement::Line3(line3);
4687 became_3d = true;
4688 }
4689 PlotElement::Line3(line) => {
4690 let mut next_x = std::mem::take(&mut line.x_data);
4691 let mut next_y = std::mem::take(&mut line.y_data);
4692 let mut next_z = std::mem::take(&mut line.z_data);
4693 next_x.extend(x);
4694 next_y.extend(y);
4695 next_z.extend(z);
4696 trim_oldest_xyz(
4697 handle.maximum_num_points,
4698 &mut next_x,
4699 &mut next_y,
4700 &mut next_z,
4701 );
4702 line.update_data(next_x, next_y, next_z)
4703 .map_err(|err| format!("failed to update animated line: {err}"))?;
4704 }
4705 _ => return Err("invalid animated line handle".to_string()),
4706 },
4707 }
4708
4709 state.revision = state.revision.wrapping_add(1);
4710 state.figure.clone()
4711 };
4712
4713 if became_3d {
4714 for child in reg.plot_children.values_mut() {
4715 if let PlotChildHandleState::AnimatedLine(animated) = child {
4716 if animated.figure == handle.figure && animated.plot_index == handle.plot_index
4717 {
4718 animated.is_3d = true;
4719 }
4720 }
4721 }
4722 }
4723 (figure_clone, became_3d)
4724 };
4725 let _ = became_3d;
4726 notify_with_figure(handle.figure, &figure_clone, FigureEventKind::Updated);
4727 Ok(())
4728}
4729
4730pub fn set_animated_line_maximum_num_points(
4731 handle: &AnimatedLineHandleState,
4732 maximum_num_points: Option<usize>,
4733) -> Result<(), String> {
4734 let figure_clone = {
4735 let mut reg = registry();
4736 let figure_clone = {
4737 let state = get_state_mut(&mut reg, handle.figure);
4738 let plot = state
4739 .figure
4740 .get_plot_mut(handle.plot_index)
4741 .ok_or_else(|| "invalid animated line handle".to_string())?;
4742 match plot {
4743 PlotElement::Line(line) => {
4744 let mut x = std::mem::take(&mut line.x_data);
4745 let mut y = std::mem::take(&mut line.y_data);
4746 trim_oldest_xy(maximum_num_points, &mut x, &mut y);
4747 line.update_data(x, y)
4748 .map_err(|err| format!("failed to update animated line: {err}"))?;
4749 }
4750 PlotElement::Line3(line) => {
4751 let mut x = std::mem::take(&mut line.x_data);
4752 let mut y = std::mem::take(&mut line.y_data);
4753 let mut z = std::mem::take(&mut line.z_data);
4754 trim_oldest_xyz(maximum_num_points, &mut x, &mut y, &mut z);
4755 line.update_data(x, y, z)
4756 .map_err(|err| format!("failed to update animated line: {err}"))?;
4757 }
4758 _ => return Err("invalid animated line handle".to_string()),
4759 }
4760 state.revision = state.revision.wrapping_add(1);
4761 state.figure.clone()
4762 };
4763
4764 for child in reg.plot_children.values_mut() {
4765 if let PlotChildHandleState::AnimatedLine(animated) = child {
4766 if animated.figure == handle.figure && animated.plot_index == handle.plot_index {
4767 animated.maximum_num_points = maximum_num_points;
4768 }
4769 }
4770 }
4771 figure_clone
4772 };
4773 notify_with_figure(handle.figure, &figure_clone, FigureEventKind::Updated);
4774 Ok(())
4775}
4776
4777fn trim_oldest_xy(maximum: Option<usize>, x: &mut Vec<f64>, y: &mut Vec<f64>) {
4778 let Some(maximum) = maximum else {
4779 return;
4780 };
4781 if x.len() <= maximum {
4782 return;
4783 }
4784 let drop = x.len() - maximum;
4785 x.drain(0..drop);
4786 y.drain(0..drop);
4787}
4788
4789fn trim_oldest_xyz(maximum: Option<usize>, x: &mut Vec<f64>, y: &mut Vec<f64>, z: &mut Vec<f64>) {
4790 let Some(maximum) = maximum else {
4791 return;
4792 };
4793 if x.len() <= maximum {
4794 return;
4795 }
4796 let drop = x.len() - maximum;
4797 x.drain(0..drop);
4798 y.drain(0..drop);
4799 z.drain(0..drop);
4800}
4801
4802fn purge_plot_children_for_figure(reg: &mut PlotRegistry, handle: FigureHandle) {
4803 reg.plot_children.retain(|_, state| match state {
4804 PlotChildHandleState::Histogram(hist) => hist.figure != handle,
4805 PlotChildHandleState::Histogram2(hist) => hist.figure != handle,
4806 PlotChildHandleState::Line(plot)
4807 | PlotChildHandleState::Scatter(plot)
4808 | PlotChildHandleState::Bar(plot)
4809 | PlotChildHandleState::Stairs(plot)
4810 | PlotChildHandleState::Surface(plot)
4811 | PlotChildHandleState::Patch(plot)
4812 | PlotChildHandleState::Line3(plot)
4813 | PlotChildHandleState::Scatter3(plot)
4814 | PlotChildHandleState::Contour(plot)
4815 | PlotChildHandleState::ContourFill(plot)
4816 | PlotChildHandleState::ReferenceLine(plot)
4817 | PlotChildHandleState::Pie(plot) => plot.figure != handle,
4818 PlotChildHandleState::AnimatedLine(animated) => animated.figure != handle,
4819 PlotChildHandleState::Stem(stem) => stem.figure != handle,
4820 PlotChildHandleState::ErrorBar(err) => err.figure != handle,
4821 PlotChildHandleState::Quiver(quiver) => quiver.figure != handle,
4822 PlotChildHandleState::Image(image) => image.figure != handle,
4823 PlotChildHandleState::Heatmap(heatmap) => heatmap.figure != handle,
4824 PlotChildHandleState::Binscatter(binscatter) => binscatter.figure != handle,
4825 PlotChildHandleState::FunctionSurface(function_surface) => {
4826 function_surface.figure != handle
4827 }
4828 PlotChildHandleState::FunctionContour(function_contour) => {
4829 function_contour.figure != handle
4830 }
4831 PlotChildHandleState::Area(area) => area.figure != handle,
4832 PlotChildHandleState::Text(text) => text.figure != handle,
4833 PlotChildHandleState::TextScatter(textscatter) => textscatter.figure != handle,
4834 PlotChildHandleState::WordCloud(wordcloud) => wordcloud.figure != handle,
4835 PlotChildHandleState::StackedPlot(stacked) => stacked.figure != handle,
4836 });
4837}
4838
4839fn purge_plot_children_for_axes(reg: &mut PlotRegistry, handle: FigureHandle, axes_index: usize) {
4840 reg.plot_children.retain(|_, state| match state {
4841 PlotChildHandleState::Histogram(hist) => {
4842 !(hist.figure == handle && hist.axes_index == axes_index)
4843 }
4844 PlotChildHandleState::Histogram2(hist) => {
4845 !(hist.figure == handle && hist.axes_index == axes_index)
4846 }
4847 PlotChildHandleState::Line(plot)
4848 | PlotChildHandleState::Scatter(plot)
4849 | PlotChildHandleState::Bar(plot)
4850 | PlotChildHandleState::Stairs(plot)
4851 | PlotChildHandleState::Surface(plot)
4852 | PlotChildHandleState::Patch(plot)
4853 | PlotChildHandleState::Line3(plot)
4854 | PlotChildHandleState::Scatter3(plot)
4855 | PlotChildHandleState::Contour(plot)
4856 | PlotChildHandleState::ContourFill(plot)
4857 | PlotChildHandleState::ReferenceLine(plot)
4858 | PlotChildHandleState::Pie(plot) => {
4859 !(plot.figure == handle && plot.axes_index == axes_index)
4860 }
4861 PlotChildHandleState::AnimatedLine(animated) => {
4862 !(animated.figure == handle && animated.axes_index == axes_index)
4863 }
4864 PlotChildHandleState::Stem(stem) => {
4865 !(stem.figure == handle && stem.axes_index == axes_index)
4866 }
4867 PlotChildHandleState::ErrorBar(err) => {
4868 !(err.figure == handle && err.axes_index == axes_index)
4869 }
4870 PlotChildHandleState::Quiver(quiver) => {
4871 !(quiver.figure == handle && quiver.axes_index == axes_index)
4872 }
4873 PlotChildHandleState::Image(image) => {
4874 !(image.figure == handle && image.axes_index == axes_index)
4875 }
4876 PlotChildHandleState::Heatmap(heatmap) => {
4877 !(heatmap.figure == handle && heatmap.axes_index == axes_index)
4878 }
4879 PlotChildHandleState::Binscatter(binscatter) => {
4880 !(binscatter.figure == handle && binscatter.axes_index == axes_index)
4881 }
4882 PlotChildHandleState::FunctionSurface(function_surface) => {
4883 !(function_surface.figure == handle && function_surface.axes_index == axes_index)
4884 }
4885 PlotChildHandleState::FunctionContour(function_contour) => {
4886 !(function_contour.figure == handle && function_contour.axes_index == axes_index)
4887 }
4888 PlotChildHandleState::Area(area) => {
4889 !(area.figure == handle && area.axes_index == axes_index)
4890 }
4891 PlotChildHandleState::Text(text) => {
4892 !(text.figure == handle && text.axes_index == axes_index)
4893 }
4894 PlotChildHandleState::TextScatter(textscatter) => {
4895 !(textscatter.figure == handle && textscatter.axes_index == axes_index)
4896 }
4897 PlotChildHandleState::WordCloud(wordcloud) => {
4898 !(wordcloud.figure == handle && wordcloud.axes_index == axes_index)
4899 }
4900 PlotChildHandleState::StackedPlot(stacked) => {
4901 !(stacked.figure == handle && stacked.axes_indices.contains(&axes_index))
4902 }
4903 });
4904}
4905
4906pub fn decode_axes_handle(value: f64) -> Result<(FigureHandle, usize), FigureError> {
4907 if !value.is_finite() || value <= 0.0 {
4908 return Err(FigureError::InvalidAxesHandle);
4909 }
4910 let encoded = value.round() as u64;
4911 let figure_id = encoded >> AXES_INDEX_BITS;
4912 if figure_id == 0 {
4913 return Err(FigureError::InvalidAxesHandle);
4914 }
4915 let axes_index = (encoded & AXES_INDEX_MASK) as usize;
4916 Ok((FigureHandle::from(figure_id as u32), axes_index))
4917}
4918
4919#[cfg(not(target_arch = "wasm32"))]
4920fn registry() -> PlotRegistryGuard<'static> {
4921 #[cfg(test)]
4922 let test_lock = TEST_PLOT_OUTER_LOCK_HELD.with(|flag| {
4923 if flag.get() {
4924 None
4925 } else {
4926 Some(
4927 TEST_PLOT_REGISTRY_LOCK
4928 .lock()
4929 .unwrap_or_else(|e| e.into_inner()),
4930 )
4931 }
4932 });
4933 let guard = REGISTRY
4934 .get_or_init(|| Mutex::new(PlotRegistry::default()))
4935 .lock()
4936 .expect("plot registry poisoned");
4937 #[cfg(test)]
4938 {
4939 PlotRegistryGuard::new(guard, test_lock)
4940 }
4941 #[cfg(not(test))]
4942 {
4943 PlotRegistryGuard::new(guard)
4944 }
4945}
4946
4947#[cfg(target_arch = "wasm32")]
4948fn registry() -> PlotRegistryGuard<'static> {
4949 REGISTRY.with(|cell| {
4950 let guard = cell.borrow_mut();
4951 let guard_static: std::cell::RefMut<'static, PlotRegistry> =
4955 unsafe { std::mem::transmute::<std::cell::RefMut<'_, PlotRegistry>, _>(guard) };
4956 #[cfg(test)]
4957 {
4958 let test_lock = TEST_PLOT_OUTER_LOCK_HELD.with(|flag| {
4959 if flag.get() {
4960 None
4961 } else {
4962 Some(
4963 TEST_PLOT_REGISTRY_LOCK
4964 .lock()
4965 .unwrap_or_else(|e| e.into_inner()),
4966 )
4967 }
4968 });
4969 PlotRegistryGuard::new(guard_static, test_lock)
4970 }
4971 #[cfg(not(test))]
4972 {
4973 PlotRegistryGuard::new(guard_static)
4974 }
4975 })
4976}
4977
4978fn get_state_mut(registry: &mut PlotRegistry, handle: FigureHandle) -> &mut FigureState {
4979 registry
4980 .figures
4981 .entry(handle)
4982 .or_insert_with(|| FigureState::new(handle))
4983}
4984
4985fn observer_registry() -> &'static FigureObserverRegistry {
4986 FIGURE_OBSERVERS.get_or_init(FigureObserverRegistry::new)
4987}
4988
4989pub fn install_figure_observer(observer: Arc<FigureObserver>) -> BuiltinResult<()> {
4990 observer_registry().install(observer);
4991 Ok(())
4992}
4993
4994fn notify_event<'a>(view: FigureEventView<'a>) {
4995 note_recent_figure(view.handle);
4996 if let Some(registry) = FIGURE_OBSERVERS.get() {
4997 if registry.is_empty() {
4998 return;
4999 }
5000 registry.notify(view);
5001 }
5002}
5003
5004fn notify_with_figure(handle: FigureHandle, figure: &Figure, kind: FigureEventKind) {
5005 notify_event(FigureEventView {
5006 handle,
5007 kind,
5008 revision: current_figure_revision(handle),
5009 figure: Some(figure),
5010 });
5011}
5012
5013fn notify_without_figure(handle: FigureHandle, kind: FigureEventKind) {
5014 notify_event(FigureEventView {
5015 handle,
5016 kind,
5017 revision: current_figure_revision(handle),
5018 figure: None,
5019 });
5020}
5021
5022fn note_recent_figure(handle: FigureHandle) {
5023 RECENT_FIGURES.with(|set| {
5024 set.borrow_mut().insert(handle);
5025 });
5026}
5027
5028pub fn record_recent_figure(handle: FigureHandle) {
5029 note_recent_figure(handle);
5030}
5031
5032pub fn reset_recent_figures() {
5033 RECENT_FIGURES.with(|set| set.borrow_mut().clear());
5034}
5035
5036pub fn reset_plot_state() {
5037 {
5038 let mut reg = registry();
5039 *reg = PlotRegistry::default();
5040 }
5041 reset_recent_figures();
5042}
5043
5044pub fn take_recent_figures() -> Vec<FigureHandle> {
5045 RECENT_FIGURES.with(|set| set.borrow_mut().drain().collect())
5046}
5047
5048pub fn select_figure(handle: FigureHandle) {
5049 let mut reg = registry();
5050 reg.current = handle;
5051 let maybe_new = match reg.figures.entry(handle) {
5052 Entry::Occupied(entry) => {
5053 let _ = entry.into_mut();
5054 None
5055 }
5056 Entry::Vacant(vacant) => {
5057 let state = vacant.insert(FigureState::new(handle));
5058 Some(state.figure.clone())
5059 }
5060 };
5061 drop(reg);
5062 if let Some(figure_clone) = maybe_new {
5063 notify_with_figure(handle, &figure_clone, FigureEventKind::Created);
5064 }
5065}
5066
5067pub fn new_figure_handle() -> FigureHandle {
5068 let mut reg = registry();
5069 let handle = reg.next_handle;
5070 reg.next_handle = reg.next_handle.next();
5071 reg.current = handle;
5072 let figure_clone = {
5073 let state = get_state_mut(&mut reg, handle);
5074 state.figure.clone()
5075 };
5076 drop(reg);
5077 notify_with_figure(handle, &figure_clone, FigureEventKind::Created);
5078 handle
5079}
5080
5081pub fn current_figure_handle() -> FigureHandle {
5082 registry().current
5083}
5084
5085pub fn current_figure_handle_if_exists() -> Option<FigureHandle> {
5086 let reg = registry();
5087 if reg.figures.contains_key(®.current) {
5088 Some(reg.current)
5089 } else {
5090 None
5091 }
5092}
5093
5094pub fn select_current_figure_if_exists(handle: FigureHandle) -> Result<(), FigureError> {
5095 let mut reg = registry();
5096 if !reg.figures.contains_key(&handle) {
5097 return Err(FigureError::InvalidHandle(handle.as_u32()));
5098 }
5099 reg.current = handle;
5100 Ok(())
5101}
5102
5103pub fn current_axes_state() -> FigureAxesState {
5104 let mut reg = registry();
5105 let handle = reg.current;
5106 let state = get_state_mut(&mut reg, handle);
5108 FigureAxesState {
5109 handle,
5110 rows: state.figure.axes_rows.max(1),
5111 cols: state.figure.axes_cols.max(1),
5112 active_index: state.active_axes,
5113 }
5114}
5115
5116pub fn axes_handle_exists(handle: FigureHandle, axes_index: usize) -> bool {
5117 let reg = registry();
5118 reg.figures
5119 .get(&handle)
5120 .map(|state| axes_index < axes_count(state))
5121 .unwrap_or(false)
5122}
5123
5124pub fn figure_handle_exists(handle: FigureHandle) -> bool {
5125 let reg = registry();
5126 reg.figures.contains_key(&handle)
5127}
5128
5129pub fn axes_metadata_snapshot(
5130 handle: FigureHandle,
5131 axes_index: usize,
5132) -> Result<runmat_plot::plots::AxesMetadata, FigureError> {
5133 let mut reg = registry();
5134 let state = get_state_mut(&mut reg, handle);
5135 let total_axes = axes_count(state);
5136 if axes_index >= total_axes {
5137 return Err(FigureError::InvalidSubplotIndex {
5138 rows: state.figure.axes_rows.max(1),
5139 cols: state.figure.axes_cols.max(1),
5140 index: axes_index,
5141 });
5142 }
5143 state
5144 .figure
5145 .axes_metadata(axes_index)
5146 .cloned()
5147 .ok_or(FigureError::InvalidAxesHandle)
5148}
5149
5150pub fn axes_state_snapshot(
5151 handle: FigureHandle,
5152 axes_index: usize,
5153) -> Result<FigureAxesState, FigureError> {
5154 let mut reg = registry();
5155 let state = get_state_mut(&mut reg, handle);
5156 let total_axes = axes_count(state);
5157 if axes_index >= total_axes {
5158 return Err(FigureError::InvalidSubplotIndex {
5159 rows: state.figure.axes_rows.max(1),
5160 cols: state.figure.axes_cols.max(1),
5161 index: axes_index,
5162 });
5163 }
5164 Ok(FigureAxesState {
5165 handle,
5166 rows: state.figure.axes_rows.max(1),
5167 cols: state.figure.axes_cols.max(1),
5168 active_index: axes_index,
5169 })
5170}
5171
5172pub fn current_axes_handle_for_figure(handle: FigureHandle) -> Result<f64, FigureError> {
5173 let mut reg = registry();
5174 let state = get_state_mut(&mut reg, handle);
5175 Ok(encode_axes_handle(handle, state.active_axes))
5176}
5177
5178pub fn axes_handles_for_figure(handle: FigureHandle) -> Result<Vec<f64>, FigureError> {
5179 let mut reg = registry();
5180 let state = get_state_mut(&mut reg, handle);
5181 let total_axes = axes_count(state);
5182 Ok((0..total_axes)
5183 .map(|idx| encode_axes_handle(handle, idx))
5184 .collect())
5185}
5186
5187pub fn plot_child_handles_for_axes(handle: FigureHandle, axes_index: usize) -> Vec<f64> {
5188 let mut handles = registry()
5189 .plot_children
5190 .iter()
5191 .filter_map(|(id, state)| {
5192 let (figure, axes) = state.figure_axes();
5193 (figure == handle && axes == axes_index).then_some(*id as f64)
5194 })
5195 .collect::<Vec<_>>();
5196 handles.sort_by(|left, right| left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal));
5197 handles
5198}
5199
5200pub fn select_axes_for_figure(handle: FigureHandle, axes_index: usize) -> Result<(), FigureError> {
5201 let mut reg = registry();
5202 let state = get_state_mut(&mut reg, handle);
5203 let total_axes = axes_count(state);
5204 if axes_index >= total_axes {
5205 return Err(FigureError::InvalidSubplotIndex {
5206 rows: state.figure.axes_rows.max(1),
5207 cols: state.figure.axes_cols.max(1),
5208 index: axes_index,
5209 });
5210 }
5211 reg.current = handle;
5212 let state = get_state_mut(&mut reg, handle);
5213 state.active_axes = axes_index;
5214 state.figure.set_active_axes_index(axes_index);
5215 Ok(())
5216}
5217
5218pub fn create_axes_for_figure(
5219 target: Option<FigureHandle>,
5220) -> Result<(FigureHandle, usize), FigureError> {
5221 let mut reg = registry();
5222 let handle = target.unwrap_or(reg.current);
5223 let axes_index = {
5224 let state = get_state_mut(&mut reg, handle);
5225 let axes_index = axes_count(state);
5226 state.figure.ensure_axes(axes_index);
5227 state.figure.set_active_axes_index(axes_index);
5228 state.active_axes = axes_index;
5229 state.reset_cycle(axes_index);
5230 axes_index
5231 };
5232 reg.current = handle;
5233 Ok((handle, axes_index))
5234}
5235
5236fn with_axes_target_mut<R>(
5237 handle: FigureHandle,
5238 axes_index: usize,
5239 f: impl FnOnce(&mut FigureState) -> R,
5240) -> Result<(R, Figure), FigureError> {
5241 let mut reg = registry();
5242 let state = get_state_mut(&mut reg, handle);
5243 let total_axes = axes_count(state);
5244 if axes_index >= total_axes {
5245 return Err(FigureError::InvalidSubplotIndex {
5246 rows: state.figure.axes_rows.max(1),
5247 cols: state.figure.axes_cols.max(1),
5248 index: axes_index,
5249 });
5250 }
5251 state.active_axes = axes_index;
5252 state.figure.set_active_axes_index(axes_index);
5253 let result = f(state);
5254 state.revision = state.revision.wrapping_add(1);
5255 Ok((result, state.figure.clone()))
5256}
5257
5258fn with_figure_mut<R>(
5259 handle: FigureHandle,
5260 f: impl FnOnce(&mut FigureState) -> R,
5261) -> Result<(R, Figure), FigureError> {
5262 let mut reg = registry();
5263 let state = get_state_mut(&mut reg, handle);
5264 let result = f(state);
5265 state.revision = state.revision.wrapping_add(1);
5266 Ok((result, state.figure.clone()))
5267}
5268
5269pub fn current_hold_enabled() -> bool {
5270 let mut reg = registry();
5271 let handle = reg.current;
5272 let state = get_state_mut(&mut reg, handle);
5274 *state
5275 .hold_per_axes
5276 .get(&state.active_axes)
5277 .unwrap_or(&false)
5278}
5279
5280pub fn reset_hold_state_for_run() {
5286 let mut reg = registry();
5287 for state in reg.figures.values_mut() {
5288 state.hold_per_axes.clear();
5289 }
5290}
5291
5292pub fn figure_handles() -> Vec<FigureHandle> {
5293 let reg = registry();
5294 reg.figures.keys().copied().collect()
5295}
5296
5297pub fn root_figure_handles() -> Vec<FigureHandle> {
5298 let mut handles = figure_handles();
5299 handles.sort_by_key(|handle| handle.as_u32());
5300 handles
5301}
5302
5303pub fn root_default_properties() -> Vec<(String, RootPropertyValue)> {
5304 let reg = registry();
5305 let mut properties: Vec<_> = reg
5306 .root_defaults
5307 .iter()
5308 .map(|(key, entry)| (key.clone(), entry.display_name.clone(), entry.value.clone()))
5309 .collect();
5310 properties.sort_by(|(left, _, _), (right, _, _)| left.cmp(right));
5311 properties
5312 .into_iter()
5313 .map(|(_, display_name, value)| (display_name, value))
5314 .collect()
5315}
5316
5317pub fn root_default_property(name: &str) -> Option<RootPropertyValue> {
5318 registry()
5319 .root_defaults
5320 .get(name)
5321 .map(|entry| entry.value.clone())
5322}
5323
5324pub fn set_root_default_property(name: String, display_name: String, value: RootPropertyValue) {
5325 registry().root_defaults.insert(
5326 name,
5327 RootPropertyEntry {
5328 display_name,
5329 value,
5330 },
5331 );
5332}
5333
5334pub fn root_units() -> String {
5335 registry().root_units.clone()
5336}
5337
5338pub fn set_root_units(units: String) {
5339 registry().root_units = units;
5340}
5341
5342pub fn root_show_hidden_handles() -> bool {
5343 registry().root_show_hidden_handles
5344}
5345
5346pub fn set_root_show_hidden_handles(enabled: bool) {
5347 registry().root_show_hidden_handles = enabled;
5348}
5349
5350pub fn clone_figure(handle: FigureHandle) -> Option<Figure> {
5351 let reg = registry();
5352 reg.figures.get(&handle).map(|state| state.figure.clone())
5353}
5354
5355pub fn figure_tag(handle: FigureHandle) -> Option<String> {
5356 let reg = registry();
5357 reg.figures.get(&handle).map(|state| state.tag.clone())
5358}
5359
5360pub fn figure_has_sg_title(handle: FigureHandle) -> bool {
5361 let reg = registry();
5362 reg.figures
5363 .get(&handle)
5364 .map(|state| state.figure.sg_title.is_some())
5365 .unwrap_or(false)
5366}
5367
5368pub fn import_figure(figure: Figure) -> FigureHandle {
5369 let mut reg = registry();
5370 let handle = reg.next_handle;
5371 reg.next_handle = reg.next_handle.next();
5372 reg.current = handle;
5373 let figure_clone = figure.clone();
5374 reg.figures.insert(
5375 handle,
5376 FigureState {
5377 figure,
5378 ..FigureState::new(handle)
5379 },
5380 );
5381 drop(reg);
5382 notify_with_figure(handle, &figure_clone, FigureEventKind::Created);
5383 handle
5384}
5385
5386pub fn clear_figure(target: Option<FigureHandle>) -> Result<FigureHandle, FigureError> {
5387 let mut reg = registry();
5388 let handle = target.unwrap_or(reg.current);
5389 {
5390 let state = reg
5391 .figures
5392 .get_mut(&handle)
5393 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
5394 *state = FigureState::new(handle);
5395 }
5396 purge_link_axes_for_figure(&mut reg, handle);
5397 purge_plot_children_for_figure(&mut reg, handle);
5398 let figure_clone = reg
5399 .figures
5400 .get(&handle)
5401 .expect("figure exists")
5402 .figure
5403 .clone();
5404 drop(reg);
5405 notify_with_figure(handle, &figure_clone, FigureEventKind::Cleared);
5406 Ok(handle)
5407}
5408
5409pub fn close_figure(target: Option<FigureHandle>) -> Result<FigureHandle, FigureError> {
5410 let mut reg = registry();
5411 let handle = target.unwrap_or(reg.current);
5412 let existed = reg.figures.remove(&handle);
5413 if existed.is_none() {
5414 return Err(FigureError::InvalidHandle(handle.as_u32()));
5415 }
5416 purge_link_axes_for_figure(&mut reg, handle);
5417 purge_plot_children_for_figure(&mut reg, handle);
5418
5419 if reg.current == handle {
5420 if let Some((&next_handle, _)) = reg.figures.iter().next() {
5421 reg.current = next_handle;
5422 } else {
5423 let default = FigureHandle::default();
5424 reg.current = default;
5425 reg.next_handle = default.next();
5426 drop(reg);
5427 notify_without_figure(handle, FigureEventKind::Closed);
5428 return Ok(handle);
5429 }
5430 }
5431
5432 drop(reg);
5433 notify_without_figure(handle, FigureEventKind::Closed);
5434 Ok(handle)
5435}
5436
5437#[derive(Clone)]
5438pub struct PlotRenderOptions<'a> {
5439 pub title: &'a str,
5440 pub x_label: &'a str,
5441 pub y_label: &'a str,
5442 pub grid: bool,
5443 pub axis_equal: bool,
5444}
5445
5446impl<'a> Default for PlotRenderOptions<'a> {
5447 fn default() -> Self {
5448 Self {
5449 title: "",
5450 x_label: "X",
5451 y_label: "Y",
5452 grid: true,
5453 axis_equal: false,
5454 }
5455 }
5456}
5457
5458pub enum HoldMode {
5459 On,
5460 Off,
5461 Toggle,
5462}
5463
5464pub fn set_hold(mode: HoldMode) -> bool {
5465 let mut reg = registry();
5466 let handle = reg.current;
5467 let state = get_state_mut(&mut reg, handle);
5468 let current = state.hold();
5469 let new_value = match mode {
5470 HoldMode::On => true,
5471 HoldMode::Off => false,
5472 HoldMode::Toggle => !current,
5473 };
5474 state.set_hold(new_value);
5475 new_value
5476}
5477
5478pub fn configure_subplot(rows: usize, cols: usize, index: usize) -> Result<(), FigureError> {
5479 if rows == 0 || cols == 0 {
5480 return Err(FigureError::InvalidSubplotGrid { rows, cols });
5481 }
5482 let total_axes = rows
5483 .checked_mul(cols)
5484 .ok_or(FigureError::InvalidSubplotGrid { rows, cols })?;
5485 if index >= total_axes {
5486 return Err(FigureError::InvalidSubplotIndex { rows, cols, index });
5487 }
5488 let mut reg = registry();
5489 let handle = reg.current;
5490 let state = get_state_mut(&mut reg, handle);
5491 state.figure.set_subplot_grid(rows, cols);
5492 state.active_axes = index;
5493 state.figure.set_active_axes_index(index);
5494 Ok(())
5495}
5496
5497pub fn prepare_plotyy_axes() -> Result<(FigureHandle, usize, usize, f64, f64), FigureError> {
5498 let mut reg = registry();
5499 let handle = reg.current;
5500 let (left_axes, right_axes) = {
5501 let state = get_state_mut(&mut reg, handle);
5502 let left_axes = state.active_axes;
5503 let right_axes = state.figure.ensure_overlay_axes(left_axes);
5504 state.figure.clear_axes(left_axes);
5505 state.figure.clear_axes(right_axes);
5506 state.figure.set_axes_kind(left_axes, AxesKind::Cartesian);
5507 state.figure.set_axes_kind(right_axes, AxesKind::Cartesian);
5508 state.figure.set_axes_limits(left_axes, None, None);
5509 state.figure.set_axes_limits(right_axes, None, None);
5510 state.figure.set_axes_z_limits(left_axes, None);
5511 state.figure.set_axes_z_limits(right_axes, None);
5512 state.figure.set_axes_y_axis_location(left_axes, "left");
5513 state.figure.set_axes_y_axis_location(right_axes, "right");
5514 state.figure.set_axes_grid_enabled(right_axes, false);
5515 state.figure.set_axes_minor_grid_enabled(right_axes, false);
5516 state.figure.set_axes_legend_enabled(right_axes, false);
5517 state.reset_cycle(left_axes);
5518 state.reset_cycle(right_axes);
5519 state.active_axes = left_axes;
5520 state.figure.set_active_axes_index(left_axes);
5521 (left_axes, right_axes)
5522 };
5523 purge_plot_children_for_axes(&mut reg, handle, left_axes);
5524 purge_plot_children_for_axes(&mut reg, handle, right_axes);
5525 Ok((
5526 handle,
5527 left_axes,
5528 right_axes,
5529 encode_axes_handle(handle, left_axes),
5530 encode_axes_handle(handle, right_axes),
5531 ))
5532}
5533
5534pub fn render_active_plot<F>(
5535 builtin: &'static str,
5536 opts: PlotRenderOptions<'_>,
5537 mut apply: F,
5538) -> BuiltinResult<String>
5539where
5540 F: FnMut(&mut Figure, usize) -> BuiltinResult<()>,
5541{
5542 let rendering_disabled = interactive_rendering_disabled();
5543 let host_managed_rendering = host_managed_rendering_enabled();
5544 let (handle, figure_clone) = {
5545 let mut reg = registry();
5546 let handle = reg.current;
5547 let axes_index = { get_state_mut(&mut reg, handle).active_axes };
5548 let should_clear = { !get_state_mut(&mut reg, handle).hold() };
5549 {
5550 let state = get_state_mut(&mut reg, handle);
5551 state.figure.set_active_axes_index(axes_index);
5552 if should_clear {
5553 state.figure.clear_axes(axes_index);
5554 state.figure.set_axes_kind(axes_index, AxesKind::Cartesian);
5555 state.figure.set_axes_limits(axes_index, None, None);
5556 state.figure.set_axes_z_limits(axes_index, None);
5557 state.reset_cycle(axes_index);
5558 }
5559 }
5560 if should_clear {
5561 purge_plot_children_for_axes(&mut reg, handle, axes_index);
5562 }
5563 {
5564 let state = get_state_mut(&mut reg, handle);
5565 if !opts.title.is_empty() {
5566 state.figure.set_axes_title(axes_index, opts.title);
5567 }
5568 if !opts.x_label.is_empty() || !opts.y_label.is_empty() {
5569 state
5570 .figure
5571 .set_axes_labels(axes_index, opts.x_label, opts.y_label);
5572 }
5573 state.figure.set_grid(opts.grid);
5574 state.figure.set_axis_equal(opts.axis_equal);
5575
5576 let _axes_context = AxesContextGuard::install(state, axes_index);
5577 apply(&mut state.figure, axes_index)
5578 .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
5579
5580 state.revision = state.revision.wrapping_add(1);
5583 }
5584 let figure_clone = reg
5585 .figures
5586 .get(&handle)
5587 .expect("figure exists")
5588 .figure
5589 .clone();
5590 (handle, figure_clone)
5591 };
5592 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
5593
5594 present_figure_update_with_options(
5595 builtin,
5596 handle,
5597 figure_clone,
5598 rendering_disabled,
5599 host_managed_rendering,
5600 )
5601}
5602
5603pub fn append_active_plot<F>(
5604 builtin: &'static str,
5605 opts: PlotRenderOptions<'_>,
5606 mut apply: F,
5607) -> BuiltinResult<String>
5608where
5609 F: FnMut(&mut Figure, usize) -> BuiltinResult<()>,
5610{
5611 let rendering_disabled = interactive_rendering_disabled();
5612 let host_managed_rendering = host_managed_rendering_enabled();
5613 let (handle, figure_clone) = {
5614 let mut reg = registry();
5615 let handle = reg.current;
5616 let axes_index = { get_state_mut(&mut reg, handle).active_axes };
5617 {
5618 let state = get_state_mut(&mut reg, handle);
5619 state.figure.set_active_axes_index(axes_index);
5620 if !opts.title.is_empty() {
5621 state.figure.set_axes_title(axes_index, opts.title);
5622 }
5623 if !opts.x_label.is_empty() || !opts.y_label.is_empty() {
5624 state
5625 .figure
5626 .set_axes_labels(axes_index, opts.x_label, opts.y_label);
5627 }
5628 state.figure.set_grid(opts.grid);
5629 state.figure.set_axis_equal(opts.axis_equal);
5630
5631 let _axes_context = AxesContextGuard::install(state, axes_index);
5632 apply(&mut state.figure, axes_index)
5633 .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
5634 state.revision = state.revision.wrapping_add(1);
5635 }
5636 let figure_clone = reg
5637 .figures
5638 .get(&handle)
5639 .expect("figure exists")
5640 .figure
5641 .clone();
5642 (handle, figure_clone)
5643 };
5644 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
5645
5646 present_figure_update_with_options(
5647 builtin,
5648 handle,
5649 figure_clone,
5650 rendering_disabled,
5651 host_managed_rendering,
5652 )
5653}
5654
5655pub fn present_figure_update(
5656 builtin: &'static str,
5657 handle: FigureHandle,
5658 figure_clone: Figure,
5659) -> BuiltinResult<String> {
5660 present_figure_update_with_options(
5661 builtin,
5662 handle,
5663 figure_clone,
5664 interactive_rendering_disabled(),
5665 host_managed_rendering_enabled(),
5666 )
5667}
5668
5669fn present_figure_update_with_options(
5670 builtin: &'static str,
5671 handle: FigureHandle,
5672 figure_clone: Figure,
5673 rendering_disabled: bool,
5674 host_managed_rendering: bool,
5675) -> BuiltinResult<String> {
5676 let updated = || format!("Figure {} updated", handle.as_u32());
5677 if !figure_clone.visible {
5678 #[cfg(all(
5679 feature = "gui",
5680 not(all(target_arch = "wasm32", feature = "plot-web"))
5681 ))]
5682 {
5683 if !rendering_disabled && !host_managed_rendering {
5684 let rendered = render_figure(handle, figure_clone)
5685 .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
5686 return Ok(format!("{}: {rendered}", updated()));
5687 }
5688 }
5689 return Ok(updated());
5690 }
5691
5692 if rendering_disabled {
5693 if host_managed_rendering {
5694 return Ok(updated());
5695 }
5696 return Err(plotting_error(builtin, ERR_PLOTTING_UNAVAILABLE));
5697 }
5698
5699 if host_managed_rendering {
5700 return Ok(updated());
5701 }
5702
5703 #[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
5707 {
5708 let _ = figure_clone;
5709 Ok(updated())
5710 }
5711
5712 #[cfg(not(all(target_arch = "wasm32", feature = "plot-web")))]
5713 {
5714 let rendered = render_figure(handle, figure_clone)
5715 .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
5716 Ok(format!("{}: {rendered}", updated()))
5717 }
5718}
5719
5720pub fn current_figure_revision(handle: FigureHandle) -> Option<u64> {
5724 let reg = registry();
5725 reg.figures.get(&handle).map(|state| state.revision)
5726}
5727
5728fn interactive_rendering_disabled() -> bool {
5729 std::env::var_os("RUNMAT_DISABLE_INTERACTIVE_PLOTS").is_some()
5730}
5731
5732fn host_managed_rendering_enabled() -> bool {
5733 std::env::var_os("RUNMAT_HOST_MANAGED_PLOTS").is_some()
5734}
5735
5736#[cfg(test)]
5737pub(crate) fn disable_rendering_for_tests() {
5738 set_plot_test_env_vars();
5739}
5740
5741pub fn set_line_style_order_for_axes(axes_index: usize, order: &[LineStyle]) {
5742 if with_active_style_cycle(axes_index, |cycle| cycle.set_order(order)).is_some() {
5743 return;
5744 }
5745 let mut reg = registry();
5746 let handle = reg.current;
5747 let state = get_state_mut(&mut reg, handle);
5748 state.cycle_for_axes_mut(axes_index).set_order(order);
5749}
5750
5751pub fn next_line_style_for_axes(axes_index: usize) -> LineStyle {
5752 if let Some(style) = with_active_style_cycle(axes_index, |cycle| cycle.next()) {
5753 return style;
5754 }
5755 let mut reg = registry();
5756 let handle = reg.current;
5757 let state = get_state_mut(&mut reg, handle);
5758 state.cycle_for_axes_mut(axes_index).next()
5759}
5760
5761pub fn line_color_for_series_index(series_index: usize) -> Vec4 {
5762 let theme = current_plot_theme_config().build_theme();
5763 theme.get_data_color(series_index)
5764}
5765
5766pub fn line_color_for_axes_series_index(axes_index: usize, series_index: usize) -> Vec4 {
5767 if let Some(color) = with_active_color_cycle(axes_index, |cycle| cycle.color_at(series_index)) {
5768 return color;
5769 }
5770 let mut reg = registry();
5771 let handle = reg.current;
5772 let state = get_state_mut(&mut reg, handle);
5773 state
5774 .color_cycle_for_axes_mut(axes_index)
5775 .color_at(series_index)
5776}
5777
5778pub fn line_color_for_target_axes_series_index(
5779 handle: FigureHandle,
5780 axes_index: usize,
5781 series_index: usize,
5782) -> Vec4 {
5783 let mut reg = registry();
5784 let state = get_state_mut(&mut reg, handle);
5785 state
5786 .color_cycle_for_axes_mut(axes_index)
5787 .color_at(series_index)
5788}
5789
5790pub fn next_line_color_for_axes(axes_index: usize) -> Vec4 {
5791 if let Some(color) = with_active_color_cycle(axes_index, |cycle| cycle.next()) {
5792 return color;
5793 }
5794 let mut reg = registry();
5795 let handle = reg.current;
5796 let state = get_state_mut(&mut reg, handle);
5797 state.color_cycle_for_axes_mut(axes_index).next()
5798}
5799
5800#[cfg(test)]
5801mod tests {
5802 use super::*;
5803 use crate::builtins::plotting::tests::ensure_plot_test_env;
5804
5805 #[cfg(test)]
5806 pub(crate) fn reset_for_tests() {
5807 let mut reg = registry();
5808 reg.figures.clear();
5809 reg.current = FigureHandle::default();
5810 reg.next_handle = FigureHandle::default().next();
5811 }
5812
5813 #[test]
5814 fn closing_last_figure_leaves_no_visible_figures() {
5815 let _guard = lock_plot_test_registry();
5816 ensure_plot_test_env();
5817 reset_for_tests();
5818
5819 let handle = new_figure_handle();
5820 assert_eq!(figure_handles(), vec![handle]);
5821
5822 close_figure(Some(handle)).expect("close figure");
5823
5824 assert!(
5825 figure_handles().is_empty(),
5826 "closing the last figure should not recreate a default visible figure"
5827 );
5828 }
5829
5830 #[test]
5831 fn hidden_figure_update_does_not_require_interactive_renderer() {
5832 let _guard = lock_plot_test_registry();
5833 ensure_plot_test_env();
5834 reset_for_tests();
5835
5836 let handle = new_figure_handle();
5837 let mut figure = clone_figure(handle).expect("figure exists");
5838 figure.set_visible(false);
5839
5840 let result = present_figure_update_with_options("plot", handle, figure, true, false)
5841 .expect("hidden figure should not try to present");
5842 assert_eq!(result, format!("Figure {} updated", handle.as_u32()));
5843 }
5844
5845 #[cfg(all(
5846 feature = "gui",
5847 not(all(target_arch = "wasm32", feature = "plot-web"))
5848 ))]
5849 #[test]
5850 fn hidden_figure_update_uses_native_close_path_when_rendering_available() {
5851 let _guard = lock_plot_test_registry();
5852 ensure_plot_test_env();
5853 reset_for_tests();
5854
5855 let handle = new_figure_handle();
5856 let mut figure = clone_figure(handle).expect("figure exists");
5857 figure.set_visible(false);
5858
5859 let result = present_figure_update_with_options("plot", handle, figure, false, false)
5860 .expect("hidden figure should request native close");
5861 assert_eq!(
5862 result,
5863 format!(
5864 "Figure {} updated: Figure {} is hidden",
5865 handle.as_u32(),
5866 handle.as_u32()
5867 )
5868 );
5869 }
5870
5871 #[test]
5872 fn hidden_figure_update_does_not_render_when_host_managed() {
5873 let _guard = lock_plot_test_registry();
5874 ensure_plot_test_env();
5875 reset_for_tests();
5876
5877 let handle = new_figure_handle();
5878 let mut figure = clone_figure(handle).expect("figure exists");
5879 figure.set_visible(false);
5880
5881 let result = present_figure_update_with_options("plot", handle, figure, false, true)
5882 .expect("hidden host-managed figure should not render directly");
5883 assert_eq!(result, format!("Figure {} updated", handle.as_u32()));
5884 }
5885
5886 #[test]
5887 fn toggle_minor_grid_uses_effective_inherited_state() {
5888 let _guard = lock_plot_test_registry();
5889 ensure_plot_test_env();
5890 reset_for_tests();
5891
5892 let handle = new_figure_handle();
5893 {
5894 let mut reg = registry();
5895 let state = get_state_mut(&mut reg, handle);
5896 state.figure.minor_grid_enabled = true;
5897 assert!(state.figure.minor_grid_enabled_for_axes(state.active_axes));
5898 assert!(
5899 !state
5900 .figure
5901 .axes_metadata(state.active_axes)
5902 .expect("active axes metadata")
5903 .minor_grid_explicit
5904 );
5905 }
5906
5907 assert!(!toggle_minor_grid());
5908
5909 let figure = clone_figure(handle).expect("figure exists");
5910 assert!(!figure.minor_grid_enabled_for_axes(0));
5911 let meta = figure.axes_metadata(0).expect("active axes metadata");
5912 assert!(meta.minor_grid_explicit);
5913 assert!(!meta.minor_grid_enabled);
5914 }
5915}