1pub mod context;
10pub mod core;
11pub mod data;
12pub mod event;
13pub mod geometry;
14pub mod gpu;
15
16pub mod plots;
18
19pub mod export;
21
22pub use context::{install_shared_wgpu_context, shared_wgpu_context, SharedWgpuContext};
23
24#[cfg(feature = "gui")]
26pub mod gui;
27
28#[cfg(feature = "egui-overlay")]
30pub mod overlay;
31
32#[cfg(all(target_arch = "wasm32", feature = "web"))]
34pub mod web;
35
36pub mod styling;
38
39pub use core::scene::GpuVertexBuffer;
42
43pub 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#[cfg(feature = "gui")]
57pub use gui::{PlotWindow, WindowConfig};
58
59#[cfg(feature = "gui")]
61pub use gui::{is_window_available, show_plot_sequential};
62
63#[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
70pub use export::image::*;
73
74#[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], }
93 }
94}
95
96pub 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 render_figure_to_file(figure, path)
108 }
109 None => {
110 #[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#[cfg(not(target_arch = "wasm32"))]
134fn render_figure_to_file(figure: plots::Figure, path: &str) -> Result<String, String> {
135 use crate::export::ImageExporter;
136 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
152pub 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), 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
181pub 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), 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
212pub 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); 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
239pub 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 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 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
301pub fn show_interactive_platform_optimal(figure: plots::Figure) -> Result<String, String> {
306 render_interactive_with_handle(0, figure)
307}
308
309pub 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}