1use crate::core::{
6 vertex_utils, BoundingBox, DrawCall, Material, PipelineType, RenderData, Vertex,
7};
8use glam::{Vec3, Vec4};
9
10#[derive(Debug, Clone)]
12pub struct ScatterPlot {
13 pub x_data: Vec<f64>,
15 pub y_data: Vec<f64>,
16
17 pub color: Vec4,
19 pub marker_size: f32,
20 pub marker_style: MarkerStyle,
21
22 pub label: Option<String>,
24 pub visible: bool,
25
26 vertices: Option<Vec<Vertex>>,
28 bounds: Option<BoundingBox>,
29 dirty: bool,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum MarkerStyle {
35 Circle,
36 Square,
37 Triangle,
38 Diamond,
39 Plus,
40 Cross,
41 Star,
42 Hexagon,
43}
44
45impl Default for MarkerStyle {
46 fn default() -> Self {
47 Self::Circle
48 }
49}
50
51impl ScatterPlot {
52 pub fn new(x_data: Vec<f64>, y_data: Vec<f64>) -> Result<Self, String> {
54 if x_data.len() != y_data.len() {
55 return Err(format!(
56 "Data length mismatch: x_data has {} points, y_data has {} points",
57 x_data.len(),
58 y_data.len()
59 ));
60 }
61
62 if x_data.is_empty() {
63 return Err("Cannot create scatter plot with empty data".to_string());
64 }
65
66 Ok(Self {
67 x_data,
68 y_data,
69 color: Vec4::new(1.0, 0.0, 0.0, 1.0), marker_size: 3.0,
71 marker_style: MarkerStyle::default(),
72 label: None,
73 visible: true,
74 vertices: None,
75 bounds: None,
76 dirty: true,
77 })
78 }
79
80 pub fn with_style(mut self, color: Vec4, marker_size: f32, marker_style: MarkerStyle) -> Self {
82 self.color = color;
83 self.marker_size = marker_size;
84 self.marker_style = marker_style;
85 self.dirty = true;
86 self
87 }
88
89 pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
91 self.label = Some(label.into());
92 self
93 }
94
95 pub fn update_data(&mut self, x_data: Vec<f64>, y_data: Vec<f64>) -> Result<(), String> {
97 if x_data.len() != y_data.len() {
98 return Err(format!(
99 "Data length mismatch: x_data has {} points, y_data has {} points",
100 x_data.len(),
101 y_data.len()
102 ));
103 }
104
105 if x_data.is_empty() {
106 return Err("Cannot update with empty data".to_string());
107 }
108
109 self.x_data = x_data;
110 self.y_data = y_data;
111 self.dirty = true;
112 Ok(())
113 }
114
115 pub fn set_color(&mut self, color: Vec4) {
117 self.color = color;
118 self.dirty = true;
119 }
120
121 pub fn set_marker_size(&mut self, size: f32) {
123 self.marker_size = size.max(0.1); self.dirty = true;
125 }
126
127 pub fn set_marker_style(&mut self, style: MarkerStyle) {
129 self.marker_style = style;
130 self.dirty = true;
131 }
132
133 pub fn set_visible(&mut self, visible: bool) {
135 self.visible = visible;
136 }
137
138 pub fn len(&self) -> usize {
140 self.x_data.len()
141 }
142
143 pub fn is_empty(&self) -> bool {
145 self.x_data.is_empty()
146 }
147
148 pub fn generate_vertices(&mut self) -> &Vec<Vertex> {
150 if self.dirty || self.vertices.is_none() {
151 self.vertices = Some(vertex_utils::create_scatter_plot(
152 &self.x_data,
153 &self.y_data,
154 self.color,
155 ));
156 self.dirty = false;
157 }
158 self.vertices.as_ref().unwrap()
159 }
160
161 pub fn bounds(&mut self) -> BoundingBox {
163 if self.dirty || self.bounds.is_none() {
164 let points: Vec<Vec3> = self
165 .x_data
166 .iter()
167 .zip(self.y_data.iter())
168 .map(|(&x, &y)| Vec3::new(x as f32, y as f32, 0.0))
169 .collect();
170 self.bounds = Some(BoundingBox::from_points(&points));
171 }
172 self.bounds.unwrap()
173 }
174
175 pub fn render_data(&mut self) -> RenderData {
177 let vertices = self.generate_vertices().clone();
178 let vertex_count = vertices.len();
179
180 let material = Material {
181 albedo: self.color,
182 ..Default::default()
183 };
184
185 let draw_call = DrawCall {
186 vertex_offset: 0,
187 vertex_count,
188 index_offset: None,
189 index_count: None,
190 instance_count: 1,
191 };
192
193 RenderData {
194 pipeline_type: PipelineType::Points,
195 vertices,
196 indices: None,
197 material,
198 draw_calls: vec![draw_call],
199 }
200 }
201
202 pub fn statistics(&self) -> PlotStatistics {
204 let (min_x, max_x) = self
205 .x_data
206 .iter()
207 .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), &x| {
208 (min.min(x), max.max(x))
209 });
210 let (min_y, max_y) = self
211 .y_data
212 .iter()
213 .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), &y| {
214 (min.min(y), max.max(y))
215 });
216
217 PlotStatistics {
218 point_count: self.x_data.len(),
219 x_range: (min_x, max_x),
220 y_range: (min_y, max_y),
221 memory_usage: self.estimated_memory_usage(),
222 }
223 }
224
225 pub fn estimated_memory_usage(&self) -> usize {
227 std::mem::size_of::<f64>() * (self.x_data.len() + self.y_data.len())
228 + self
229 .vertices
230 .as_ref()
231 .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>())
232 }
233}
234
235#[derive(Debug, Clone)]
237pub struct PlotStatistics {
238 pub point_count: usize,
239 pub x_range: (f64, f64),
240 pub y_range: (f64, f64),
241 pub memory_usage: usize,
242}
243
244pub mod matlab_compat {
246 use super::*;
247
248 pub fn scatter(x: Vec<f64>, y: Vec<f64>) -> Result<ScatterPlot, String> {
250 ScatterPlot::new(x, y)
251 }
252
253 pub fn scatter_with_style(
255 x: Vec<f64>,
256 y: Vec<f64>,
257 size: f32,
258 color: &str,
259 ) -> Result<ScatterPlot, String> {
260 let color_vec = parse_matlab_color(color)?;
261 Ok(ScatterPlot::new(x, y)?.with_style(color_vec, size, MarkerStyle::Circle))
262 }
263
264 fn parse_matlab_color(color: &str) -> Result<Vec4, String> {
266 match color {
267 "r" | "red" => Ok(Vec4::new(1.0, 0.0, 0.0, 1.0)),
268 "g" | "green" => Ok(Vec4::new(0.0, 1.0, 0.0, 1.0)),
269 "b" | "blue" => Ok(Vec4::new(0.0, 0.0, 1.0, 1.0)),
270 "c" | "cyan" => Ok(Vec4::new(0.0, 1.0, 1.0, 1.0)),
271 "m" | "magenta" => Ok(Vec4::new(1.0, 0.0, 1.0, 1.0)),
272 "y" | "yellow" => Ok(Vec4::new(1.0, 1.0, 0.0, 1.0)),
273 "k" | "black" => Ok(Vec4::new(0.0, 0.0, 0.0, 1.0)),
274 "w" | "white" => Ok(Vec4::new(1.0, 1.0, 1.0, 1.0)),
275 _ => Err(format!("Unknown color: {color}")),
276 }
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn test_scatter_plot_creation() {
286 let x = vec![0.0, 1.0, 2.0, 3.0];
287 let y = vec![0.0, 1.0, 4.0, 9.0];
288
289 let plot = ScatterPlot::new(x.clone(), y.clone()).unwrap();
290
291 assert_eq!(plot.x_data, x);
292 assert_eq!(plot.y_data, y);
293 assert_eq!(plot.len(), 4);
294 assert!(!plot.is_empty());
295 assert!(plot.visible);
296 }
297
298 #[test]
299 fn test_scatter_plot_styling() {
300 let x = vec![0.0, 1.0, 2.0];
301 let y = vec![1.0, 2.0, 1.5];
302 let color = Vec4::new(0.0, 1.0, 0.0, 1.0);
303
304 let plot = ScatterPlot::new(x, y)
305 .unwrap()
306 .with_style(color, 5.0, MarkerStyle::Square)
307 .with_label("Test Scatter");
308
309 assert_eq!(plot.color, color);
310 assert_eq!(plot.marker_size, 5.0);
311 assert_eq!(plot.marker_style, MarkerStyle::Square);
312 assert_eq!(plot.label, Some("Test Scatter".to_string()));
313 }
314
315 #[test]
316 fn test_scatter_plot_render_data() {
317 let x = vec![0.0, 1.0, 2.0];
318 let y = vec![1.0, 2.0, 1.0];
319
320 let mut plot = ScatterPlot::new(x, y).unwrap();
321 let render_data = plot.render_data();
322
323 assert_eq!(render_data.pipeline_type, PipelineType::Points);
324 assert_eq!(render_data.vertices.len(), 3); assert!(render_data.indices.is_none());
326 assert_eq!(render_data.draw_calls.len(), 1);
327 }
328
329 #[test]
330 fn test_matlab_compat_scatter() {
331 use super::matlab_compat::*;
332
333 let x = vec![0.0, 1.0];
334 let y = vec![0.0, 1.0];
335
336 let basic_scatter = scatter(x.clone(), y.clone()).unwrap();
337 assert_eq!(basic_scatter.len(), 2);
338
339 let styled_scatter = scatter_with_style(x.clone(), y.clone(), 5.0, "g").unwrap();
340 assert_eq!(styled_scatter.color, Vec4::new(0.0, 1.0, 0.0, 1.0));
341 assert_eq!(styled_scatter.marker_size, 5.0);
342 }
343}