Skip to main content

runmat_runtime/builtins/plotting/core/
state.rs

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        // The returned guard borrows `self`, so TEST_PLOT_REGISTRY_LOCK stays held
1154        // for the full duration of this process-env override.
1155        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(&reg.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(&reg.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(&reg.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(&reg.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        // SAFETY: the thread-local RefCell lives for the program lifetime and the borrow
5127        // guard is dropped when PlotRegistryGuard is dropped, so extending the lifetime
5128        // to 'static is sound.
5129        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(&reg.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    // Ensure a default figure exists even if nothing has rendered yet (common on wasm/web).
5282    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    // Ensure a default figure exists even if nothing has rendered yet (common on wasm/web).
5448    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
5455/// Reset hold state for all figures/axes.
5456///
5457/// In the IDE, we want re-running code to behave like a fresh plotting run unless the code
5458/// explicitly enables `hold on` again. Without this, a prior `hold on` will cause subsequent
5459/// runs to keep appending to the same axes (surprising for typical "Run" workflows).
5460pub 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            // Increment revision after a successful mutation so surfaces can avoid
5756            // re-rendering unchanged figures when "presenting" an already-loaded handle.
5757            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    // On Web/WASM we deliberately decouple "mutate figure state" from "present pixels".
5879    // The host coalesces figure events and presents on a frame cadence, and `drawnow()` /
5880    // `pause()` provide explicit "flush" boundaries for scripts.
5881    #[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
5895/// Monotonic revision counter that increments on each successful mutation of the figure.
5896/// Used by web surface presentation logic to avoid redundant `render_figure` calls when
5897/// a surface is already up-to-date for a handle.
5898pub 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}