runmat_plot/plots/
line.rs

1//! Line plot implementation
2//!
3//! High-performance line 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 line plot
11#[derive(Debug, Clone)]
12pub struct LinePlot {
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 line_width: f32,
20    pub line_style: LineStyle,
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/// Line rendering styles
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum LineStyle {
35    Solid,
36    Dashed,
37    Dotted,
38    DashDot,
39}
40
41impl Default for LineStyle {
42    fn default() -> Self {
43        Self::Solid
44    }
45}
46
47impl LinePlot {
48    /// Create a new line plot with data
49    pub fn new(x_data: Vec<f64>, y_data: Vec<f64>) -> Result<Self, String> {
50        if x_data.len() != y_data.len() {
51            return Err(format!(
52                "Data length mismatch: x_data has {} points, y_data has {} points",
53                x_data.len(),
54                y_data.len()
55            ));
56        }
57
58        if x_data.is_empty() {
59            return Err("Cannot create line plot with empty data".to_string());
60        }
61
62        Ok(Self {
63            x_data,
64            y_data,
65            color: Vec4::new(0.0, 0.5, 1.0, 1.0), // Default blue
66            line_width: 1.0,
67            line_style: LineStyle::default(),
68            label: None,
69            visible: true,
70            vertices: None,
71            bounds: None,
72            dirty: true,
73        })
74    }
75
76    /// Create a line plot with custom styling
77    pub fn with_style(mut self, color: Vec4, line_width: f32, line_style: LineStyle) -> Self {
78        self.color = color;
79        self.line_width = line_width;
80        self.line_style = line_style;
81        self.dirty = true;
82        self
83    }
84
85    /// Set the plot label for legends
86    pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
87        self.label = Some(label.into());
88        self
89    }
90
91    /// Update the data points
92    pub fn update_data(&mut self, x_data: Vec<f64>, y_data: Vec<f64>) -> Result<(), String> {
93        if x_data.len() != y_data.len() {
94            return Err(format!(
95                "Data length mismatch: x_data has {} points, y_data has {} points",
96                x_data.len(),
97                y_data.len()
98            ));
99        }
100
101        if x_data.is_empty() {
102            return Err("Cannot update with empty data".to_string());
103        }
104
105        self.x_data = x_data;
106        self.y_data = y_data;
107        self.dirty = true;
108        Ok(())
109    }
110
111    /// Set the color of the line
112    pub fn set_color(&mut self, color: Vec4) {
113        self.color = color;
114        self.dirty = true;
115    }
116
117    /// Set the line width
118    pub fn set_line_width(&mut self, width: f32) {
119        self.line_width = width.max(0.1); // Minimum line width
120        self.dirty = true;
121    }
122
123    /// Set the line style
124    pub fn set_line_style(&mut self, style: LineStyle) {
125        self.line_style = style;
126        self.dirty = true;
127    }
128
129    /// Show or hide the plot
130    pub fn set_visible(&mut self, visible: bool) {
131        self.visible = visible;
132    }
133
134    /// Get the number of data points
135    pub fn len(&self) -> usize {
136        self.x_data.len()
137    }
138
139    /// Check if the plot has no data
140    pub fn is_empty(&self) -> bool {
141        self.x_data.is_empty()
142    }
143
144    /// Generate vertices for GPU rendering
145    pub fn generate_vertices(&mut self) -> &Vec<Vertex> {
146        if self.dirty || self.vertices.is_none() {
147            self.vertices = Some(vertex_utils::create_line_plot(
148                &self.x_data,
149                &self.y_data,
150                self.color,
151            ));
152            self.dirty = false;
153        }
154        self.vertices.as_ref().unwrap()
155    }
156
157    /// Get the bounding box of the data
158    pub fn bounds(&mut self) -> BoundingBox {
159        if self.dirty || self.bounds.is_none() {
160            let points: Vec<Vec3> = self
161                .x_data
162                .iter()
163                .zip(self.y_data.iter())
164                .map(|(&x, &y)| Vec3::new(x as f32, y as f32, 0.0))
165                .collect();
166            self.bounds = Some(BoundingBox::from_points(&points));
167        }
168        self.bounds.unwrap()
169    }
170
171    /// Generate complete render data for the graphics pipeline
172    pub fn render_data(&mut self) -> RenderData {
173        let vertices = self.generate_vertices().clone();
174        let vertex_count = vertices.len();
175
176        let material = Material {
177            albedo: self.color,
178            ..Default::default()
179        };
180
181        let draw_call = DrawCall {
182            vertex_offset: 0,
183            vertex_count,
184            index_offset: None,
185            index_count: None,
186            instance_count: 1,
187        };
188
189        RenderData {
190            pipeline_type: PipelineType::Lines,
191            vertices,
192            indices: None,
193            material,
194            draw_calls: vec![draw_call],
195        }
196    }
197
198    /// Get plot statistics for debugging
199    pub fn statistics(&self) -> PlotStatistics {
200        let (min_x, max_x) = self
201            .x_data
202            .iter()
203            .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), &x| {
204                (min.min(x), max.max(x))
205            });
206        let (min_y, max_y) = self
207            .y_data
208            .iter()
209            .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), &y| {
210                (min.min(y), max.max(y))
211            });
212
213        PlotStatistics {
214            point_count: self.x_data.len(),
215            x_range: (min_x, max_x),
216            y_range: (min_y, max_y),
217            memory_usage: self.estimated_memory_usage(),
218        }
219    }
220
221    /// Estimate memory usage in bytes
222    pub fn estimated_memory_usage(&self) -> usize {
223        std::mem::size_of::<f64>() * (self.x_data.len() + self.y_data.len())
224            + self
225                .vertices
226                .as_ref()
227                .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>())
228    }
229}
230
231/// Plot performance and data statistics
232#[derive(Debug, Clone)]
233pub struct PlotStatistics {
234    pub point_count: usize,
235    pub x_range: (f64, f64),
236    pub y_range: (f64, f64),
237    pub memory_usage: usize,
238}
239
240/// MATLAB-compatible line plot creation utilities
241pub mod matlab_compat {
242    use super::*;
243
244    /// Create a simple line plot (equivalent to MATLAB's `plot(x, y)`)
245    pub fn plot(x: Vec<f64>, y: Vec<f64>) -> Result<LinePlot, String> {
246        LinePlot::new(x, y)
247    }
248
249    /// Create a line plot with specified color (`plot(x, y, 'r')`)
250    pub fn plot_with_color(x: Vec<f64>, y: Vec<f64>, color: &str) -> Result<LinePlot, String> {
251        let color_vec = parse_matlab_color(color)?;
252        Ok(LinePlot::new(x, y)?.with_style(color_vec, 1.0, LineStyle::Solid))
253    }
254
255    /// Parse MATLAB color specifications
256    fn parse_matlab_color(color: &str) -> Result<Vec4, String> {
257        match color {
258            "r" | "red" => Ok(Vec4::new(1.0, 0.0, 0.0, 1.0)),
259            "g" | "green" => Ok(Vec4::new(0.0, 1.0, 0.0, 1.0)),
260            "b" | "blue" => Ok(Vec4::new(0.0, 0.0, 1.0, 1.0)),
261            "c" | "cyan" => Ok(Vec4::new(0.0, 1.0, 1.0, 1.0)),
262            "m" | "magenta" => Ok(Vec4::new(1.0, 0.0, 1.0, 1.0)),
263            "y" | "yellow" => Ok(Vec4::new(1.0, 1.0, 0.0, 1.0)),
264            "k" | "black" => Ok(Vec4::new(0.0, 0.0, 0.0, 1.0)),
265            "w" | "white" => Ok(Vec4::new(1.0, 1.0, 1.0, 1.0)),
266            _ => Err(format!("Unknown color: {color}")),
267        }
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn test_line_plot_creation() {
277        let x = vec![0.0, 1.0, 2.0, 3.0];
278        let y = vec![0.0, 1.0, 0.0, 1.0];
279
280        let plot = LinePlot::new(x.clone(), y.clone()).unwrap();
281
282        assert_eq!(plot.x_data, x);
283        assert_eq!(plot.y_data, y);
284        assert_eq!(plot.len(), 4);
285        assert!(!plot.is_empty());
286        assert!(plot.visible);
287    }
288
289    #[test]
290    fn test_line_plot_data_validation() {
291        // Mismatched lengths should fail
292        let x = vec![0.0, 1.0, 2.0];
293        let y = vec![0.0, 1.0];
294        assert!(LinePlot::new(x, y).is_err());
295
296        // Empty data should fail
297        let empty_x: Vec<f64> = vec![];
298        let empty_y: Vec<f64> = vec![];
299        assert!(LinePlot::new(empty_x, empty_y).is_err());
300    }
301
302    #[test]
303    fn test_line_plot_styling() {
304        let x = vec![0.0, 1.0, 2.0];
305        let y = vec![1.0, 2.0, 1.5];
306        let color = Vec4::new(1.0, 0.0, 0.0, 1.0);
307
308        let plot = LinePlot::new(x, y)
309            .unwrap()
310            .with_style(color, 2.0, LineStyle::Dashed)
311            .with_label("Test Line");
312
313        assert_eq!(plot.color, color);
314        assert_eq!(plot.line_width, 2.0);
315        assert_eq!(plot.line_style, LineStyle::Dashed);
316        assert_eq!(plot.label, Some("Test Line".to_string()));
317    }
318
319    #[test]
320    fn test_line_plot_data_update() {
321        let mut plot = LinePlot::new(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
322
323        let new_x = vec![0.0, 0.5, 1.0, 1.5];
324        let new_y = vec![0.0, 0.25, 1.0, 2.25];
325
326        plot.update_data(new_x.clone(), new_y.clone()).unwrap();
327
328        assert_eq!(plot.x_data, new_x);
329        assert_eq!(plot.y_data, new_y);
330        assert_eq!(plot.len(), 4);
331    }
332
333    #[test]
334    fn test_line_plot_bounds() {
335        let x = vec![-1.0, 0.0, 1.0, 2.0];
336        let y = vec![-2.0, 0.0, 1.0, 3.0];
337
338        let mut plot = LinePlot::new(x, y).unwrap();
339        let bounds = plot.bounds();
340
341        assert_eq!(bounds.min.x, -1.0);
342        assert_eq!(bounds.max.x, 2.0);
343        assert_eq!(bounds.min.y, -2.0);
344        assert_eq!(bounds.max.y, 3.0);
345    }
346
347    #[test]
348    fn test_line_plot_vertex_generation() {
349        let x = vec![0.0, 1.0, 2.0];
350        let y = vec![0.0, 1.0, 0.0];
351
352        let mut plot = LinePlot::new(x, y).unwrap();
353        let vertices = plot.generate_vertices();
354
355        // Should have 2 line segments (4 vertices total)
356        assert_eq!(vertices.len(), 4);
357
358        // Check first line segment
359        assert_eq!(vertices[0].position, [0.0, 0.0, 0.0]);
360        assert_eq!(vertices[1].position, [1.0, 1.0, 0.0]);
361    }
362
363    #[test]
364    fn test_line_plot_render_data() {
365        let x = vec![0.0, 1.0, 2.0];
366        let y = vec![1.0, 2.0, 1.0];
367
368        let mut plot = LinePlot::new(x, y).unwrap();
369        let render_data = plot.render_data();
370
371        assert_eq!(render_data.pipeline_type, PipelineType::Lines);
372        assert_eq!(render_data.vertices.len(), 4); // 2 line segments
373        assert!(render_data.indices.is_none());
374        assert_eq!(render_data.draw_calls.len(), 1);
375    }
376
377    #[test]
378    fn test_line_plot_statistics() {
379        let x = vec![0.0, 1.0, 2.0, 3.0];
380        let y = vec![-1.0, 0.0, 1.0, 2.0];
381
382        let plot = LinePlot::new(x, y).unwrap();
383        let stats = plot.statistics();
384
385        assert_eq!(stats.point_count, 4);
386        assert_eq!(stats.x_range, (0.0, 3.0));
387        assert_eq!(stats.y_range, (-1.0, 2.0));
388        assert!(stats.memory_usage > 0);
389    }
390
391    #[test]
392    fn test_matlab_compat_colors() {
393        use super::matlab_compat::*;
394
395        let x = vec![0.0, 1.0];
396        let y = vec![0.0, 1.0];
397
398        let red_plot = plot_with_color(x.clone(), y.clone(), "r").unwrap();
399        assert_eq!(red_plot.color, Vec4::new(1.0, 0.0, 0.0, 1.0));
400
401        let blue_plot = plot_with_color(x.clone(), y.clone(), "blue").unwrap();
402        assert_eq!(blue_plot.color, Vec4::new(0.0, 0.0, 1.0, 1.0));
403
404        // Invalid color should fail
405        assert!(plot_with_color(x, y, "invalid").is_err());
406    }
407}