Skip to main content

runmat_runtime/builtins/plotting/core/
state.rs

1use once_cell::sync::OnceCell;
2use runmat_builtins::Tensor;
3use runmat_plot::plots::{
4    surface::ColorMap, surface::ShadingMode, Figure, LegendStyle, LineStyle, PlotElement, TextStyle,
5};
6use runmat_thread_local::runmat_thread_local;
7use std::cell::RefCell;
8use std::collections::{hash_map::Entry, HashMap, HashSet};
9use std::ops::{Deref, DerefMut};
10#[cfg(not(target_arch = "wasm32"))]
11use std::sync::MutexGuard;
12#[cfg(test)]
13use std::sync::Once;
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::{plotting_error, plotting_error_with_source};
21
22use crate::builtins::common::map_control_flow_with_builtin;
23use crate::{BuiltinResult, RuntimeError};
24
25type AxisLimitSnapshot = (Option<(f64, f64)>, Option<(f64, f64)>);
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28pub struct FigureHandle(u32);
29
30impl FigureHandle {
31    pub fn as_u32(self) -> u32 {
32        self.0
33    }
34
35    fn next(self) -> FigureHandle {
36        FigureHandle(self.0 + 1)
37    }
38}
39
40impl From<u32> for FigureHandle {
41    fn from(value: u32) -> Self {
42        FigureHandle(value.max(1))
43    }
44}
45
46impl Default for FigureHandle {
47    fn default() -> Self {
48        FigureHandle(1)
49    }
50}
51
52const DEFAULT_LINE_STYLE_ORDER: [LineStyle; 4] = [
53    LineStyle::Solid,
54    LineStyle::Dashed,
55    LineStyle::Dotted,
56    LineStyle::DashDot,
57];
58
59#[derive(Clone)]
60struct LineStyleCycle {
61    order: Vec<LineStyle>,
62    cursor: usize,
63}
64
65impl Default for LineStyleCycle {
66    fn default() -> Self {
67        Self {
68            order: DEFAULT_LINE_STYLE_ORDER.to_vec(),
69            cursor: 0,
70        }
71    }
72}
73
74impl LineStyleCycle {
75    fn next(&mut self) -> LineStyle {
76        if self.order.is_empty() {
77            self.order = DEFAULT_LINE_STYLE_ORDER.to_vec();
78        }
79        let style = self.order[self.cursor % self.order.len()];
80        self.cursor = (self.cursor + 1) % self.order.len();
81        style
82    }
83
84    fn set_order(&mut self, order: &[LineStyle]) {
85        if order.is_empty() {
86            self.order = DEFAULT_LINE_STYLE_ORDER.to_vec();
87        } else {
88            self.order = order.to_vec();
89        }
90        self.cursor = 0;
91    }
92
93    fn reset_cursor(&mut self) {
94        self.cursor = 0;
95    }
96}
97
98#[derive(Default)]
99struct FigureState {
100    figure: Figure,
101    active_axes: usize,
102    hold_per_axes: HashMap<usize, bool>,
103    line_style_cycles: HashMap<usize, LineStyleCycle>,
104    revision: u64,
105}
106
107impl FigureState {
108    fn new(handle: FigureHandle) -> Self {
109        let title = format!("Figure {}", handle.as_u32());
110        let figure = default_figure(&title, "X", "Y");
111        Self {
112            figure,
113            active_axes: 0,
114            hold_per_axes: HashMap::new(),
115            line_style_cycles: HashMap::new(),
116            revision: 0,
117        }
118    }
119
120    fn hold(&self) -> bool {
121        *self.hold_per_axes.get(&self.active_axes).unwrap_or(&false)
122    }
123
124    fn set_hold(&mut self, hold: bool) {
125        self.hold_per_axes.insert(self.active_axes, hold);
126    }
127
128    fn cycle_for_axes_mut(&mut self, axes_index: usize) -> &mut LineStyleCycle {
129        self.line_style_cycles.entry(axes_index).or_default()
130    }
131
132    fn reset_cycle(&mut self, axes_index: usize) {
133        if let Some(cycle) = self.line_style_cycles.get_mut(&axes_index) {
134            cycle.reset_cursor();
135        }
136    }
137}
138
139struct ActiveAxesContext {
140    axes_index: usize,
141    cycle_ptr: *mut LineStyleCycle,
142}
143
144struct AxesContextGuard {
145    _private: (),
146}
147
148impl AxesContextGuard {
149    fn install(state: &mut FigureState, axes_index: usize) -> Self {
150        let cycle_ptr = state.cycle_for_axes_mut(axes_index) as *mut LineStyleCycle;
151        ACTIVE_AXES_CONTEXT.with(|ctx| {
152            debug_assert!(
153                ctx.borrow().is_none(),
154                "plot axes context already installed"
155            );
156            ctx.borrow_mut().replace(ActiveAxesContext {
157                axes_index,
158                cycle_ptr,
159            });
160        });
161        Self { _private: () }
162    }
163}
164
165impl Drop for AxesContextGuard {
166    fn drop(&mut self) {
167        ACTIVE_AXES_CONTEXT.with(|ctx| {
168            ctx.borrow_mut().take();
169        });
170    }
171}
172
173fn with_active_cycle<R>(axes_index: usize, f: impl FnOnce(&mut LineStyleCycle) -> R) -> Option<R> {
174    ACTIVE_AXES_CONTEXT.with(|ctx| {
175        let guard = ctx.borrow();
176        let active = guard.as_ref()?;
177        if active.axes_index != axes_index {
178            return None;
179        }
180        let cycle = unsafe { &mut *active.cycle_ptr };
181        Some(f(cycle))
182    })
183}
184
185struct PlotRegistry {
186    current: FigureHandle,
187    next_handle: FigureHandle,
188    figures: HashMap<FigureHandle, FigureState>,
189    next_plot_child_handle: u64,
190    plot_children: HashMap<u64, PlotChildHandleState>,
191}
192
193#[derive(Clone, Debug)]
194pub struct HistogramHandleState {
195    pub figure: FigureHandle,
196    pub axes_index: usize,
197    pub plot_index: usize,
198    pub bin_edges: Vec<f64>,
199    pub raw_counts: Vec<f64>,
200    pub normalization: String,
201}
202
203#[derive(Clone, Debug)]
204pub struct StemHandleState {
205    pub figure: FigureHandle,
206    pub axes_index: usize,
207    pub plot_index: usize,
208}
209
210#[derive(Clone, Debug)]
211pub struct SimplePlotHandleState {
212    pub figure: FigureHandle,
213    pub axes_index: usize,
214    pub plot_index: usize,
215}
216
217#[derive(Clone, Debug)]
218pub struct ErrorBarHandleState {
219    pub figure: FigureHandle,
220    pub axes_index: usize,
221    pub plot_index: usize,
222}
223
224#[derive(Clone, Debug)]
225pub struct QuiverHandleState {
226    pub figure: FigureHandle,
227    pub axes_index: usize,
228    pub plot_index: usize,
229}
230
231#[derive(Clone, Debug)]
232pub struct ImageHandleState {
233    pub figure: FigureHandle,
234    pub axes_index: usize,
235    pub plot_index: usize,
236}
237
238#[derive(Clone, Debug)]
239pub struct HeatmapHandleState {
240    pub figure: FigureHandle,
241    pub axes_index: usize,
242    pub plot_index: usize,
243    pub x_labels: Vec<String>,
244    pub y_labels: Vec<String>,
245    pub color_data: Tensor,
246}
247
248#[derive(Clone, Debug)]
249pub struct AreaHandleState {
250    pub figure: FigureHandle,
251    pub axes_index: usize,
252    pub plot_index: usize,
253}
254
255#[derive(Clone, Debug)]
256pub struct TextAnnotationHandleState {
257    pub figure: FigureHandle,
258    pub axes_index: usize,
259    pub annotation_index: usize,
260}
261
262#[derive(Clone, Debug)]
263pub enum PlotChildHandleState {
264    Histogram(HistogramHandleState),
265    Line(SimplePlotHandleState),
266    Scatter(SimplePlotHandleState),
267    Bar(SimplePlotHandleState),
268    Stem(StemHandleState),
269    ErrorBar(ErrorBarHandleState),
270    Stairs(SimplePlotHandleState),
271    Quiver(QuiverHandleState),
272    Image(ImageHandleState),
273    Heatmap(HeatmapHandleState),
274    Area(AreaHandleState),
275    Surface(SimplePlotHandleState),
276    Patch(SimplePlotHandleState),
277    Line3(SimplePlotHandleState),
278    Scatter3(SimplePlotHandleState),
279    Contour(SimplePlotHandleState),
280    ContourFill(SimplePlotHandleState),
281    ReferenceLine(SimplePlotHandleState),
282    Pie(SimplePlotHandleState),
283    Text(TextAnnotationHandleState),
284}
285
286impl Default for PlotRegistry {
287    fn default() -> Self {
288        Self {
289            current: FigureHandle::default(),
290            next_handle: FigureHandle::default().next(),
291            figures: HashMap::new(),
292            next_plot_child_handle: 1u64 << 40,
293            plot_children: HashMap::new(),
294        }
295    }
296}
297
298#[cfg(not(target_arch = "wasm32"))]
299static REGISTRY: OnceCell<Mutex<PlotRegistry>> = OnceCell::new();
300
301#[cfg(test)]
302static TEST_PLOT_REGISTRY_LOCK: Mutex<()> = Mutex::new(());
303
304#[cfg(test)]
305thread_local! {
306    static TEST_PLOT_OUTER_LOCK_HELD: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
307}
308
309#[cfg(test)]
310pub(crate) struct PlotTestLockGuard {
311    _guard: std::sync::MutexGuard<'static, ()>,
312}
313
314#[cfg(test)]
315impl Drop for PlotTestLockGuard {
316    fn drop(&mut self) {
317        TEST_PLOT_OUTER_LOCK_HELD.with(|flag| flag.set(false));
318    }
319}
320
321#[cfg(test)]
322pub(crate) fn lock_plot_test_registry() -> PlotTestLockGuard {
323    let guard = TEST_PLOT_REGISTRY_LOCK
324        .lock()
325        .unwrap_or_else(|e| e.into_inner());
326    TEST_PLOT_OUTER_LOCK_HELD.with(|flag| flag.set(true));
327    PlotTestLockGuard { _guard: guard }
328}
329
330#[cfg(target_arch = "wasm32")]
331runmat_thread_local! {
332    static REGISTRY: RefCell<PlotRegistry> = RefCell::new(PlotRegistry::default());
333}
334
335#[cfg(not(target_arch = "wasm32"))]
336type RegistryBackendGuard<'a> = MutexGuard<'a, PlotRegistry>;
337#[cfg(target_arch = "wasm32")]
338type RegistryBackendGuard<'a> = std::cell::RefMut<'a, PlotRegistry>;
339
340struct PlotRegistryGuard<'a> {
341    inner: RegistryBackendGuard<'a>,
342    #[cfg(test)]
343    _test_lock: Option<std::sync::MutexGuard<'static, ()>>,
344}
345
346impl<'a> PlotRegistryGuard<'a> {
347    #[cfg(test)]
348    fn new(
349        inner: RegistryBackendGuard<'a>,
350        _test_lock: Option<std::sync::MutexGuard<'static, ()>>,
351    ) -> Self {
352        Self { inner, _test_lock }
353    }
354
355    #[cfg(not(test))]
356    fn new(inner: RegistryBackendGuard<'a>) -> Self {
357        Self { inner }
358    }
359}
360
361impl<'a> Deref for PlotRegistryGuard<'a> {
362    type Target = PlotRegistry;
363
364    fn deref(&self) -> &Self::Target {
365        &self.inner
366    }
367}
368
369impl<'a> DerefMut for PlotRegistryGuard<'a> {
370    fn deref_mut(&mut self) -> &mut Self::Target {
371        &mut self.inner
372    }
373}
374
375const AXES_INDEX_BITS: u32 = 20;
376const AXES_INDEX_MASK: u64 = (1 << AXES_INDEX_BITS) - 1;
377const OBJECT_KIND_BITS: u32 = 4;
378const OBJECT_KIND_MASK: u64 = (1 << OBJECT_KIND_BITS) - 1;
379
380#[derive(Clone, Copy, Debug, PartialEq, Eq)]
381pub enum PlotObjectKind {
382    Title = 1,
383    XLabel = 2,
384    YLabel = 3,
385    ZLabel = 4,
386    Legend = 5,
387    SuperTitle = 6,
388}
389
390impl PlotObjectKind {
391    fn from_u64(value: u64) -> Option<Self> {
392        match value {
393            1 => Some(Self::Title),
394            2 => Some(Self::XLabel),
395            3 => Some(Self::YLabel),
396            4 => Some(Self::ZLabel),
397            5 => Some(Self::Legend),
398            6 => Some(Self::SuperTitle),
399            _ => None,
400        }
401    }
402}
403
404#[derive(Debug, Error)]
405pub enum FigureError {
406    #[error("figure handle {0} does not exist")]
407    InvalidHandle(u32),
408    #[error("subplot grid dimensions must be positive (rows={rows}, cols={cols})")]
409    InvalidSubplotGrid { rows: usize, cols: usize },
410    #[error("subplot index {index} is out of range for a {rows}x{cols} grid")]
411    InvalidSubplotIndex {
412        rows: usize,
413        cols: usize,
414        index: usize,
415    },
416    #[error("invalid axes handle")]
417    InvalidAxesHandle,
418    #[error("invalid plot object handle")]
419    InvalidPlotObjectHandle,
420    #[error("failed to render figure snapshot: {source}")]
421    RenderFailure {
422        #[source]
423        source: Box<dyn std::error::Error + Send + Sync>,
424    },
425}
426
427fn map_figure_error(builtin: &'static str, err: FigureError) -> RuntimeError {
428    let message = format!("{builtin}: {err}");
429    plotting_error_with_source(builtin, message, err)
430}
431
432pub(crate) fn clear_figure_with_builtin(
433    builtin: &'static str,
434    target: Option<FigureHandle>,
435) -> BuiltinResult<FigureHandle> {
436    clear_figure(target).map_err(|err| map_figure_error(builtin, err))
437}
438
439pub(crate) fn close_figure_with_builtin(
440    builtin: &'static str,
441    target: Option<FigureHandle>,
442) -> BuiltinResult<FigureHandle> {
443    close_figure(target).map_err(|err| map_figure_error(builtin, err))
444}
445
446pub(crate) fn configure_subplot_with_builtin(
447    builtin: &'static str,
448    rows: usize,
449    cols: usize,
450    index: usize,
451) -> BuiltinResult<()> {
452    configure_subplot(rows, cols, index).map_err(|err| map_figure_error(builtin, err))
453}
454
455pub fn set_grid_enabled(enabled: bool) {
456    let (handle, figure_clone) = {
457        let mut reg = registry();
458        let handle = reg.current;
459        let state = get_state_mut(&mut reg, handle);
460        let axes = state.active_axes;
461        state.figure.set_axes_grid_enabled(axes, enabled);
462        state.revision = state.revision.wrapping_add(1);
463        (handle, state.figure.clone())
464    };
465    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
466}
467
468pub fn set_grid_enabled_for_axes(
469    handle: FigureHandle,
470    axes_index: usize,
471    enabled: bool,
472) -> Result<(), FigureError> {
473    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
474        state.figure.set_axes_grid_enabled(axes_index, enabled);
475    })?;
476    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
477    Ok(())
478}
479
480pub fn toggle_grid() -> bool {
481    let (handle, figure_clone, enabled) = {
482        let mut reg = registry();
483        let handle = reg.current;
484        let state = get_state_mut(&mut reg, handle);
485        let axes = state.active_axes;
486        let next = !state
487            .figure
488            .axes_metadata(axes)
489            .map(|m| m.grid_enabled)
490            .unwrap_or(true);
491        state.figure.set_axes_grid_enabled(axes, next);
492        state.revision = state.revision.wrapping_add(1);
493        (handle, state.figure.clone(), next)
494    };
495    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
496    enabled
497}
498
499pub fn set_box_enabled(enabled: bool) {
500    let (handle, figure_clone) = {
501        let mut reg = registry();
502        let handle = reg.current;
503        let state = get_state_mut(&mut reg, handle);
504        let axes = state.active_axes;
505        state.figure.set_axes_box_enabled(axes, enabled);
506        state.revision = state.revision.wrapping_add(1);
507        (handle, state.figure.clone())
508    };
509    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
510}
511
512pub fn set_box_enabled_for_axes(
513    handle: FigureHandle,
514    axes_index: usize,
515    enabled: bool,
516) -> Result<(), FigureError> {
517    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
518        state.figure.set_axes_box_enabled(axes_index, enabled);
519    })?;
520    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
521    Ok(())
522}
523
524pub fn set_figure_title_for_axes(
525    handle: FigureHandle,
526    axes_index: usize,
527    title: &str,
528    style: TextStyle,
529) -> Result<f64, FigureError> {
530    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
531        state.figure.set_axes_title(axes_index, title.to_string());
532        state.figure.set_axes_title_style(axes_index, style);
533        encode_plot_object_handle(handle, axes_index, PlotObjectKind::Title)
534    })?;
535    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
536    Ok(object_handle)
537}
538
539pub fn set_sg_title_for_figure(
540    handle: FigureHandle,
541    title: &str,
542    style: TextStyle,
543) -> Result<f64, FigureError> {
544    let (object_handle, figure_clone) = with_figure_mut(handle, |state| {
545        state.figure.set_sg_title(title.to_string());
546        state.figure.set_sg_title_style(style);
547        encode_plot_object_handle(handle, 0, PlotObjectKind::SuperTitle)
548    })?;
549    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
550    Ok(object_handle)
551}
552
553pub fn set_sg_title_properties_for_figure(
554    handle: FigureHandle,
555    text: Option<String>,
556    style: Option<TextStyle>,
557) -> Result<f64, FigureError> {
558    let (object_handle, figure_clone) = with_figure_mut(handle, |state| {
559        if let Some(text) = text {
560            state.figure.set_sg_title(text);
561        }
562        if let Some(style) = style {
563            state.figure.set_sg_title_style(style);
564        }
565        encode_plot_object_handle(handle, 0, PlotObjectKind::SuperTitle)
566    })?;
567    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
568    Ok(object_handle)
569}
570
571pub fn set_text_properties_for_axes(
572    handle: FigureHandle,
573    axes_index: usize,
574    kind: PlotObjectKind,
575    text: Option<String>,
576    style: Option<TextStyle>,
577) -> Result<f64, FigureError> {
578    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
579        if let Some(text) = text {
580            match kind {
581                PlotObjectKind::Title => state.figure.set_axes_title(axes_index, text),
582                PlotObjectKind::XLabel => state.figure.set_axes_xlabel(axes_index, text),
583                PlotObjectKind::YLabel => state.figure.set_axes_ylabel(axes_index, text),
584                PlotObjectKind::ZLabel => state.figure.set_axes_zlabel(axes_index, text),
585                PlotObjectKind::Legend => {}
586                PlotObjectKind::SuperTitle => state.figure.set_sg_title(text),
587            }
588        }
589        if let Some(style) = style {
590            match kind {
591                PlotObjectKind::Title => state.figure.set_axes_title_style(axes_index, style),
592                PlotObjectKind::XLabel => state.figure.set_axes_xlabel_style(axes_index, style),
593                PlotObjectKind::YLabel => state.figure.set_axes_ylabel_style(axes_index, style),
594                PlotObjectKind::ZLabel => state.figure.set_axes_zlabel_style(axes_index, style),
595                PlotObjectKind::Legend => {}
596                PlotObjectKind::SuperTitle => state.figure.set_sg_title_style(style),
597            }
598        }
599        encode_plot_object_handle(handle, axes_index, kind)
600    })?;
601    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
602    Ok(object_handle)
603}
604
605pub fn set_xlabel_for_axes(
606    handle: FigureHandle,
607    axes_index: usize,
608    label: &str,
609    style: TextStyle,
610) -> Result<f64, FigureError> {
611    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
612        state.figure.set_axes_xlabel(axes_index, label.to_string());
613        state.figure.set_axes_xlabel_style(axes_index, style);
614        encode_plot_object_handle(handle, axes_index, PlotObjectKind::XLabel)
615    })?;
616    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
617    Ok(object_handle)
618}
619
620pub fn set_ylabel_for_axes(
621    handle: FigureHandle,
622    axes_index: usize,
623    label: &str,
624    style: TextStyle,
625) -> Result<f64, FigureError> {
626    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
627        state.figure.set_axes_ylabel(axes_index, label.to_string());
628        state.figure.set_axes_ylabel_style(axes_index, style);
629        encode_plot_object_handle(handle, axes_index, PlotObjectKind::YLabel)
630    })?;
631    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
632    Ok(object_handle)
633}
634
635pub fn set_zlabel_for_axes(
636    handle: FigureHandle,
637    axes_index: usize,
638    label: &str,
639    style: TextStyle,
640) -> Result<f64, FigureError> {
641    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
642        state.figure.set_axes_zlabel(axes_index, label.to_string());
643        state.figure.set_axes_zlabel_style(axes_index, style);
644        encode_plot_object_handle(handle, axes_index, PlotObjectKind::ZLabel)
645    })?;
646    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
647    Ok(object_handle)
648}
649
650pub fn add_text_annotation_for_axes(
651    handle: FigureHandle,
652    axes_index: usize,
653    position: glam::Vec3,
654    text: &str,
655    style: TextStyle,
656) -> Result<f64, FigureError> {
657    let (annotation_index, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
658        state
659            .figure
660            .add_axes_text_annotation(axes_index, position, text.to_string(), style)
661    })?;
662    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
663    Ok(register_text_annotation_handle(
664        handle,
665        axes_index,
666        annotation_index,
667    ))
668}
669
670pub fn set_text_annotation_properties_for_axes(
671    handle: FigureHandle,
672    axes_index: usize,
673    annotation_index: usize,
674    text: Option<String>,
675    position: Option<glam::Vec3>,
676    style: Option<TextStyle>,
677) -> Result<f64, FigureError> {
678    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
679        if let Some(text) = text {
680            state
681                .figure
682                .set_axes_text_annotation_text(axes_index, annotation_index, text);
683        }
684        if let Some(position) = position {
685            state
686                .figure
687                .set_axes_text_annotation_position(axes_index, annotation_index, position);
688        }
689        if let Some(style) = style {
690            state
691                .figure
692                .set_axes_text_annotation_style(axes_index, annotation_index, style);
693        }
694        register_text_annotation_handle(handle, axes_index, annotation_index)
695    })?;
696    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
697    Ok(object_handle)
698}
699
700pub fn toggle_box() -> bool {
701    let (handle, figure_clone, enabled) = {
702        let mut reg = registry();
703        let handle = reg.current;
704        let state = get_state_mut(&mut reg, handle);
705        let axes = state.active_axes;
706        let next = !state
707            .figure
708            .axes_metadata(axes)
709            .map(|m| m.box_enabled)
710            .unwrap_or(true);
711        state.figure.set_axes_box_enabled(axes, next);
712        state.revision = state.revision.wrapping_add(1);
713        (handle, state.figure.clone(), next)
714    };
715    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
716    enabled
717}
718
719pub fn set_axis_equal(enabled: bool) {
720    let (handle, figure_clone) = {
721        let mut reg = registry();
722        let handle = reg.current;
723        let state = get_state_mut(&mut reg, handle);
724        let axes = state.active_axes;
725        state.figure.set_axes_axis_equal(axes, enabled);
726        state.revision = state.revision.wrapping_add(1);
727        (handle, state.figure.clone())
728    };
729    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
730}
731
732pub fn set_axis_equal_for_axes(
733    handle: FigureHandle,
734    axes_index: usize,
735    enabled: bool,
736) -> Result<(), FigureError> {
737    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
738        state.figure.set_axes_axis_equal(axes_index, enabled);
739    })?;
740    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
741    Ok(())
742}
743
744pub fn set_axis_limits(x: Option<(f64, f64)>, y: Option<(f64, f64)>) {
745    let (handle, figure_clone) = {
746        let mut reg = registry();
747        let handle = reg.current;
748        let state = get_state_mut(&mut reg, handle);
749        let axes = state.active_axes;
750        state.figure.set_axes_limits(axes, x, y);
751        state.revision = state.revision.wrapping_add(1);
752        (handle, state.figure.clone())
753    };
754    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
755}
756
757pub fn set_axis_limits_for_axes(
758    handle: FigureHandle,
759    axes_index: usize,
760    x: Option<(f64, f64)>,
761    y: Option<(f64, f64)>,
762) -> Result<(), FigureError> {
763    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
764        state.figure.set_axes_limits(axes_index, x, y);
765    })?;
766    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
767    Ok(())
768}
769
770pub fn axis_limits_snapshot() -> AxisLimitSnapshot {
771    let mut reg = registry();
772    let handle = reg.current;
773    let state = get_state_mut(&mut reg, handle);
774    let axes = state.active_axes;
775    let meta = state
776        .figure
777        .axes_metadata(axes)
778        .cloned()
779        .unwrap_or_default();
780    (meta.x_limits, meta.y_limits)
781}
782
783pub fn z_limits_snapshot() -> Option<(f64, f64)> {
784    let mut reg = registry();
785    let handle = reg.current;
786    let state = get_state_mut(&mut reg, handle);
787    let axes = state.active_axes;
788    state.figure.axes_metadata(axes).and_then(|m| m.z_limits)
789}
790
791pub fn color_limits_snapshot() -> Option<(f64, f64)> {
792    let mut reg = registry();
793    let handle = reg.current;
794    let state = get_state_mut(&mut reg, handle);
795    let axes = state.active_axes;
796    state
797        .figure
798        .axes_metadata(axes)
799        .and_then(|m| m.color_limits)
800}
801
802pub fn set_z_limits(limits: Option<(f64, f64)>) {
803    let (handle, figure_clone) = {
804        let mut reg = registry();
805        let handle = reg.current;
806        let state = get_state_mut(&mut reg, handle);
807        let axes = state.active_axes;
808        state.figure.set_axes_z_limits(axes, limits);
809        state.revision = state.revision.wrapping_add(1);
810        (handle, state.figure.clone())
811    };
812    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
813}
814
815pub fn set_z_limits_for_axes(
816    handle: FigureHandle,
817    axes_index: usize,
818    limits: Option<(f64, f64)>,
819) -> Result<(), FigureError> {
820    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
821        state.figure.set_axes_z_limits(axes_index, limits);
822    })?;
823    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
824    Ok(())
825}
826
827pub fn set_color_limits_runtime(limits: Option<(f64, f64)>) {
828    let (handle, figure_clone) = {
829        let mut reg = registry();
830        let handle = reg.current;
831        let state = get_state_mut(&mut reg, handle);
832        let axes = state.active_axes;
833        state.figure.set_axes_color_limits(axes, limits);
834        state.revision = state.revision.wrapping_add(1);
835        (handle, state.figure.clone())
836    };
837    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
838}
839
840pub fn set_color_limits_for_axes(
841    handle: FigureHandle,
842    axes_index: usize,
843    limits: Option<(f64, f64)>,
844) -> Result<(), FigureError> {
845    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
846        state.figure.set_axes_color_limits(axes_index, limits);
847    })?;
848    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
849    Ok(())
850}
851
852pub fn clear_current_axes() {
853    let (handle, figure_clone) = {
854        let mut reg = registry();
855        let handle = reg.current;
856        let axes_index = {
857            let state = get_state_mut(&mut reg, handle);
858            let axes_index = state.active_axes;
859            state.figure.clear_axes(axes_index);
860            state.reset_cycle(axes_index);
861            state.revision = state.revision.wrapping_add(1);
862            axes_index
863        };
864        purge_plot_children_for_axes(&mut reg, handle, axes_index);
865        let figure_clone = reg
866            .figures
867            .get(&handle)
868            .expect("figure exists")
869            .figure
870            .clone();
871        (handle, figure_clone)
872    };
873    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
874}
875
876pub fn set_colorbar_enabled(enabled: bool) {
877    let (handle, figure_clone) = {
878        let mut reg = registry();
879        let handle = reg.current;
880        let state = get_state_mut(&mut reg, handle);
881        let axes = state.active_axes;
882        state.figure.set_axes_colorbar_enabled(axes, enabled);
883        state.revision = state.revision.wrapping_add(1);
884        (handle, state.figure.clone())
885    };
886    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
887}
888
889pub fn set_colorbar_enabled_for_axes(
890    handle: FigureHandle,
891    axes_index: usize,
892    enabled: bool,
893) -> Result<(), FigureError> {
894    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
895        state.figure.set_axes_colorbar_enabled(axes_index, enabled);
896    })?;
897    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
898    Ok(())
899}
900
901pub fn set_legend_for_axes(
902    handle: FigureHandle,
903    axes_index: usize,
904    enabled: bool,
905    labels: Option<&[String]>,
906    style: Option<LegendStyle>,
907) -> Result<f64, FigureError> {
908    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
909        state.figure.set_axes_legend_enabled(axes_index, enabled);
910        if let Some(labels) = labels {
911            state.figure.set_labels_for_axes(axes_index, labels);
912        }
913        if let Some(style) = style {
914            state.figure.set_axes_legend_style(axes_index, style);
915        }
916        encode_plot_object_handle(handle, axes_index, PlotObjectKind::Legend)
917    })?;
918    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
919    Ok(object_handle)
920}
921
922pub fn set_log_modes_for_axes(
923    handle: FigureHandle,
924    axes_index: usize,
925    x_log: bool,
926    y_log: bool,
927) -> Result<(), FigureError> {
928    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
929        state.figure.set_axes_log_modes(axes_index, x_log, y_log);
930    })?;
931    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
932    Ok(())
933}
934
935pub fn set_view_for_axes(
936    handle: FigureHandle,
937    axes_index: usize,
938    azimuth_deg: f32,
939    elevation_deg: f32,
940) -> Result<(), FigureError> {
941    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
942        state
943            .figure
944            .set_axes_view(axes_index, azimuth_deg, elevation_deg);
945    })?;
946    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
947    Ok(())
948}
949
950pub fn legend_entries_snapshot(
951    handle: FigureHandle,
952    axes_index: usize,
953) -> Result<Vec<runmat_plot::plots::LegendEntry>, FigureError> {
954    let mut reg = registry();
955    let state = get_state_mut(&mut reg, handle);
956    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
957    if axes_index >= total_axes {
958        return Err(FigureError::InvalidSubplotIndex {
959            rows: state.figure.axes_rows.max(1),
960            cols: state.figure.axes_cols.max(1),
961            index: axes_index,
962        });
963    }
964    Ok(state.figure.legend_entries_for_axes(axes_index))
965}
966
967pub fn toggle_colorbar() -> bool {
968    let (handle, figure_clone, enabled) = {
969        let mut reg = registry();
970        let handle = reg.current;
971        let state = get_state_mut(&mut reg, handle);
972        let axes = state.active_axes;
973        let next = !state
974            .figure
975            .axes_metadata(axes)
976            .map(|m| m.colorbar_enabled)
977            .unwrap_or(false);
978        state.figure.set_axes_colorbar_enabled(axes, next);
979        state.revision = state.revision.wrapping_add(1);
980        (handle, state.figure.clone(), next)
981    };
982    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
983    enabled
984}
985
986pub fn set_colormap(colormap: ColorMap) {
987    let (handle, figure_clone) = {
988        let mut reg = registry();
989        let handle = reg.current;
990        let state = get_state_mut(&mut reg, handle);
991        let axes = state.active_axes;
992        state.figure.set_axes_colormap(axes, colormap);
993        state.revision = state.revision.wrapping_add(1);
994        (handle, state.figure.clone())
995    };
996    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
997}
998
999pub fn set_colormap_for_axes(
1000    handle: FigureHandle,
1001    axes_index: usize,
1002    colormap: ColorMap,
1003) -> Result<(), FigureError> {
1004    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1005        state.figure.set_axes_colormap(axes_index, colormap);
1006    })?;
1007    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1008    Ok(())
1009}
1010
1011pub fn set_surface_shading(mode: ShadingMode) {
1012    let (handle, figure_clone) = {
1013        let mut reg = registry();
1014        let handle = reg.current;
1015        let state = get_state_mut(&mut reg, handle);
1016        let plot_count = state.figure.len();
1017        for idx in 0..plot_count {
1018            if let Some(PlotElement::Surface(surface)) = state.figure.get_plot_mut(idx) {
1019                *surface = surface.clone().with_shading(mode);
1020            }
1021        }
1022        state.revision = state.revision.wrapping_add(1);
1023        (handle, state.figure.clone())
1024    };
1025    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1026}
1027
1028#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1029pub enum FigureEventKind {
1030    Created,
1031    Updated,
1032    Cleared,
1033    Closed,
1034}
1035
1036#[derive(Clone, Copy)]
1037pub struct FigureEventView<'a> {
1038    pub handle: FigureHandle,
1039    pub kind: FigureEventKind,
1040    pub figure: Option<&'a Figure>,
1041}
1042
1043type FigureObserver = dyn for<'a> Fn(FigureEventView<'a>) + Send + Sync + 'static;
1044
1045struct FigureObserverRegistry {
1046    observers: Mutex<Vec<Arc<FigureObserver>>>,
1047}
1048
1049impl FigureObserverRegistry {
1050    fn new() -> Self {
1051        Self {
1052            observers: Mutex::new(Vec::new()),
1053        }
1054    }
1055
1056    fn install(&self, observer: Arc<FigureObserver>) {
1057        let mut guard = self.observers.lock().expect("figure observers poisoned");
1058        guard.push(observer);
1059    }
1060
1061    fn notify(&self, view: FigureEventView<'_>) {
1062        let snapshot = {
1063            let guard = self.observers.lock().expect("figure observers poisoned");
1064            guard.clone()
1065        };
1066        for observer in snapshot {
1067            observer(view);
1068        }
1069    }
1070
1071    fn is_empty(&self) -> bool {
1072        self.observers
1073            .lock()
1074            .map(|guard| guard.is_empty())
1075            .unwrap_or(true)
1076    }
1077}
1078
1079static FIGURE_OBSERVERS: OnceCell<FigureObserverRegistry> = OnceCell::new();
1080
1081runmat_thread_local! {
1082    static RECENT_FIGURES: RefCell<HashSet<FigureHandle>> = RefCell::new(HashSet::new());
1083    static ACTIVE_AXES_CONTEXT: RefCell<Option<ActiveAxesContext>> = const { RefCell::new(None) };
1084}
1085
1086#[derive(Clone, Copy, Debug)]
1087pub struct FigureAxesState {
1088    pub handle: FigureHandle,
1089    pub rows: usize,
1090    pub cols: usize,
1091    pub active_index: usize,
1092}
1093
1094pub fn encode_axes_handle(handle: FigureHandle, axes_index: usize) -> f64 {
1095    let encoded =
1096        ((handle.as_u32() as u64) << AXES_INDEX_BITS) | ((axes_index as u64) & AXES_INDEX_MASK);
1097    encoded as f64
1098}
1099
1100pub fn encode_plot_object_handle(
1101    handle: FigureHandle,
1102    axes_index: usize,
1103    kind: PlotObjectKind,
1104) -> f64 {
1105    let encoded = (((handle.as_u32() as u64) << AXES_INDEX_BITS)
1106        | ((axes_index as u64) & AXES_INDEX_MASK))
1107        << OBJECT_KIND_BITS
1108        | ((kind as u64) & OBJECT_KIND_MASK);
1109    encoded as f64
1110}
1111
1112pub fn decode_plot_object_handle(
1113    value: f64,
1114) -> Result<(FigureHandle, usize, PlotObjectKind), FigureError> {
1115    if !value.is_finite() || value <= 0.0 {
1116        return Err(FigureError::InvalidPlotObjectHandle);
1117    }
1118    let encoded = value.round() as u64;
1119    let kind = PlotObjectKind::from_u64(encoded & OBJECT_KIND_MASK)
1120        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1121    let base = encoded >> OBJECT_KIND_BITS;
1122    let figure_id = base >> AXES_INDEX_BITS;
1123    if figure_id == 0 {
1124        return Err(FigureError::InvalidPlotObjectHandle);
1125    }
1126    let axes_index = (base & AXES_INDEX_MASK) as usize;
1127    Ok((FigureHandle::from(figure_id as u32), axes_index, kind))
1128}
1129
1130pub fn register_histogram_handle(
1131    figure: FigureHandle,
1132    axes_index: usize,
1133    plot_index: usize,
1134    bin_edges: Vec<f64>,
1135    raw_counts: Vec<f64>,
1136    normalization: String,
1137) -> f64 {
1138    let mut reg = registry();
1139    let id = reg.next_plot_child_handle;
1140    reg.next_plot_child_handle += 1;
1141    reg.plot_children.insert(
1142        id,
1143        PlotChildHandleState::Histogram(HistogramHandleState {
1144            figure,
1145            axes_index,
1146            plot_index,
1147            bin_edges,
1148            raw_counts,
1149            normalization,
1150        }),
1151    );
1152    id as f64
1153}
1154
1155fn register_simple_plot_handle(
1156    figure: FigureHandle,
1157    axes_index: usize,
1158    plot_index: usize,
1159    constructor: fn(SimplePlotHandleState) -> PlotChildHandleState,
1160) -> f64 {
1161    let mut reg = registry();
1162    let id = reg.next_plot_child_handle;
1163    reg.next_plot_child_handle += 1;
1164    reg.plot_children.insert(
1165        id,
1166        constructor(SimplePlotHandleState {
1167            figure,
1168            axes_index,
1169            plot_index,
1170        }),
1171    );
1172    id as f64
1173}
1174
1175pub fn register_line_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1176    register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Line)
1177}
1178
1179pub fn register_reference_line_handle(
1180    figure: FigureHandle,
1181    axes_index: usize,
1182    plot_index: usize,
1183) -> f64 {
1184    register_simple_plot_handle(
1185        figure,
1186        axes_index,
1187        plot_index,
1188        PlotChildHandleState::ReferenceLine,
1189    )
1190}
1191
1192pub fn register_scatter_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1193    register_simple_plot_handle(
1194        figure,
1195        axes_index,
1196        plot_index,
1197        PlotChildHandleState::Scatter,
1198    )
1199}
1200
1201pub fn register_bar_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1202    register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Bar)
1203}
1204
1205pub fn register_stem_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1206    register_simple_plot_handle(figure, axes_index, plot_index, |state| {
1207        PlotChildHandleState::Stem(StemHandleState {
1208            figure: state.figure,
1209            axes_index: state.axes_index,
1210            plot_index: state.plot_index,
1211        })
1212    })
1213}
1214
1215pub fn register_errorbar_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1216    register_simple_plot_handle(figure, axes_index, plot_index, |state| {
1217        PlotChildHandleState::ErrorBar(ErrorBarHandleState {
1218            figure: state.figure,
1219            axes_index: state.axes_index,
1220            plot_index: state.plot_index,
1221        })
1222    })
1223}
1224
1225pub fn register_stairs_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1226    register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Stairs)
1227}
1228
1229pub fn register_quiver_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1230    register_simple_plot_handle(figure, axes_index, plot_index, |state| {
1231        PlotChildHandleState::Quiver(QuiverHandleState {
1232            figure: state.figure,
1233            axes_index: state.axes_index,
1234            plot_index: state.plot_index,
1235        })
1236    })
1237}
1238
1239pub fn register_image_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1240    register_simple_plot_handle(figure, axes_index, plot_index, |state| {
1241        PlotChildHandleState::Image(ImageHandleState {
1242            figure: state.figure,
1243            axes_index: state.axes_index,
1244            plot_index: state.plot_index,
1245        })
1246    })
1247}
1248
1249pub fn register_heatmap_handle(
1250    figure: FigureHandle,
1251    axes_index: usize,
1252    plot_index: usize,
1253    x_labels: Vec<String>,
1254    y_labels: Vec<String>,
1255    color_data: Tensor,
1256) -> f64 {
1257    let mut reg = registry();
1258    let id = reg.next_plot_child_handle;
1259    reg.next_plot_child_handle += 1;
1260    reg.plot_children.insert(
1261        id,
1262        PlotChildHandleState::Heatmap(HeatmapHandleState {
1263            figure,
1264            axes_index,
1265            plot_index,
1266            x_labels,
1267            y_labels,
1268            color_data,
1269        }),
1270    );
1271    id as f64
1272}
1273
1274pub fn register_area_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1275    register_simple_plot_handle(figure, axes_index, plot_index, |state| {
1276        PlotChildHandleState::Area(AreaHandleState {
1277            figure: state.figure,
1278            axes_index: state.axes_index,
1279            plot_index: state.plot_index,
1280        })
1281    })
1282}
1283
1284pub fn register_surface_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1285    register_simple_plot_handle(
1286        figure,
1287        axes_index,
1288        plot_index,
1289        PlotChildHandleState::Surface,
1290    )
1291}
1292
1293pub fn register_patch_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1294    register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Patch)
1295}
1296
1297pub fn register_line3_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1298    register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Line3)
1299}
1300
1301pub fn register_scatter3_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1302    register_simple_plot_handle(
1303        figure,
1304        axes_index,
1305        plot_index,
1306        PlotChildHandleState::Scatter3,
1307    )
1308}
1309
1310pub fn register_contour_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1311    register_simple_plot_handle(
1312        figure,
1313        axes_index,
1314        plot_index,
1315        PlotChildHandleState::Contour,
1316    )
1317}
1318
1319pub fn register_contour_fill_handle(
1320    figure: FigureHandle,
1321    axes_index: usize,
1322    plot_index: usize,
1323) -> f64 {
1324    register_simple_plot_handle(
1325        figure,
1326        axes_index,
1327        plot_index,
1328        PlotChildHandleState::ContourFill,
1329    )
1330}
1331
1332pub fn register_pie_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1333    register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Pie)
1334}
1335
1336pub fn register_text_annotation_handle(
1337    figure: FigureHandle,
1338    axes_index: usize,
1339    annotation_index: usize,
1340) -> f64 {
1341    let mut reg = registry();
1342    let id = reg.next_plot_child_handle;
1343    reg.next_plot_child_handle += 1;
1344    reg.plot_children.insert(
1345        id,
1346        PlotChildHandleState::Text(TextAnnotationHandleState {
1347            figure,
1348            axes_index,
1349            annotation_index,
1350        }),
1351    );
1352    id as f64
1353}
1354
1355pub fn plot_child_handle_snapshot(handle: f64) -> Result<PlotChildHandleState, FigureError> {
1356    if !handle.is_finite() || handle <= 0.0 {
1357        return Err(FigureError::InvalidPlotObjectHandle);
1358    }
1359    let reg = registry();
1360    reg.plot_children
1361        .get(&(handle.round() as u64))
1362        .cloned()
1363        .ok_or(FigureError::InvalidPlotObjectHandle)
1364}
1365
1366pub fn set_heatmap_display_labels(
1367    figure: FigureHandle,
1368    axes_index: usize,
1369    plot_index: usize,
1370    x_labels: Option<Vec<String>>,
1371    y_labels: Option<Vec<String>>,
1372) -> Result<(), FigureError> {
1373    let figure_clone = {
1374        let mut reg = registry();
1375        let (current_x_labels, current_y_labels) = {
1376            let state = reg.plot_children.values_mut().find(|state| match state {
1377                PlotChildHandleState::Heatmap(heatmap) => {
1378                    heatmap.figure == figure
1379                        && heatmap.axes_index == axes_index
1380                        && heatmap.plot_index == plot_index
1381                }
1382                _ => false,
1383            });
1384            let PlotChildHandleState::Heatmap(heatmap) =
1385                state.ok_or(FigureError::InvalidPlotObjectHandle)?
1386            else {
1387                return Err(FigureError::InvalidPlotObjectHandle);
1388            };
1389            if let Some(labels) = x_labels {
1390                heatmap.x_labels = labels;
1391            }
1392            if let Some(labels) = y_labels {
1393                heatmap.y_labels = labels;
1394            }
1395            (heatmap.x_labels.clone(), heatmap.y_labels.clone())
1396        };
1397
1398        let state = get_state_mut(&mut reg, figure);
1399        let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1400        if axes_index >= total_axes {
1401            return Err(FigureError::InvalidSubplotIndex {
1402                rows: state.figure.axes_rows.max(1),
1403                cols: state.figure.axes_cols.max(1),
1404                index: axes_index,
1405            });
1406        }
1407        state.active_axes = axes_index;
1408        state.figure.set_active_axes_index(axes_index);
1409        state.figure.set_axes_tick_labels(
1410            axes_index,
1411            Some(current_x_labels),
1412            Some(current_y_labels),
1413        );
1414        state.revision = state.revision.wrapping_add(1);
1415        state.figure.clone()
1416    };
1417    notify_with_figure(figure, &figure_clone, FigureEventKind::Updated);
1418    Ok(())
1419}
1420
1421pub fn update_histogram_handle_for_plot(
1422    figure: FigureHandle,
1423    axes_index: usize,
1424    plot_index: usize,
1425    normalization: String,
1426    raw_counts: Vec<f64>,
1427) -> Result<(), FigureError> {
1428    let mut reg = registry();
1429    let state = reg.plot_children.values_mut().find(|state| match state {
1430        PlotChildHandleState::Histogram(hist) => {
1431            hist.figure == figure && hist.axes_index == axes_index && hist.plot_index == plot_index
1432        }
1433        _ => false,
1434    });
1435    match state.ok_or(FigureError::InvalidPlotObjectHandle)? {
1436        PlotChildHandleState::Histogram(hist) => {
1437            hist.normalization = normalization;
1438            hist.raw_counts = raw_counts;
1439            Ok(())
1440        }
1441        _ => Err(FigureError::InvalidPlotObjectHandle),
1442    }
1443}
1444
1445pub fn update_errorbar_plot(
1446    figure_handle: FigureHandle,
1447    plot_index: usize,
1448    updater: impl FnOnce(&mut runmat_plot::plots::ErrorBar),
1449) -> Result<(), FigureError> {
1450    let mut reg = registry();
1451    let state = get_state_mut(&mut reg, figure_handle);
1452    let plot = state
1453        .figure
1454        .get_plot_mut(plot_index)
1455        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1456    match plot {
1457        runmat_plot::plots::figure::PlotElement::ErrorBar(errorbar) => {
1458            updater(errorbar);
1459            Ok(())
1460        }
1461        _ => Err(FigureError::InvalidPlotObjectHandle),
1462    }
1463}
1464
1465pub fn update_histogram_plot_data(
1466    figure_handle: FigureHandle,
1467    plot_index: usize,
1468    labels: Vec<String>,
1469    values: Vec<f64>,
1470) -> Result<(), FigureError> {
1471    let mut reg = registry();
1472    let state = get_state_mut(&mut reg, figure_handle);
1473    let plot = state
1474        .figure
1475        .get_plot_mut(plot_index)
1476        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1477    match plot {
1478        runmat_plot::plots::figure::PlotElement::Bar(bar) => {
1479            bar.set_data(labels, values)
1480                .map_err(|_| FigureError::InvalidPlotObjectHandle)?;
1481            Ok(())
1482        }
1483        _ => Err(FigureError::InvalidPlotObjectHandle),
1484    }
1485}
1486
1487pub fn update_stem_plot(
1488    figure_handle: FigureHandle,
1489    plot_index: usize,
1490    updater: impl FnOnce(&mut runmat_plot::plots::StemPlot),
1491) -> Result<(), FigureError> {
1492    let mut reg = registry();
1493    let state = get_state_mut(&mut reg, figure_handle);
1494    let plot = state
1495        .figure
1496        .get_plot_mut(plot_index)
1497        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1498    match plot {
1499        runmat_plot::plots::figure::PlotElement::Stem(stem) => {
1500            updater(stem);
1501            Ok(())
1502        }
1503        _ => Err(FigureError::InvalidPlotObjectHandle),
1504    }
1505}
1506
1507pub fn update_quiver_plot(
1508    figure_handle: FigureHandle,
1509    plot_index: usize,
1510    updater: impl FnOnce(&mut runmat_plot::plots::QuiverPlot),
1511) -> Result<(), FigureError> {
1512    let mut reg = registry();
1513    let state = get_state_mut(&mut reg, figure_handle);
1514    let plot = state
1515        .figure
1516        .get_plot_mut(plot_index)
1517        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1518    match plot {
1519        runmat_plot::plots::figure::PlotElement::Quiver(quiver) => {
1520            updater(quiver);
1521            Ok(())
1522        }
1523        _ => Err(FigureError::InvalidPlotObjectHandle),
1524    }
1525}
1526
1527pub fn update_image_plot(
1528    figure_handle: FigureHandle,
1529    plot_index: usize,
1530    updater: impl FnOnce(&mut runmat_plot::plots::SurfacePlot),
1531) -> Result<(), FigureError> {
1532    let mut reg = registry();
1533    let state = get_state_mut(&mut reg, figure_handle);
1534    let plot = state
1535        .figure
1536        .get_plot_mut(plot_index)
1537        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1538    match plot {
1539        runmat_plot::plots::figure::PlotElement::Surface(surface) if surface.image_mode => {
1540            updater(surface);
1541            Ok(())
1542        }
1543        _ => Err(FigureError::InvalidPlotObjectHandle),
1544    }
1545}
1546
1547pub fn update_area_plot(
1548    figure_handle: FigureHandle,
1549    plot_index: usize,
1550    updater: impl FnOnce(&mut runmat_plot::plots::AreaPlot),
1551) -> Result<(), FigureError> {
1552    let mut reg = registry();
1553    let state = get_state_mut(&mut reg, figure_handle);
1554    let plot = state
1555        .figure
1556        .get_plot_mut(plot_index)
1557        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1558    match plot {
1559        runmat_plot::plots::figure::PlotElement::Area(area) => {
1560            updater(area);
1561            Ok(())
1562        }
1563        _ => Err(FigureError::InvalidPlotObjectHandle),
1564    }
1565}
1566
1567pub fn update_plot_element(
1568    figure_handle: FigureHandle,
1569    plot_index: usize,
1570    updater: impl FnOnce(&mut runmat_plot::plots::figure::PlotElement),
1571) -> Result<(), FigureError> {
1572    let mut reg = registry();
1573    let state = get_state_mut(&mut reg, figure_handle);
1574    let plot = state
1575        .figure
1576        .get_plot_mut(plot_index)
1577        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1578    updater(plot);
1579    Ok(())
1580}
1581
1582fn purge_plot_children_for_figure(reg: &mut PlotRegistry, handle: FigureHandle) {
1583    reg.plot_children.retain(|_, state| match state {
1584        PlotChildHandleState::Histogram(hist) => hist.figure != handle,
1585        PlotChildHandleState::Line(plot)
1586        | PlotChildHandleState::Scatter(plot)
1587        | PlotChildHandleState::Bar(plot)
1588        | PlotChildHandleState::Stairs(plot)
1589        | PlotChildHandleState::Surface(plot)
1590        | PlotChildHandleState::Patch(plot)
1591        | PlotChildHandleState::Line3(plot)
1592        | PlotChildHandleState::Scatter3(plot)
1593        | PlotChildHandleState::Contour(plot)
1594        | PlotChildHandleState::ContourFill(plot)
1595        | PlotChildHandleState::ReferenceLine(plot)
1596        | PlotChildHandleState::Pie(plot) => plot.figure != handle,
1597        PlotChildHandleState::Stem(stem) => stem.figure != handle,
1598        PlotChildHandleState::ErrorBar(err) => err.figure != handle,
1599        PlotChildHandleState::Quiver(quiver) => quiver.figure != handle,
1600        PlotChildHandleState::Image(image) => image.figure != handle,
1601        PlotChildHandleState::Heatmap(heatmap) => heatmap.figure != handle,
1602        PlotChildHandleState::Area(area) => area.figure != handle,
1603        PlotChildHandleState::Text(text) => text.figure != handle,
1604    });
1605}
1606
1607fn purge_plot_children_for_axes(reg: &mut PlotRegistry, handle: FigureHandle, axes_index: usize) {
1608    reg.plot_children.retain(|_, state| match state {
1609        PlotChildHandleState::Histogram(hist) => {
1610            !(hist.figure == handle && hist.axes_index == axes_index)
1611        }
1612        PlotChildHandleState::Line(plot)
1613        | PlotChildHandleState::Scatter(plot)
1614        | PlotChildHandleState::Bar(plot)
1615        | PlotChildHandleState::Stairs(plot)
1616        | PlotChildHandleState::Surface(plot)
1617        | PlotChildHandleState::Patch(plot)
1618        | PlotChildHandleState::Line3(plot)
1619        | PlotChildHandleState::Scatter3(plot)
1620        | PlotChildHandleState::Contour(plot)
1621        | PlotChildHandleState::ContourFill(plot)
1622        | PlotChildHandleState::ReferenceLine(plot)
1623        | PlotChildHandleState::Pie(plot) => {
1624            !(plot.figure == handle && plot.axes_index == axes_index)
1625        }
1626        PlotChildHandleState::Stem(stem) => {
1627            !(stem.figure == handle && stem.axes_index == axes_index)
1628        }
1629        PlotChildHandleState::ErrorBar(err) => {
1630            !(err.figure == handle && err.axes_index == axes_index)
1631        }
1632        PlotChildHandleState::Quiver(quiver) => {
1633            !(quiver.figure == handle && quiver.axes_index == axes_index)
1634        }
1635        PlotChildHandleState::Image(image) => {
1636            !(image.figure == handle && image.axes_index == axes_index)
1637        }
1638        PlotChildHandleState::Heatmap(heatmap) => {
1639            !(heatmap.figure == handle && heatmap.axes_index == axes_index)
1640        }
1641        PlotChildHandleState::Area(area) => {
1642            !(area.figure == handle && area.axes_index == axes_index)
1643        }
1644        PlotChildHandleState::Text(text) => {
1645            !(text.figure == handle && text.axes_index == axes_index)
1646        }
1647    });
1648}
1649
1650#[allow(dead_code)]
1651pub fn decode_axes_handle(value: f64) -> Result<(FigureHandle, usize), FigureError> {
1652    if !value.is_finite() || value <= 0.0 {
1653        return Err(FigureError::InvalidAxesHandle);
1654    }
1655    let encoded = value.round() as u64;
1656    let figure_id = encoded >> AXES_INDEX_BITS;
1657    if figure_id == 0 {
1658        return Err(FigureError::InvalidAxesHandle);
1659    }
1660    let axes_index = (encoded & AXES_INDEX_MASK) as usize;
1661    Ok((FigureHandle::from(figure_id as u32), axes_index))
1662}
1663
1664#[cfg(not(target_arch = "wasm32"))]
1665fn registry() -> PlotRegistryGuard<'static> {
1666    #[cfg(test)]
1667    let test_lock = TEST_PLOT_OUTER_LOCK_HELD.with(|flag| {
1668        if flag.get() {
1669            None
1670        } else {
1671            Some(
1672                TEST_PLOT_REGISTRY_LOCK
1673                    .lock()
1674                    .unwrap_or_else(|e| e.into_inner()),
1675            )
1676        }
1677    });
1678    let guard = REGISTRY
1679        .get_or_init(|| Mutex::new(PlotRegistry::default()))
1680        .lock()
1681        .expect("plot registry poisoned");
1682    #[cfg(test)]
1683    {
1684        PlotRegistryGuard::new(guard, test_lock)
1685    }
1686    #[cfg(not(test))]
1687    {
1688        PlotRegistryGuard::new(guard)
1689    }
1690}
1691
1692#[cfg(target_arch = "wasm32")]
1693fn registry() -> PlotRegistryGuard<'static> {
1694    REGISTRY.with(|cell| {
1695        let guard = cell.borrow_mut();
1696        // SAFETY: the thread-local RefCell lives for the program lifetime and the borrow
1697        // guard is dropped when PlotRegistryGuard is dropped, so extending the lifetime
1698        // to 'static is sound.
1699        let guard_static: std::cell::RefMut<'static, PlotRegistry> =
1700            unsafe { std::mem::transmute::<std::cell::RefMut<'_, PlotRegistry>, _>(guard) };
1701        #[cfg(test)]
1702        {
1703            let test_lock = TEST_PLOT_OUTER_LOCK_HELD.with(|flag| {
1704                if flag.get() {
1705                    None
1706                } else {
1707                    Some(
1708                        TEST_PLOT_REGISTRY_LOCK
1709                            .lock()
1710                            .unwrap_or_else(|e| e.into_inner()),
1711                    )
1712                }
1713            });
1714            PlotRegistryGuard::new(guard_static, test_lock)
1715        }
1716        #[cfg(not(test))]
1717        {
1718            PlotRegistryGuard::new(guard_static)
1719        }
1720    })
1721}
1722
1723fn get_state_mut(registry: &mut PlotRegistry, handle: FigureHandle) -> &mut FigureState {
1724    registry
1725        .figures
1726        .entry(handle)
1727        .or_insert_with(|| FigureState::new(handle))
1728}
1729
1730fn observer_registry() -> &'static FigureObserverRegistry {
1731    FIGURE_OBSERVERS.get_or_init(FigureObserverRegistry::new)
1732}
1733
1734pub fn install_figure_observer(observer: Arc<FigureObserver>) -> BuiltinResult<()> {
1735    observer_registry().install(observer);
1736    Ok(())
1737}
1738
1739fn notify_event<'a>(view: FigureEventView<'a>) {
1740    note_recent_figure(view.handle);
1741    if let Some(registry) = FIGURE_OBSERVERS.get() {
1742        if registry.is_empty() {
1743            return;
1744        }
1745        registry.notify(view);
1746    }
1747}
1748
1749fn notify_with_figure(handle: FigureHandle, figure: &Figure, kind: FigureEventKind) {
1750    notify_event(FigureEventView {
1751        handle,
1752        kind,
1753        figure: Some(figure),
1754    });
1755}
1756
1757fn notify_without_figure(handle: FigureHandle, kind: FigureEventKind) {
1758    notify_event(FigureEventView {
1759        handle,
1760        kind,
1761        figure: None,
1762    });
1763}
1764
1765fn note_recent_figure(handle: FigureHandle) {
1766    RECENT_FIGURES.with(|set| {
1767        set.borrow_mut().insert(handle);
1768    });
1769}
1770
1771pub fn reset_recent_figures() {
1772    RECENT_FIGURES.with(|set| set.borrow_mut().clear());
1773}
1774
1775pub fn reset_plot_state() {
1776    {
1777        let mut reg = registry();
1778        *reg = PlotRegistry::default();
1779    }
1780    reset_recent_figures();
1781}
1782
1783pub fn take_recent_figures() -> Vec<FigureHandle> {
1784    RECENT_FIGURES.with(|set| set.borrow_mut().drain().collect())
1785}
1786
1787pub fn select_figure(handle: FigureHandle) {
1788    let mut reg = registry();
1789    reg.current = handle;
1790    let maybe_new = match reg.figures.entry(handle) {
1791        Entry::Occupied(entry) => {
1792            let _ = entry.into_mut();
1793            None
1794        }
1795        Entry::Vacant(vacant) => {
1796            let state = vacant.insert(FigureState::new(handle));
1797            Some(state.figure.clone())
1798        }
1799    };
1800    drop(reg);
1801    if let Some(figure_clone) = maybe_new {
1802        notify_with_figure(handle, &figure_clone, FigureEventKind::Created);
1803    }
1804}
1805
1806pub fn new_figure_handle() -> FigureHandle {
1807    let mut reg = registry();
1808    let handle = reg.next_handle;
1809    reg.next_handle = reg.next_handle.next();
1810    reg.current = handle;
1811    let figure_clone = {
1812        let state = get_state_mut(&mut reg, handle);
1813        state.figure.clone()
1814    };
1815    drop(reg);
1816    notify_with_figure(handle, &figure_clone, FigureEventKind::Created);
1817    handle
1818}
1819
1820pub fn current_figure_handle() -> FigureHandle {
1821    registry().current
1822}
1823
1824pub fn current_axes_state() -> FigureAxesState {
1825    let mut reg = registry();
1826    let handle = reg.current;
1827    // Ensure a default figure exists even if nothing has rendered yet (common on wasm/web).
1828    let state = get_state_mut(&mut reg, handle);
1829    FigureAxesState {
1830        handle,
1831        rows: state.figure.axes_rows.max(1),
1832        cols: state.figure.axes_cols.max(1),
1833        active_index: state.active_axes,
1834    }
1835}
1836
1837pub fn axes_handle_exists(handle: FigureHandle, axes_index: usize) -> bool {
1838    let mut reg = registry();
1839    let state = get_state_mut(&mut reg, handle);
1840    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1841    axes_index < total_axes
1842}
1843
1844pub fn figure_handle_exists(handle: FigureHandle) -> bool {
1845    let reg = registry();
1846    reg.figures.contains_key(&handle)
1847}
1848
1849pub fn axes_metadata_snapshot(
1850    handle: FigureHandle,
1851    axes_index: usize,
1852) -> Result<runmat_plot::plots::AxesMetadata, FigureError> {
1853    let mut reg = registry();
1854    let state = get_state_mut(&mut reg, handle);
1855    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1856    if axes_index >= total_axes {
1857        return Err(FigureError::InvalidSubplotIndex {
1858            rows: state.figure.axes_rows.max(1),
1859            cols: state.figure.axes_cols.max(1),
1860            index: axes_index,
1861        });
1862    }
1863    state
1864        .figure
1865        .axes_metadata(axes_index)
1866        .cloned()
1867        .ok_or(FigureError::InvalidAxesHandle)
1868}
1869
1870pub fn axes_state_snapshot(
1871    handle: FigureHandle,
1872    axes_index: usize,
1873) -> Result<FigureAxesState, FigureError> {
1874    let mut reg = registry();
1875    let state = get_state_mut(&mut reg, handle);
1876    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1877    if axes_index >= total_axes {
1878        return Err(FigureError::InvalidSubplotIndex {
1879            rows: state.figure.axes_rows.max(1),
1880            cols: state.figure.axes_cols.max(1),
1881            index: axes_index,
1882        });
1883    }
1884    Ok(FigureAxesState {
1885        handle,
1886        rows: state.figure.axes_rows.max(1),
1887        cols: state.figure.axes_cols.max(1),
1888        active_index: axes_index,
1889    })
1890}
1891
1892pub fn current_axes_handle_for_figure(handle: FigureHandle) -> Result<f64, FigureError> {
1893    let mut reg = registry();
1894    let state = get_state_mut(&mut reg, handle);
1895    Ok(encode_axes_handle(handle, state.active_axes))
1896}
1897
1898pub fn axes_handles_for_figure(handle: FigureHandle) -> Result<Vec<f64>, FigureError> {
1899    let mut reg = registry();
1900    let state = get_state_mut(&mut reg, handle);
1901    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1902    Ok((0..total_axes)
1903        .map(|idx| encode_axes_handle(handle, idx))
1904        .collect())
1905}
1906
1907pub fn select_axes_for_figure(handle: FigureHandle, axes_index: usize) -> Result<(), FigureError> {
1908    let mut reg = registry();
1909    let state = get_state_mut(&mut reg, handle);
1910    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1911    if axes_index >= total_axes {
1912        return Err(FigureError::InvalidSubplotIndex {
1913            rows: state.figure.axes_rows.max(1),
1914            cols: state.figure.axes_cols.max(1),
1915            index: axes_index,
1916        });
1917    }
1918    reg.current = handle;
1919    let state = get_state_mut(&mut reg, handle);
1920    state.active_axes = axes_index;
1921    state.figure.set_active_axes_index(axes_index);
1922    Ok(())
1923}
1924
1925fn with_axes_target_mut<R>(
1926    handle: FigureHandle,
1927    axes_index: usize,
1928    f: impl FnOnce(&mut FigureState) -> R,
1929) -> Result<(R, Figure), FigureError> {
1930    let mut reg = registry();
1931    let state = get_state_mut(&mut reg, handle);
1932    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1933    if axes_index >= total_axes {
1934        return Err(FigureError::InvalidSubplotIndex {
1935            rows: state.figure.axes_rows.max(1),
1936            cols: state.figure.axes_cols.max(1),
1937            index: axes_index,
1938        });
1939    }
1940    state.active_axes = axes_index;
1941    state.figure.set_active_axes_index(axes_index);
1942    let result = f(state);
1943    state.revision = state.revision.wrapping_add(1);
1944    Ok((result, state.figure.clone()))
1945}
1946
1947fn with_figure_mut<R>(
1948    handle: FigureHandle,
1949    f: impl FnOnce(&mut FigureState) -> R,
1950) -> Result<(R, Figure), FigureError> {
1951    let mut reg = registry();
1952    let state = get_state_mut(&mut reg, handle);
1953    let result = f(state);
1954    state.revision = state.revision.wrapping_add(1);
1955    Ok((result, state.figure.clone()))
1956}
1957
1958pub fn current_hold_enabled() -> bool {
1959    let mut reg = registry();
1960    let handle = reg.current;
1961    // Ensure a default figure exists even if nothing has rendered yet (common on wasm/web).
1962    let state = get_state_mut(&mut reg, handle);
1963    *state
1964        .hold_per_axes
1965        .get(&state.active_axes)
1966        .unwrap_or(&false)
1967}
1968
1969/// Reset hold state for all figures/axes.
1970///
1971/// In the IDE, we want re-running code to behave like a fresh plotting run unless the code
1972/// explicitly enables `hold on` again. Without this, a prior `hold on` will cause subsequent
1973/// runs to keep appending to the same axes (surprising for typical "Run" workflows).
1974pub fn reset_hold_state_for_run() {
1975    let mut reg = registry();
1976    for state in reg.figures.values_mut() {
1977        state.hold_per_axes.clear();
1978    }
1979}
1980
1981pub fn figure_handles() -> Vec<FigureHandle> {
1982    let reg = registry();
1983    reg.figures.keys().copied().collect()
1984}
1985
1986pub fn clone_figure(handle: FigureHandle) -> Option<Figure> {
1987    let reg = registry();
1988    reg.figures.get(&handle).map(|state| state.figure.clone())
1989}
1990
1991pub fn figure_has_sg_title(handle: FigureHandle) -> bool {
1992    let reg = registry();
1993    reg.figures
1994        .get(&handle)
1995        .map(|state| state.figure.sg_title.is_some())
1996        .unwrap_or(false)
1997}
1998
1999pub fn import_figure(figure: Figure) -> FigureHandle {
2000    let mut reg = registry();
2001    let handle = reg.next_handle;
2002    reg.next_handle = reg.next_handle.next();
2003    reg.current = handle;
2004    let figure_clone = figure.clone();
2005    reg.figures.insert(
2006        handle,
2007        FigureState {
2008            figure,
2009            ..FigureState::new(handle)
2010        },
2011    );
2012    drop(reg);
2013    notify_with_figure(handle, &figure_clone, FigureEventKind::Created);
2014    handle
2015}
2016
2017pub fn clear_figure(target: Option<FigureHandle>) -> Result<FigureHandle, FigureError> {
2018    let mut reg = registry();
2019    let handle = target.unwrap_or(reg.current);
2020    {
2021        let state = reg
2022            .figures
2023            .get_mut(&handle)
2024            .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2025        *state = FigureState::new(handle);
2026    }
2027    purge_plot_children_for_figure(&mut reg, handle);
2028    let figure_clone = reg
2029        .figures
2030        .get(&handle)
2031        .expect("figure exists")
2032        .figure
2033        .clone();
2034    drop(reg);
2035    notify_with_figure(handle, &figure_clone, FigureEventKind::Cleared);
2036    Ok(handle)
2037}
2038
2039pub fn close_figure(target: Option<FigureHandle>) -> Result<FigureHandle, FigureError> {
2040    let mut reg = registry();
2041    let handle = target.unwrap_or(reg.current);
2042    let existed = reg.figures.remove(&handle);
2043    if existed.is_none() {
2044        return Err(FigureError::InvalidHandle(handle.as_u32()));
2045    }
2046    purge_plot_children_for_figure(&mut reg, handle);
2047
2048    if reg.current == handle {
2049        if let Some((&next_handle, _)) = reg.figures.iter().next() {
2050            reg.current = next_handle;
2051        } else {
2052            let default = FigureHandle::default();
2053            reg.current = default;
2054            reg.next_handle = default.next();
2055            drop(reg);
2056            notify_without_figure(handle, FigureEventKind::Closed);
2057            return Ok(handle);
2058        }
2059    }
2060
2061    drop(reg);
2062    notify_without_figure(handle, FigureEventKind::Closed);
2063    Ok(handle)
2064}
2065
2066#[derive(Clone)]
2067pub struct PlotRenderOptions<'a> {
2068    pub title: &'a str,
2069    pub x_label: &'a str,
2070    pub y_label: &'a str,
2071    pub grid: bool,
2072    pub axis_equal: bool,
2073}
2074
2075impl<'a> Default for PlotRenderOptions<'a> {
2076    fn default() -> Self {
2077        Self {
2078            title: "",
2079            x_label: "X",
2080            y_label: "Y",
2081            grid: true,
2082            axis_equal: false,
2083        }
2084    }
2085}
2086
2087pub enum HoldMode {
2088    On,
2089    Off,
2090    Toggle,
2091}
2092
2093pub fn set_hold(mode: HoldMode) -> bool {
2094    let mut reg = registry();
2095    let handle = reg.current;
2096    let state = get_state_mut(&mut reg, handle);
2097    let current = state.hold();
2098    let new_value = match mode {
2099        HoldMode::On => true,
2100        HoldMode::Off => false,
2101        HoldMode::Toggle => !current,
2102    };
2103    state.set_hold(new_value);
2104    new_value
2105}
2106
2107pub fn configure_subplot(rows: usize, cols: usize, index: usize) -> Result<(), FigureError> {
2108    if rows == 0 || cols == 0 {
2109        return Err(FigureError::InvalidSubplotGrid { rows, cols });
2110    }
2111    let total_axes = rows
2112        .checked_mul(cols)
2113        .ok_or(FigureError::InvalidSubplotGrid { rows, cols })?;
2114    if index >= total_axes {
2115        return Err(FigureError::InvalidSubplotIndex { rows, cols, index });
2116    }
2117    let mut reg = registry();
2118    let handle = reg.current;
2119    let state = get_state_mut(&mut reg, handle);
2120    state.figure.set_subplot_grid(rows, cols);
2121    state.active_axes = index;
2122    state.figure.set_active_axes_index(index);
2123    Ok(())
2124}
2125
2126pub fn render_active_plot<F>(
2127    builtin: &'static str,
2128    opts: PlotRenderOptions<'_>,
2129    mut apply: F,
2130) -> BuiltinResult<String>
2131where
2132    F: FnMut(&mut Figure, usize) -> BuiltinResult<()>,
2133{
2134    let rendering_disabled = interactive_rendering_disabled();
2135    let host_managed_rendering = host_managed_rendering_enabled();
2136    let (handle, figure_clone) = {
2137        let mut reg = registry();
2138        let handle = reg.current;
2139        let axes_index = { get_state_mut(&mut reg, handle).active_axes };
2140        let should_clear = { !get_state_mut(&mut reg, handle).hold() };
2141        {
2142            let state = get_state_mut(&mut reg, handle);
2143            state.figure.set_active_axes_index(axes_index);
2144            if should_clear {
2145                state.figure.clear_axes(axes_index);
2146                state.reset_cycle(axes_index);
2147            }
2148        }
2149        if should_clear {
2150            purge_plot_children_for_axes(&mut reg, handle, axes_index);
2151        }
2152        {
2153            let state = get_state_mut(&mut reg, handle);
2154            if !opts.title.is_empty() {
2155                state.figure.set_axes_title(axes_index, opts.title);
2156            }
2157            if !opts.x_label.is_empty() || !opts.y_label.is_empty() {
2158                state
2159                    .figure
2160                    .set_axes_labels(axes_index, opts.x_label, opts.y_label);
2161            }
2162            state.figure.set_grid(opts.grid);
2163            state.figure.set_axis_equal(opts.axis_equal);
2164
2165            let _axes_context = AxesContextGuard::install(state, axes_index);
2166            apply(&mut state.figure, axes_index)
2167                .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
2168
2169            // Increment revision after a successful mutation so surfaces can avoid
2170            // re-rendering unchanged figures when "presenting" an already-loaded handle.
2171            state.revision = state.revision.wrapping_add(1);
2172        }
2173        let figure_clone = reg
2174            .figures
2175            .get(&handle)
2176            .expect("figure exists")
2177            .figure
2178            .clone();
2179        (handle, figure_clone)
2180    };
2181    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2182
2183    if rendering_disabled {
2184        if host_managed_rendering {
2185            return Ok(format!("Figure {} updated", handle.as_u32()));
2186        }
2187        return Err(plotting_error(builtin, ERR_PLOTTING_UNAVAILABLE));
2188    }
2189
2190    if host_managed_rendering {
2191        return Ok(format!("Figure {} updated", handle.as_u32()));
2192    }
2193
2194    // On Web/WASM we deliberately decouple "mutate figure state" from "present pixels".
2195    // The host coalesces figure events and presents on a frame cadence, and `drawnow()` /
2196    // `pause()` provide explicit "flush" boundaries for scripts.
2197    #[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
2198    {
2199        let _ = figure_clone;
2200        Ok(format!("Figure {} updated", handle.as_u32()))
2201    }
2202
2203    #[cfg(not(all(target_arch = "wasm32", feature = "plot-web")))]
2204    {
2205        let rendered = render_figure(handle, figure_clone)
2206            .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
2207        Ok(format!("Figure {} updated: {rendered}", handle.as_u32()))
2208    }
2209}
2210
2211pub fn append_active_plot<F>(
2212    builtin: &'static str,
2213    opts: PlotRenderOptions<'_>,
2214    mut apply: F,
2215) -> BuiltinResult<String>
2216where
2217    F: FnMut(&mut Figure, usize) -> BuiltinResult<()>,
2218{
2219    let rendering_disabled = interactive_rendering_disabled();
2220    let host_managed_rendering = host_managed_rendering_enabled();
2221    let (handle, figure_clone) = {
2222        let mut reg = registry();
2223        let handle = reg.current;
2224        let axes_index = { get_state_mut(&mut reg, handle).active_axes };
2225        {
2226            let state = get_state_mut(&mut reg, handle);
2227            state.figure.set_active_axes_index(axes_index);
2228            if !opts.title.is_empty() {
2229                state.figure.set_axes_title(axes_index, opts.title);
2230            }
2231            if !opts.x_label.is_empty() || !opts.y_label.is_empty() {
2232                state
2233                    .figure
2234                    .set_axes_labels(axes_index, opts.x_label, opts.y_label);
2235            }
2236            state.figure.set_grid(opts.grid);
2237            state.figure.set_axis_equal(opts.axis_equal);
2238
2239            let _axes_context = AxesContextGuard::install(state, axes_index);
2240            apply(&mut state.figure, axes_index)
2241                .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
2242            state.revision = state.revision.wrapping_add(1);
2243        }
2244        let figure_clone = reg
2245            .figures
2246            .get(&handle)
2247            .expect("figure exists")
2248            .figure
2249            .clone();
2250        (handle, figure_clone)
2251    };
2252    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2253
2254    if rendering_disabled {
2255        if host_managed_rendering {
2256            return Ok(format!("Figure {} updated", handle.as_u32()));
2257        }
2258        return Err(plotting_error(builtin, ERR_PLOTTING_UNAVAILABLE));
2259    }
2260
2261    if host_managed_rendering {
2262        return Ok(format!("Figure {} updated", handle.as_u32()));
2263    }
2264
2265    #[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
2266    {
2267        let _ = figure_clone;
2268        Ok(format!("Figure {} updated", handle.as_u32()))
2269    }
2270
2271    #[cfg(not(all(target_arch = "wasm32", feature = "plot-web")))]
2272    {
2273        let rendered = render_figure(handle, figure_clone)
2274            .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
2275        Ok(format!("Figure {} updated: {rendered}", handle.as_u32()))
2276    }
2277}
2278
2279/// Monotonic revision counter that increments on each successful mutation of the figure.
2280/// Used by web surface presentation logic to avoid redundant `render_figure` calls when
2281/// a surface is already up-to-date for a handle.
2282#[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
2283pub fn current_figure_revision(handle: FigureHandle) -> Option<u64> {
2284    let reg = registry();
2285    reg.figures.get(&handle).map(|state| state.revision)
2286}
2287
2288fn interactive_rendering_disabled() -> bool {
2289    std::env::var_os("RUNMAT_DISABLE_INTERACTIVE_PLOTS").is_some()
2290}
2291
2292fn host_managed_rendering_enabled() -> bool {
2293    std::env::var_os("RUNMAT_HOST_MANAGED_PLOTS").is_some()
2294}
2295
2296#[cfg(test)]
2297pub(crate) fn disable_rendering_for_tests() {
2298    static INIT: Once = Once::new();
2299    INIT.call_once(|| unsafe {
2300        std::env::set_var("RUNMAT_DISABLE_INTERACTIVE_PLOTS", "1");
2301    });
2302}
2303
2304pub fn set_line_style_order_for_axes(axes_index: usize, order: &[LineStyle]) {
2305    if with_active_cycle(axes_index, |cycle| cycle.set_order(order)).is_some() {
2306        return;
2307    }
2308    let mut reg = registry();
2309    let handle = reg.current;
2310    let state = get_state_mut(&mut reg, handle);
2311    state.cycle_for_axes_mut(axes_index).set_order(order);
2312}
2313
2314pub fn next_line_style_for_axes(axes_index: usize) -> LineStyle {
2315    if let Some(style) = with_active_cycle(axes_index, |cycle| cycle.next()) {
2316        return style;
2317    }
2318    let mut reg = registry();
2319    let handle = reg.current;
2320    let state = get_state_mut(&mut reg, handle);
2321    state.cycle_for_axes_mut(axes_index).next()
2322}
2323
2324#[cfg(test)]
2325mod tests {
2326    use super::*;
2327    use crate::builtins::plotting::tests::ensure_plot_test_env;
2328
2329    #[cfg(test)]
2330    pub(crate) fn reset_for_tests() {
2331        let mut reg = registry();
2332        reg.figures.clear();
2333        reg.current = FigureHandle::default();
2334        reg.next_handle = FigureHandle::default().next();
2335    }
2336
2337    #[test]
2338    fn closing_last_figure_leaves_no_visible_figures() {
2339        let _guard = lock_plot_test_registry();
2340        ensure_plot_test_env();
2341        reset_for_tests();
2342
2343        let handle = new_figure_handle();
2344        assert_eq!(figure_handles(), vec![handle]);
2345
2346        close_figure(Some(handle)).expect("close figure");
2347
2348        assert!(
2349            figure_handles().is_empty(),
2350            "closing the last figure should not recreate a default visible figure"
2351        );
2352    }
2353}