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