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