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 reset_recent_figures() {
1834 RECENT_FIGURES.with(|set| set.borrow_mut().clear());
1835}
1836
1837pub fn reset_plot_state() {
1838 {
1839 let mut reg = registry();
1840 *reg = PlotRegistry::default();
1841 }
1842 reset_recent_figures();
1843}
1844
1845pub fn take_recent_figures() -> Vec<FigureHandle> {
1846 RECENT_FIGURES.with(|set| set.borrow_mut().drain().collect())
1847}
1848
1849pub fn select_figure(handle: FigureHandle) {
1850 let mut reg = registry();
1851 reg.current = handle;
1852 let maybe_new = match reg.figures.entry(handle) {
1853 Entry::Occupied(entry) => {
1854 let _ = entry.into_mut();
1855 None
1856 }
1857 Entry::Vacant(vacant) => {
1858 let state = vacant.insert(FigureState::new(handle));
1859 Some(state.figure.clone())
1860 }
1861 };
1862 drop(reg);
1863 if let Some(figure_clone) = maybe_new {
1864 notify_with_figure(handle, &figure_clone, FigureEventKind::Created);
1865 }
1866}
1867
1868pub fn new_figure_handle() -> FigureHandle {
1869 let mut reg = registry();
1870 let handle = reg.next_handle;
1871 reg.next_handle = reg.next_handle.next();
1872 reg.current = handle;
1873 let figure_clone = {
1874 let state = get_state_mut(&mut reg, handle);
1875 state.figure.clone()
1876 };
1877 drop(reg);
1878 notify_with_figure(handle, &figure_clone, FigureEventKind::Created);
1879 handle
1880}
1881
1882pub fn current_figure_handle() -> FigureHandle {
1883 registry().current
1884}
1885
1886pub fn current_axes_state() -> FigureAxesState {
1887 let mut reg = registry();
1888 let handle = reg.current;
1889 let state = get_state_mut(&mut reg, handle);
1891 FigureAxesState {
1892 handle,
1893 rows: state.figure.axes_rows.max(1),
1894 cols: state.figure.axes_cols.max(1),
1895 active_index: state.active_axes,
1896 }
1897}
1898
1899pub fn axes_handle_exists(handle: FigureHandle, axes_index: usize) -> bool {
1900 let mut reg = registry();
1901 let state = get_state_mut(&mut reg, handle);
1902 let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1903 axes_index < total_axes
1904}
1905
1906pub fn figure_handle_exists(handle: FigureHandle) -> bool {
1907 let reg = registry();
1908 reg.figures.contains_key(&handle)
1909}
1910
1911pub fn axes_metadata_snapshot(
1912 handle: FigureHandle,
1913 axes_index: usize,
1914) -> Result<runmat_plot::plots::AxesMetadata, FigureError> {
1915 let mut reg = registry();
1916 let state = get_state_mut(&mut reg, handle);
1917 let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1918 if axes_index >= total_axes {
1919 return Err(FigureError::InvalidSubplotIndex {
1920 rows: state.figure.axes_rows.max(1),
1921 cols: state.figure.axes_cols.max(1),
1922 index: axes_index,
1923 });
1924 }
1925 state
1926 .figure
1927 .axes_metadata(axes_index)
1928 .cloned()
1929 .ok_or(FigureError::InvalidAxesHandle)
1930}
1931
1932pub fn axes_state_snapshot(
1933 handle: FigureHandle,
1934 axes_index: usize,
1935) -> Result<FigureAxesState, FigureError> {
1936 let mut reg = registry();
1937 let state = get_state_mut(&mut reg, handle);
1938 let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1939 if axes_index >= total_axes {
1940 return Err(FigureError::InvalidSubplotIndex {
1941 rows: state.figure.axes_rows.max(1),
1942 cols: state.figure.axes_cols.max(1),
1943 index: axes_index,
1944 });
1945 }
1946 Ok(FigureAxesState {
1947 handle,
1948 rows: state.figure.axes_rows.max(1),
1949 cols: state.figure.axes_cols.max(1),
1950 active_index: axes_index,
1951 })
1952}
1953
1954pub fn current_axes_handle_for_figure(handle: FigureHandle) -> Result<f64, FigureError> {
1955 let mut reg = registry();
1956 let state = get_state_mut(&mut reg, handle);
1957 Ok(encode_axes_handle(handle, state.active_axes))
1958}
1959
1960pub fn axes_handles_for_figure(handle: FigureHandle) -> Result<Vec<f64>, FigureError> {
1961 let mut reg = registry();
1962 let state = get_state_mut(&mut reg, handle);
1963 let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1964 Ok((0..total_axes)
1965 .map(|idx| encode_axes_handle(handle, idx))
1966 .collect())
1967}
1968
1969pub fn select_axes_for_figure(handle: FigureHandle, axes_index: usize) -> Result<(), FigureError> {
1970 let mut reg = registry();
1971 let state = get_state_mut(&mut reg, handle);
1972 let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1973 if axes_index >= total_axes {
1974 return Err(FigureError::InvalidSubplotIndex {
1975 rows: state.figure.axes_rows.max(1),
1976 cols: state.figure.axes_cols.max(1),
1977 index: axes_index,
1978 });
1979 }
1980 reg.current = handle;
1981 let state = get_state_mut(&mut reg, handle);
1982 state.active_axes = axes_index;
1983 state.figure.set_active_axes_index(axes_index);
1984 Ok(())
1985}
1986
1987fn with_axes_target_mut<R>(
1988 handle: FigureHandle,
1989 axes_index: usize,
1990 f: impl FnOnce(&mut FigureState) -> R,
1991) -> Result<(R, Figure), FigureError> {
1992 let mut reg = registry();
1993 let state = get_state_mut(&mut reg, handle);
1994 let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1995 if axes_index >= total_axes {
1996 return Err(FigureError::InvalidSubplotIndex {
1997 rows: state.figure.axes_rows.max(1),
1998 cols: state.figure.axes_cols.max(1),
1999 index: axes_index,
2000 });
2001 }
2002 state.active_axes = axes_index;
2003 state.figure.set_active_axes_index(axes_index);
2004 let result = f(state);
2005 state.revision = state.revision.wrapping_add(1);
2006 Ok((result, state.figure.clone()))
2007}
2008
2009fn with_figure_mut<R>(
2010 handle: FigureHandle,
2011 f: impl FnOnce(&mut FigureState) -> R,
2012) -> Result<(R, Figure), FigureError> {
2013 let mut reg = registry();
2014 let state = get_state_mut(&mut reg, handle);
2015 let result = f(state);
2016 state.revision = state.revision.wrapping_add(1);
2017 Ok((result, state.figure.clone()))
2018}
2019
2020pub fn current_hold_enabled() -> bool {
2021 let mut reg = registry();
2022 let handle = reg.current;
2023 let state = get_state_mut(&mut reg, handle);
2025 *state
2026 .hold_per_axes
2027 .get(&state.active_axes)
2028 .unwrap_or(&false)
2029}
2030
2031pub fn reset_hold_state_for_run() {
2037 let mut reg = registry();
2038 for state in reg.figures.values_mut() {
2039 state.hold_per_axes.clear();
2040 }
2041}
2042
2043pub fn figure_handles() -> Vec<FigureHandle> {
2044 let reg = registry();
2045 reg.figures.keys().copied().collect()
2046}
2047
2048pub fn clone_figure(handle: FigureHandle) -> Option<Figure> {
2049 let reg = registry();
2050 reg.figures.get(&handle).map(|state| state.figure.clone())
2051}
2052
2053pub fn figure_has_sg_title(handle: FigureHandle) -> bool {
2054 let reg = registry();
2055 reg.figures
2056 .get(&handle)
2057 .map(|state| state.figure.sg_title.is_some())
2058 .unwrap_or(false)
2059}
2060
2061pub fn import_figure(figure: Figure) -> FigureHandle {
2062 let mut reg = registry();
2063 let handle = reg.next_handle;
2064 reg.next_handle = reg.next_handle.next();
2065 reg.current = handle;
2066 let figure_clone = figure.clone();
2067 reg.figures.insert(
2068 handle,
2069 FigureState {
2070 figure,
2071 ..FigureState::new(handle)
2072 },
2073 );
2074 drop(reg);
2075 notify_with_figure(handle, &figure_clone, FigureEventKind::Created);
2076 handle
2077}
2078
2079pub fn clear_figure(target: Option<FigureHandle>) -> Result<FigureHandle, FigureError> {
2080 let mut reg = registry();
2081 let handle = target.unwrap_or(reg.current);
2082 {
2083 let state = reg
2084 .figures
2085 .get_mut(&handle)
2086 .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2087 *state = FigureState::new(handle);
2088 }
2089 purge_plot_children_for_figure(&mut reg, handle);
2090 let figure_clone = reg
2091 .figures
2092 .get(&handle)
2093 .expect("figure exists")
2094 .figure
2095 .clone();
2096 drop(reg);
2097 notify_with_figure(handle, &figure_clone, FigureEventKind::Cleared);
2098 Ok(handle)
2099}
2100
2101pub fn close_figure(target: Option<FigureHandle>) -> Result<FigureHandle, FigureError> {
2102 let mut reg = registry();
2103 let handle = target.unwrap_or(reg.current);
2104 let existed = reg.figures.remove(&handle);
2105 if existed.is_none() {
2106 return Err(FigureError::InvalidHandle(handle.as_u32()));
2107 }
2108 purge_plot_children_for_figure(&mut reg, handle);
2109
2110 if reg.current == handle {
2111 if let Some((&next_handle, _)) = reg.figures.iter().next() {
2112 reg.current = next_handle;
2113 } else {
2114 let default = FigureHandle::default();
2115 reg.current = default;
2116 reg.next_handle = default.next();
2117 drop(reg);
2118 notify_without_figure(handle, FigureEventKind::Closed);
2119 return Ok(handle);
2120 }
2121 }
2122
2123 drop(reg);
2124 notify_without_figure(handle, FigureEventKind::Closed);
2125 Ok(handle)
2126}
2127
2128#[derive(Clone)]
2129pub struct PlotRenderOptions<'a> {
2130 pub title: &'a str,
2131 pub x_label: &'a str,
2132 pub y_label: &'a str,
2133 pub grid: bool,
2134 pub axis_equal: bool,
2135}
2136
2137impl<'a> Default for PlotRenderOptions<'a> {
2138 fn default() -> Self {
2139 Self {
2140 title: "",
2141 x_label: "X",
2142 y_label: "Y",
2143 grid: true,
2144 axis_equal: false,
2145 }
2146 }
2147}
2148
2149pub enum HoldMode {
2150 On,
2151 Off,
2152 Toggle,
2153}
2154
2155pub fn set_hold(mode: HoldMode) -> bool {
2156 let mut reg = registry();
2157 let handle = reg.current;
2158 let state = get_state_mut(&mut reg, handle);
2159 let current = state.hold();
2160 let new_value = match mode {
2161 HoldMode::On => true,
2162 HoldMode::Off => false,
2163 HoldMode::Toggle => !current,
2164 };
2165 state.set_hold(new_value);
2166 new_value
2167}
2168
2169pub fn configure_subplot(rows: usize, cols: usize, index: usize) -> Result<(), FigureError> {
2170 if rows == 0 || cols == 0 {
2171 return Err(FigureError::InvalidSubplotGrid { rows, cols });
2172 }
2173 let total_axes = rows
2174 .checked_mul(cols)
2175 .ok_or(FigureError::InvalidSubplotGrid { rows, cols })?;
2176 if index >= total_axes {
2177 return Err(FigureError::InvalidSubplotIndex { rows, cols, index });
2178 }
2179 let mut reg = registry();
2180 let handle = reg.current;
2181 let state = get_state_mut(&mut reg, handle);
2182 state.figure.set_subplot_grid(rows, cols);
2183 state.active_axes = index;
2184 state.figure.set_active_axes_index(index);
2185 Ok(())
2186}
2187
2188pub fn render_active_plot<F>(
2189 builtin: &'static str,
2190 opts: PlotRenderOptions<'_>,
2191 mut apply: F,
2192) -> BuiltinResult<String>
2193where
2194 F: FnMut(&mut Figure, usize) -> BuiltinResult<()>,
2195{
2196 let rendering_disabled = interactive_rendering_disabled();
2197 let host_managed_rendering = host_managed_rendering_enabled();
2198 let (handle, figure_clone) = {
2199 let mut reg = registry();
2200 let handle = reg.current;
2201 let axes_index = { get_state_mut(&mut reg, handle).active_axes };
2202 let should_clear = { !get_state_mut(&mut reg, handle).hold() };
2203 {
2204 let state = get_state_mut(&mut reg, handle);
2205 state.figure.set_active_axes_index(axes_index);
2206 if should_clear {
2207 state.figure.clear_axes(axes_index);
2208 state.reset_cycle(axes_index);
2209 }
2210 }
2211 if should_clear {
2212 purge_plot_children_for_axes(&mut reg, handle, axes_index);
2213 }
2214 {
2215 let state = get_state_mut(&mut reg, handle);
2216 if !opts.title.is_empty() {
2217 state.figure.set_axes_title(axes_index, opts.title);
2218 }
2219 if !opts.x_label.is_empty() || !opts.y_label.is_empty() {
2220 state
2221 .figure
2222 .set_axes_labels(axes_index, opts.x_label, opts.y_label);
2223 }
2224 state.figure.set_grid(opts.grid);
2225 state.figure.set_axis_equal(opts.axis_equal);
2226
2227 let _axes_context = AxesContextGuard::install(state, axes_index);
2228 apply(&mut state.figure, axes_index)
2229 .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
2230
2231 state.revision = state.revision.wrapping_add(1);
2234 }
2235 let figure_clone = reg
2236 .figures
2237 .get(&handle)
2238 .expect("figure exists")
2239 .figure
2240 .clone();
2241 (handle, figure_clone)
2242 };
2243 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2244
2245 if rendering_disabled {
2246 if host_managed_rendering {
2247 return Ok(format!("Figure {} updated", handle.as_u32()));
2248 }
2249 return Err(plotting_error(builtin, ERR_PLOTTING_UNAVAILABLE));
2250 }
2251
2252 if host_managed_rendering {
2253 return Ok(format!("Figure {} updated", handle.as_u32()));
2254 }
2255
2256 #[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
2260 {
2261 let _ = figure_clone;
2262 Ok(format!("Figure {} updated", handle.as_u32()))
2263 }
2264
2265 #[cfg(not(all(target_arch = "wasm32", feature = "plot-web")))]
2266 {
2267 let rendered = render_figure(handle, figure_clone)
2268 .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
2269 Ok(format!("Figure {} updated: {rendered}", handle.as_u32()))
2270 }
2271}
2272
2273pub fn append_active_plot<F>(
2274 builtin: &'static str,
2275 opts: PlotRenderOptions<'_>,
2276 mut apply: F,
2277) -> BuiltinResult<String>
2278where
2279 F: FnMut(&mut Figure, usize) -> BuiltinResult<()>,
2280{
2281 let rendering_disabled = interactive_rendering_disabled();
2282 let host_managed_rendering = host_managed_rendering_enabled();
2283 let (handle, figure_clone) = {
2284 let mut reg = registry();
2285 let handle = reg.current;
2286 let axes_index = { get_state_mut(&mut reg, handle).active_axes };
2287 {
2288 let state = get_state_mut(&mut reg, handle);
2289 state.figure.set_active_axes_index(axes_index);
2290 if !opts.title.is_empty() {
2291 state.figure.set_axes_title(axes_index, opts.title);
2292 }
2293 if !opts.x_label.is_empty() || !opts.y_label.is_empty() {
2294 state
2295 .figure
2296 .set_axes_labels(axes_index, opts.x_label, opts.y_label);
2297 }
2298 state.figure.set_grid(opts.grid);
2299 state.figure.set_axis_equal(opts.axis_equal);
2300
2301 let _axes_context = AxesContextGuard::install(state, axes_index);
2302 apply(&mut state.figure, axes_index)
2303 .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
2304 state.revision = state.revision.wrapping_add(1);
2305 }
2306 let figure_clone = reg
2307 .figures
2308 .get(&handle)
2309 .expect("figure exists")
2310 .figure
2311 .clone();
2312 (handle, figure_clone)
2313 };
2314 notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2315
2316 if rendering_disabled {
2317 if host_managed_rendering {
2318 return Ok(format!("Figure {} updated", handle.as_u32()));
2319 }
2320 return Err(plotting_error(builtin, ERR_PLOTTING_UNAVAILABLE));
2321 }
2322
2323 if host_managed_rendering {
2324 return Ok(format!("Figure {} updated", handle.as_u32()));
2325 }
2326
2327 #[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
2328 {
2329 let _ = figure_clone;
2330 Ok(format!("Figure {} updated", handle.as_u32()))
2331 }
2332
2333 #[cfg(not(all(target_arch = "wasm32", feature = "plot-web")))]
2334 {
2335 let rendered = render_figure(handle, figure_clone)
2336 .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
2337 Ok(format!("Figure {} updated: {rendered}", handle.as_u32()))
2338 }
2339}
2340
2341pub fn current_figure_revision(handle: FigureHandle) -> Option<u64> {
2345 let reg = registry();
2346 reg.figures.get(&handle).map(|state| state.revision)
2347}
2348
2349fn interactive_rendering_disabled() -> bool {
2350 std::env::var_os("RUNMAT_DISABLE_INTERACTIVE_PLOTS").is_some()
2351}
2352
2353fn host_managed_rendering_enabled() -> bool {
2354 std::env::var_os("RUNMAT_HOST_MANAGED_PLOTS").is_some()
2355}
2356
2357#[cfg(test)]
2358pub(crate) fn disable_rendering_for_tests() {
2359 static INIT: Once = Once::new();
2360 INIT.call_once(|| unsafe {
2361 std::env::set_var("RUNMAT_DISABLE_INTERACTIVE_PLOTS", "1");
2362 });
2363}
2364
2365pub fn set_line_style_order_for_axes(axes_index: usize, order: &[LineStyle]) {
2366 if with_active_style_cycle(axes_index, |cycle| cycle.set_order(order)).is_some() {
2367 return;
2368 }
2369 let mut reg = registry();
2370 let handle = reg.current;
2371 let state = get_state_mut(&mut reg, handle);
2372 state.cycle_for_axes_mut(axes_index).set_order(order);
2373}
2374
2375pub fn next_line_style_for_axes(axes_index: usize) -> LineStyle {
2376 if let Some(style) = with_active_style_cycle(axes_index, |cycle| cycle.next()) {
2377 return style;
2378 }
2379 let mut reg = registry();
2380 let handle = reg.current;
2381 let state = get_state_mut(&mut reg, handle);
2382 state.cycle_for_axes_mut(axes_index).next()
2383}
2384
2385pub fn line_color_for_series_index(series_index: usize) -> Vec4 {
2386 let theme = current_plot_theme_config().build_theme();
2387 theme.get_data_color(series_index)
2388}
2389
2390pub fn next_line_color_for_axes(axes_index: usize) -> Vec4 {
2391 if let Some(color) = with_active_color_cycle(axes_index, |cycle| cycle.next()) {
2392 return color;
2393 }
2394 let mut reg = registry();
2395 let handle = reg.current;
2396 let state = get_state_mut(&mut reg, handle);
2397 state.color_cycle_for_axes_mut(axes_index).next()
2398}
2399
2400#[cfg(test)]
2401mod tests {
2402 use super::*;
2403 use crate::builtins::plotting::tests::ensure_plot_test_env;
2404
2405 #[cfg(test)]
2406 pub(crate) fn reset_for_tests() {
2407 let mut reg = registry();
2408 reg.figures.clear();
2409 reg.current = FigureHandle::default();
2410 reg.next_handle = FigureHandle::default().next();
2411 }
2412
2413 #[test]
2414 fn closing_last_figure_leaves_no_visible_figures() {
2415 let _guard = lock_plot_test_registry();
2416 ensure_plot_test_env();
2417 reset_for_tests();
2418
2419 let handle = new_figure_handle();
2420 assert_eq!(figure_handles(), vec![handle]);
2421
2422 close_figure(Some(handle)).expect("close figure");
2423
2424 assert!(
2425 figure_handles().is_empty(),
2426 "closing the last figure should not recreate a default visible figure"
2427 );
2428 }
2429}