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