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