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