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