Skip to main content

runmat_plot/
lib.rs

1//! RunMat Plot - World-class interactive plotting library
2//!
3//! High-performance GPU-accelerated plotting.
4//! Unified rendering pipeline for both interactive and static export.
5
6// ===== CORE ARCHITECTURE =====
7
8// Core rendering engine (always available)
9pub mod context;
10pub mod core;
11pub mod data;
12pub mod event;
13pub mod geometry;
14pub mod geometry_scene;
15pub mod gpu;
16pub(crate) mod wgpu_compat;
17
18// High-level plot types and figures
19pub mod plots;
20
21// Export capabilities
22pub mod export;
23
24pub use context::{install_shared_wgpu_context, shared_wgpu_context, SharedWgpuContext};
25
26// Native GUI system (when enabled on desktop targets)
27#[cfg(all(feature = "gui", not(target_arch = "wasm32")))]
28pub mod gui;
29
30// Egui overlay rendering (usable without winit, including wasm)
31#[cfg(feature = "egui-overlay")]
32pub mod overlay;
33
34// WASM/WebGPU bridge
35#[cfg(all(target_arch = "wasm32", feature = "web"))]
36pub mod web;
37
38// Styling and themes
39pub mod styling;
40
41// ===== PUBLIC API =====
42
43pub use core::scene::GpuVertexBuffer;
44
45// Core plot types
46// Avoid ambiguous re-exports: explicitly export plot types
47pub use event::{
48    FigureEvent, FigureEventKind, FigureLayout, FigureLegendEntry, FigureMetadata, FigureScene,
49    FigureSnapshot, PlotDescriptor, PlotKind,
50};
51pub use geometry_scene::{
52    cad_default_material, vertex as geometry_scene_vertex, GeometryScene,
53    GeometrySceneAssemblyNode, GeometrySceneCacheKey, GeometrySceneChunk,
54    GeometrySceneCompleteness, GeometrySceneDisplayMode, GeometrySceneOverlay,
55    GeometryScenePickIndex, GeometryScenePickRequest, GeometryScenePickResult,
56    GeometryScenePresentation, GeometrySceneRegion, GeometrySceneRegionAnnotation,
57    GeometrySceneRegionSummary, GeometrySceneTriangleRange,
58};
59pub use plots::{
60    AreaPlot, ContourFillPlot, ContourPlot, Figure, Line3Plot, LinePlot, PieChart, QuiverPlot,
61    ReferenceLine, ReferenceLineOrientation, Scatter3Plot, ScatterPlot, StairsPlot, StemPlot,
62    SurfacePlot,
63};
64
65// High-level API
66#[cfg(all(feature = "gui", not(target_arch = "wasm32")))]
67pub use gui::{PlotWindow, WindowConfig};
68
69// Sequential window manager (V8-caliber EventLoop management)
70#[cfg(all(feature = "gui", not(target_arch = "wasm32")))]
71pub use gui::{is_window_available, show_plot_sequential};
72
73// Robust GUI thread management
74#[cfg(all(feature = "gui", not(target_arch = "wasm32")))]
75pub use gui::{
76    get_gui_manager, health_check_global, initialize_gui_manager, is_main_thread,
77    register_main_thread, show_plot_global, GuiErrorCode, GuiOperationResult, GuiThreadManager,
78};
79
80// Export functionality
81// Explicitly export image exporter to avoid collision with plots::image
82pub use export::image::*;
83
84// ===== UNIFIED PLOTTING FUNCTIONS =====
85
86/// Plot options for customizing output
87#[derive(Debug, Clone)]
88pub struct PlotOptions {
89    pub width: u32,
90    pub height: u32,
91    pub dpi: f32,
92    pub background_color: [f32; 4],
93}
94
95impl Default for PlotOptions {
96    fn default() -> Self {
97        Self {
98            width: 800,
99            height: 600,
100            dpi: 96.0,
101            background_color: [0.0, 0.0, 0.0, 1.0], // Black background
102        }
103    }
104}
105
106/// **UNIFIED PLOTTING FUNCTION** - One path for all plot types
107///
108/// - Interactive mode: Shows GPU-accelerated window
109/// - Static mode: Renders same GPU pipeline to PNG file
110pub fn show_plot_unified(
111    figure: plots::Figure,
112    output_path: Option<&str>,
113) -> Result<String, String> {
114    match output_path {
115        Some(path) => {
116            // Static export: Render using same GPU pipeline and save to file
117            render_figure_to_file(figure, path)
118        }
119        None => {
120            // Interactive mode: Show GPU-accelerated window
121            #[cfg(all(feature = "gui", not(target_arch = "wasm32")))]
122            {
123                if !figure.visible {
124                    return Ok("Figure is hidden".to_string());
125                }
126                #[cfg(target_os = "macos")]
127                {
128                    if !is_main_thread() {
129                        return Err("Interactive plotting is unavailable on macOS when called from a non-main thread. Launch RunMat from the main thread, or use file export APIs for headless rendering.".to_string());
130                    }
131                }
132                show_plot_sequential(figure)
133            }
134            #[cfg(any(not(feature = "gui"), target_arch = "wasm32"))]
135            {
136                Err(
137                    "GUI feature not enabled. Build with --features gui for interactive plotting."
138                        .to_string(),
139                )
140            }
141        }
142    }
143}
144
145/// Render figure to file using the same GPU pipeline as interactive mode
146#[cfg(not(target_arch = "wasm32"))]
147fn render_figure_to_file(figure: plots::Figure, path: &str) -> Result<String, String> {
148    use crate::export::ImageExporter;
149    // Use the headless GPU exporter that shares the same render pipeline
150    let rt =
151        tokio::runtime::Runtime::new().map_err(|e| format!("Failed to create runtime: {e}"))?;
152    rt.block_on(async move {
153        let mut fig = figure.clone();
154        let exporter = ImageExporter::new().await?;
155        exporter.export_png(&mut fig, path).await?;
156        Ok::<_, String>(format!("Saved plot to {path}"))
157    })
158}
159
160#[cfg(target_arch = "wasm32")]
161fn render_figure_to_file(_figure: plots::Figure, _path: &str) -> Result<String, String> {
162    Err("Static image export is not available in wasm builds".to_string())
163}
164
165// ===== BACKWARD COMPATIBILITY API =====
166// Clean, simple functions that all use the unified pipeline
167
168/// Create a line plot - unified pipeline
169pub fn plot_line(xs: &[f64], ys: &[f64], path: &str, _options: PlotOptions) -> Result<(), String> {
170    if xs.len() != ys.len() {
171        return Err("input length mismatch".into());
172    }
173
174    let line_plot = plots::LinePlot::new(xs.to_vec(), ys.to_vec())
175        .map_err(|e| format!("Failed to create line plot: {e}"))?
176        .with_label("Data")
177        .with_style(
178            glam::Vec4::new(0.0, 0.4, 0.8, 1.0), // Blue
179            2.0,
180            plots::LineStyle::Solid,
181        );
182
183    let mut figure = plots::Figure::new()
184        .with_title("Line Plot")
185        .with_labels("X", "Y")
186        .with_grid(true);
187
188    figure.add_line_plot(line_plot);
189
190    show_plot_unified(figure, Some(path))?;
191    Ok(())
192}
193
194/// Create a scatter plot - unified pipeline
195pub fn plot_scatter(
196    xs: &[f64],
197    ys: &[f64],
198    path: &str,
199    _options: PlotOptions,
200) -> Result<(), String> {
201    if xs.len() != ys.len() {
202        return Err("input length mismatch".into());
203    }
204
205    let scatter_plot = plots::ScatterPlot::new(xs.to_vec(), ys.to_vec())
206        .map_err(|e| format!("Failed to create scatter plot: {e}"))?
207        .with_label("Data")
208        .with_style(
209            glam::Vec4::new(0.8, 0.2, 0.2, 1.0), // Red
210            5.0,
211            plots::MarkerStyle::Circle,
212        );
213
214    let mut figure = plots::Figure::new()
215        .with_title("Scatter Plot")
216        .with_labels("X", "Y")
217        .with_grid(true);
218
219    figure.add_scatter_plot(scatter_plot);
220
221    show_plot_unified(figure, Some(path))?;
222    Ok(())
223}
224
225/// Create a bar chart - unified pipeline
226pub fn plot_bar(
227    labels: &[String],
228    values: &[f64],
229    path: &str,
230    _options: PlotOptions,
231) -> Result<(), String> {
232    if labels.len() != values.len() {
233        return Err("labels and values length mismatch".into());
234    }
235
236    let bar_chart = plots::BarChart::new(labels.to_vec(), values.to_vec())
237        .map_err(|e| format!("Failed to create bar chart: {e}"))?
238        .with_label("Values")
239        .with_style(glam::Vec4::new(0.2, 0.6, 0.3, 1.0), 0.8); // Green bars
240
241    let mut figure = plots::Figure::new()
242        .with_title("Bar Chart")
243        .with_labels("Categories", "Values")
244        .with_grid(true);
245
246    figure.add_bar_chart(bar_chart);
247
248    show_plot_unified(figure, Some(path))?;
249    Ok(())
250}
251
252/// Create a histogram - unified pipeline
253pub fn plot_histogram(
254    data: &[f64],
255    bins: usize,
256    path: &str,
257    _options: PlotOptions,
258) -> Result<(), String> {
259    if data.is_empty() {
260        return Err("Cannot create histogram with empty data".to_string());
261    }
262    if bins == 0 {
263        return Err("Number of bins must be greater than zero".to_string());
264    }
265
266    let min_val = data.iter().copied().fold(f64::INFINITY, f64::min);
267    let max_val = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
268    let (min_val, max_val) = if (max_val - min_val).abs() < f64::EPSILON {
269        (min_val - 0.5, max_val + 0.5)
270    } else {
271        (min_val, max_val)
272    };
273    let bin_width = (max_val - min_val) / bins as f64;
274    let edges: Vec<f64> = (0..=bins).map(|i| min_val + i as f64 * bin_width).collect();
275    // Count values
276    let mut counts = vec![0u64; bins];
277    for &v in data {
278        let mut idx = bins;
279        for i in 0..bins {
280            if v >= edges[i] && v < edges[i + 1] {
281                idx = i;
282                break;
283            }
284        }
285        if idx == bins && (v - edges[bins]).abs() < f64::EPSILON {
286            idx = bins - 1;
287        }
288        if idx < bins {
289            counts[idx] += 1;
290        }
291    }
292
293    // Build bar labels and values
294    let labels: Vec<String> = edges
295        .windows(2)
296        .map(|w| format!("[{:.3},{:.3})", w[0], w[1]))
297        .collect();
298    let values: Vec<f64> = counts.into_iter().map(|c| c as f64).collect();
299
300    let bar_chart = plots::BarChart::new(labels, values)
301        .map_err(|e| format!("Failed to create histogram bars: {e}"))?
302        .with_label("Frequency")
303        .with_style(glam::Vec4::new(0.6, 0.3, 0.7, 1.0), 0.9);
304
305    let mut figure = plots::Figure::new()
306        .with_title("Histogram")
307        .with_labels("Values", "Frequency")
308        .with_grid(true);
309    figure.add_bar_chart(bar_chart);
310    show_plot_unified(figure, Some(path))?;
311    Ok(())
312}
313
314// ===== MAIN INTERACTIVE API =====
315
316/// Show an interactive plot with optimal platform compatibility
317/// This is the main entry point used by the runtime
318pub fn show_interactive_platform_optimal(figure: plots::Figure) -> Result<String, String> {
319    render_interactive_with_handle(0, figure)
320}
321
322/// Show an interactive plot that is tied to a specific MATLAB figure handle.
323/// This allows embedding runtimes to request that a window close when the
324/// corresponding figure lifecycle event fires.
325pub fn render_interactive_with_handle(
326    handle: u32,
327    figure: plots::Figure,
328) -> Result<String, String> {
329    #[cfg(all(feature = "gui", not(target_arch = "wasm32")))]
330    {
331        if !figure.visible {
332            if handle == 0 {
333                return Ok("Figure is hidden".to_string());
334            }
335            gui::lifecycle::request_close(handle);
336            return Ok(format!("Figure {handle} is hidden"));
337        }
338        if std::env::var_os("RUNMAT_DISABLE_INTERACTIVE_PLOTS").is_some() {
339            return Err(
340                "Plotting is unavailable in this environment (interactive rendering disabled)."
341                    .to_string(),
342            );
343        }
344        #[cfg(target_os = "macos")]
345        {
346            if !is_main_thread() {
347                return Err("Interactive plotting is unavailable on macOS when called from a non-main thread. Launch RunMat from the main thread, or use file export APIs for headless rendering.".to_string());
348            }
349        }
350        if handle == 0 {
351            show_plot_unified(figure, None)
352        } else {
353            gui::lifecycle::render_figure(handle, figure)
354        }
355    }
356    #[cfg(any(not(feature = "gui"), target_arch = "wasm32"))]
357    {
358        let _ = handle;
359        let _ = figure;
360        Err(
361            "GUI feature not enabled. Build with --features gui for interactive plotting."
362                .to_string(),
363        )
364    }
365}