1pub mod context;
10pub mod core;
11pub mod data;
12pub mod event;
13pub mod gpu;
14
15pub mod plots;
17
18pub mod export;
20
21pub use context::{install_shared_wgpu_context, shared_wgpu_context, SharedWgpuContext};
22
23#[cfg(feature = "gui")]
25pub mod gui;
26
27#[cfg(feature = "egui-overlay")]
29pub mod overlay;
30
31#[cfg(all(target_arch = "wasm32", feature = "web"))]
33pub mod web;
34
35#[cfg(feature = "jupyter")]
37pub mod jupyter;
38
39pub mod styling;
41
42pub use core::scene::GpuVertexBuffer;
45
46pub 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#[cfg(feature = "gui")]
59pub use gui::{PlotWindow, WindowConfig};
60
61#[cfg(feature = "gui")]
63pub use gui::{is_window_available, show_plot_sequential};
64
65#[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
72pub use export::{image::*, vector::*};
75
76#[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], }
95 }
96}
97
98pub 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 render_figure_to_file(figure, path)
110 }
111 None => {
112 #[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#[cfg(not(target_arch = "wasm32"))]
136fn render_figure_to_file(figure: plots::Figure, path: &str) -> Result<String, String> {
137 use crate::export::ImageExporter;
138 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
154pub 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), 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
183pub 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), 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
214pub 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); 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
241pub 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 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 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
303pub fn show_interactive_platform_optimal(figure: plots::Figure) -> Result<String, String> {
308 render_interactive_with_handle(0, figure)
309}
310
311pub 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}