1pub mod context;
10pub mod core;
11pub mod data;
12pub mod event;
13pub mod geometry;
14pub mod gpu;
15pub(crate) mod wgpu_compat;
16
17pub mod plots;
19
20pub mod export;
22
23pub use context::{install_shared_wgpu_context, shared_wgpu_context, SharedWgpuContext};
24
25#[cfg(all(feature = "gui", not(target_arch = "wasm32")))]
27pub mod gui;
28
29#[cfg(feature = "egui-overlay")]
31pub mod overlay;
32
33#[cfg(all(target_arch = "wasm32", feature = "web"))]
35pub mod web;
36
37pub mod styling;
39
40pub use core::scene::GpuVertexBuffer;
43
44pub 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#[cfg(all(feature = "gui", not(target_arch = "wasm32")))]
58pub use gui::{PlotWindow, WindowConfig};
59
60#[cfg(all(feature = "gui", not(target_arch = "wasm32")))]
62pub use gui::{is_window_available, show_plot_sequential};
63
64#[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
71pub use export::image::*;
74
75#[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], }
94 }
95}
96
97pub 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 render_figure_to_file(figure, path)
109 }
110 None => {
111 #[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#[cfg(not(target_arch = "wasm32"))]
138fn render_figure_to_file(figure: plots::Figure, path: &str) -> Result<String, String> {
139 use crate::export::ImageExporter;
140 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
156pub 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), 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
185pub 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), 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
216pub 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); 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
243pub 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 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 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
305pub fn show_interactive_platform_optimal(figure: plots::Figure) -> Result<String, String> {
310 render_interactive_with_handle(0, figure)
311}
312
313pub 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}