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