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