1pub 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
18pub mod plots;
20
21pub mod export;
23
24pub use context::{install_shared_wgpu_context, shared_wgpu_context, SharedWgpuContext};
25
26#[cfg(all(feature = "gui", not(target_arch = "wasm32")))]
28pub mod gui;
29
30#[cfg(feature = "egui-overlay")]
32pub mod overlay;
33
34#[cfg(all(target_arch = "wasm32", feature = "web"))]
36pub mod web;
37
38pub mod styling;
40
41pub use core::scene::GpuVertexBuffer;
44
45pub 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#[cfg(all(feature = "gui", not(target_arch = "wasm32")))]
67pub use gui::{PlotWindow, WindowConfig};
68
69#[cfg(all(feature = "gui", not(target_arch = "wasm32")))]
71pub use gui::{is_window_available, show_plot_sequential};
72
73#[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
80pub use export::image::*;
83
84#[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], }
103 }
104}
105
106pub 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 render_figure_to_file(figure, path)
118 }
119 None => {
120 #[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#[cfg(not(target_arch = "wasm32"))]
147fn render_figure_to_file(figure: plots::Figure, path: &str) -> Result<String, String> {
148 use crate::export::ImageExporter;
149 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
165pub 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), 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
194pub 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), 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
225pub 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); 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
252pub 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 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 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
314pub fn show_interactive_platform_optimal(figure: plots::Figure) -> Result<String, String> {
319 render_interactive_with_handle(0, figure)
320}
321
322pub 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}