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