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