Skip to main content

runmat_runtime/builtins/plotting/core/
state.rs

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