runmat_plot/plots/
scatter.rs

1//! Scatter plot implementation
2//!
3//! High-performance scatter plotting with GPU acceleration.
4
5use crate::core::{
6    vertex_utils, BoundingBox, DrawCall, Material, PipelineType, RenderData, Vertex,
7};
8use glam::{Vec3, Vec4};
9
10/// High-performance GPU-accelerated scatter plot
11#[derive(Debug, Clone)]
12pub struct ScatterPlot {
13    /// Raw data points (x, y coordinates)
14    pub x_data: Vec<f64>,
15    pub y_data: Vec<f64>,
16
17    /// Visual styling
18    pub color: Vec4,
19    pub marker_size: f32,
20    pub marker_style: MarkerStyle,
21
22    /// Metadata
23    pub label: Option<String>,
24    pub visible: bool,
25
26    /// Generated rendering data (cached)
27    vertices: Option<Vec<Vertex>>,
28    bounds: Option<BoundingBox>,
29    dirty: bool,
30}
31
32/// Marker styles for scatter plots
33#[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    /// Create a new scatter plot with data
53    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), // Default red
70            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    /// Create a scatter plot with custom styling
81    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    /// Set the plot label for legends
90    pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
91        self.label = Some(label.into());
92        self
93    }
94
95    /// Update the data points
96    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    /// Set the color of the markers
116    pub fn set_color(&mut self, color: Vec4) {
117        self.color = color;
118        self.dirty = true;
119    }
120
121    /// Set the marker size
122    pub fn set_marker_size(&mut self, size: f32) {
123        self.marker_size = size.max(0.1); // Minimum marker size
124        self.dirty = true;
125    }
126
127    /// Set the marker style
128    pub fn set_marker_style(&mut self, style: MarkerStyle) {
129        self.marker_style = style;
130        self.dirty = true;
131    }
132
133    /// Show or hide the plot
134    pub fn set_visible(&mut self, visible: bool) {
135        self.visible = visible;
136    }
137
138    /// Get the number of data points
139    pub fn len(&self) -> usize {
140        self.x_data.len()
141    }
142
143    /// Check if the plot has no data
144    pub fn is_empty(&self) -> bool {
145        self.x_data.is_empty()
146    }
147
148    /// Generate vertices for GPU rendering
149    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    /// Get the bounding box of the data
162    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    /// Generate complete render data for the graphics pipeline
176    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    /// Get plot statistics for debugging
203    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    /// Estimate memory usage in bytes
226    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/// Plot performance and data statistics
236#[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
244/// MATLAB-compatible scatter plot creation utilities
245pub mod matlab_compat {
246    use super::*;
247
248    /// Create a simple scatter plot (equivalent to MATLAB's `scatter(x, y)`)
249    pub fn scatter(x: Vec<f64>, y: Vec<f64>) -> Result<ScatterPlot, String> {
250        ScatterPlot::new(x, y)
251    }
252
253    /// Create a scatter plot with specified color and size (`scatter(x, y, size, color)`)
254    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    /// Parse MATLAB color specifications
265    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); // One vertex per point
325        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}