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