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