Skip to main content

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, AlphaMode, BoundingBox, DrawCall, GpuPackContext, GpuVertexBuffer, Material,
7    PipelineType, RenderData, Vertex,
8};
9use crate::gpu::line::LineGpuInputs;
10use crate::plots::scatter::MarkerStyle as ScatterMarkerStyle;
11use glam::{Vec3, Vec4};
12use log::trace;
13
14/// High-performance GPU-accelerated line plot
15#[derive(Debug, Clone)]
16pub struct LinePlot {
17    /// Raw data points (x, y coordinates)
18    pub x_data: Vec<f64>,
19    pub y_data: Vec<f64>,
20
21    /// Visual styling
22    pub color: Vec4,
23    pub line_width: f32,
24    pub line_style: LineStyle,
25    pub line_join: LineJoin,
26    pub line_cap: LineCap,
27    pub marker: Option<LineMarkerAppearance>,
28
29    /// Metadata
30    pub label: Option<String>,
31    pub visible: bool,
32
33    /// Generated rendering data (cached)
34    vertices: Option<Vec<Vertex>>,
35    bounds: Option<BoundingBox>,
36    dirty: bool,
37    gpu_vertices: Option<GpuVertexBuffer>,
38    gpu_vertex_count: Option<usize>,
39    gpu_line_inputs: Option<LineGpuInputs>,
40    marker_vertices: Option<Vec<Vertex>>,
41    marker_gpu_vertices: Option<GpuVertexBuffer>,
42    marker_dirty: bool,
43    gpu_topology: Option<PipelineType>,
44}
45
46#[derive(Debug, Clone)]
47pub struct LineMarkerAppearance {
48    pub kind: ScatterMarkerStyle,
49    pub size: f32,
50    pub edge_color: Vec4,
51    pub face_color: Vec4,
52    pub filled: bool,
53}
54
55#[derive(Debug, Clone)]
56pub struct LineGpuStyle {
57    pub color: Vec4,
58    pub line_width: f32,
59    pub line_style: LineStyle,
60    pub marker: Option<LineMarkerAppearance>,
61}
62
63/// Line rendering styles
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum LineStyle {
66    Solid,
67    Dashed,
68    Dotted,
69    DashDot,
70}
71
72/// Line join style for thick polylines
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum LineJoin {
75    Miter,
76    Bevel,
77    Round,
78}
79
80impl Default for LineJoin {
81    fn default() -> Self {
82        Self::Miter
83    }
84}
85
86/// Line cap style for thick polylines
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum LineCap {
89    Butt,
90    Square,
91    Round,
92}
93
94impl Default for LineCap {
95    fn default() -> Self {
96        Self::Butt
97    }
98}
99
100impl Default for LineStyle {
101    fn default() -> Self {
102        Self::Solid
103    }
104}
105
106impl LinePlot {
107    pub(crate) fn has_gpu_line_inputs(&self) -> bool {
108        self.gpu_line_inputs.is_some()
109    }
110
111    pub(crate) fn has_gpu_vertices(&self) -> bool {
112        self.gpu_vertices.is_some()
113    }
114
115    /// Create a new line plot with data
116    pub fn new(x_data: Vec<f64>, y_data: Vec<f64>) -> Result<Self, String> {
117        if x_data.len() != y_data.len() {
118            return Err(format!(
119                "Data length mismatch: x_data has {} points, y_data has {} points",
120                x_data.len(),
121                y_data.len()
122            ));
123        }
124
125        if x_data.is_empty() {
126            return Err("Cannot create line plot with empty data".to_string());
127        }
128
129        Ok(Self {
130            x_data,
131            y_data,
132            color: Vec4::new(0.0, 0.5, 1.0, 1.0), // Default blue
133            line_width: 1.0,
134            line_style: LineStyle::default(),
135            line_join: LineJoin::default(),
136            line_cap: LineCap::default(),
137            marker: None,
138            label: None,
139            visible: true,
140            vertices: None,
141            bounds: None,
142            dirty: true,
143            gpu_vertices: None,
144            gpu_vertex_count: None,
145            gpu_line_inputs: None,
146            marker_vertices: None,
147            marker_gpu_vertices: None,
148            marker_dirty: true,
149            gpu_topology: None,
150        })
151    }
152
153    /// Build a line plot directly from a GPU vertex buffer.
154    pub fn from_gpu_buffer(
155        buffer: GpuVertexBuffer,
156        vertex_count: usize,
157        style: LineGpuStyle,
158        bounds: BoundingBox,
159        pipeline: PipelineType,
160        marker_buffer: Option<GpuVertexBuffer>,
161    ) -> Self {
162        Self {
163            x_data: Vec::new(),
164            y_data: Vec::new(),
165            color: style.color,
166            line_width: style.line_width,
167            line_style: style.line_style,
168            line_join: LineJoin::Miter,
169            line_cap: LineCap::Butt,
170            marker: style.marker,
171            label: None,
172            visible: true,
173            vertices: None,
174            bounds: Some(bounds),
175            dirty: false,
176            gpu_vertices: Some(buffer),
177            gpu_vertex_count: Some(vertex_count),
178            gpu_line_inputs: None,
179            marker_vertices: None,
180            marker_gpu_vertices: marker_buffer,
181            marker_dirty: true,
182            gpu_topology: Some(pipeline),
183        }
184    }
185
186    /// Create a GPU-backed line plot from X/Y device buffers.
187    ///
188    /// Geometry is packed at render-time when a viewport size is available so that pixel-based
189    /// widths can be converted into data units.
190    pub fn from_gpu_xy(
191        inputs: LineGpuInputs,
192        style: LineGpuStyle,
193        bounds: BoundingBox,
194        marker_buffer: Option<GpuVertexBuffer>,
195    ) -> Self {
196        Self {
197            x_data: Vec::new(),
198            y_data: Vec::new(),
199            color: style.color,
200            line_width: style.line_width,
201            line_style: style.line_style,
202            line_join: LineJoin::Miter,
203            line_cap: LineCap::Butt,
204            marker: style.marker,
205            label: None,
206            visible: true,
207            vertices: None,
208            bounds: Some(bounds),
209            dirty: false,
210            gpu_vertices: None,
211            gpu_vertex_count: None,
212            gpu_line_inputs: Some(inputs),
213            marker_vertices: None,
214            marker_gpu_vertices: marker_buffer,
215            marker_dirty: true,
216            gpu_topology: None,
217        }
218    }
219
220    fn invalidate_gpu_data(&mut self) {
221        self.gpu_vertices = None;
222        self.gpu_vertex_count = None;
223        self.bounds = None;
224        self.gpu_line_inputs = None;
225        self.marker_gpu_vertices = None;
226        self.marker_dirty = true;
227        self.gpu_topology = None;
228    }
229
230    fn invalidate_marker_data(&mut self) {
231        self.marker_vertices = None;
232        self.marker_dirty = true;
233        if self.gpu_vertices.is_none() {
234            self.marker_gpu_vertices = None;
235        }
236    }
237
238    /// Create a line plot with custom styling
239    pub fn with_style(mut self, color: Vec4, line_width: f32, line_style: LineStyle) -> Self {
240        self.color = color;
241        self.line_width = line_width;
242        self.line_style = line_style;
243        self.dirty = true;
244        self.invalidate_gpu_data();
245        self
246    }
247
248    /// Set the plot label for legends
249    pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
250        self.label = Some(label.into());
251        self
252    }
253
254    /// Update the data points
255    pub fn update_data(&mut self, x_data: Vec<f64>, y_data: Vec<f64>) -> Result<(), String> {
256        if x_data.len() != y_data.len() {
257            return Err(format!(
258                "Data length mismatch: x_data has {} points, y_data has {} points",
259                x_data.len(),
260                y_data.len()
261            ));
262        }
263
264        if x_data.is_empty() {
265            return Err("Cannot update with empty data".to_string());
266        }
267
268        self.x_data = x_data;
269        self.y_data = y_data;
270        self.dirty = true;
271        self.invalidate_marker_data();
272        Ok(())
273    }
274
275    /// Set the color of the line
276    pub fn set_color(&mut self, color: Vec4) {
277        self.color = color;
278        self.dirty = true;
279        self.invalidate_gpu_data();
280        self.invalidate_marker_data();
281    }
282
283    /// Set the line width
284    pub fn set_line_width(&mut self, width: f32) {
285        self.line_width = width.max(0.1); // Minimum line width
286        self.dirty = true;
287        self.invalidate_gpu_data();
288    }
289
290    /// Set the line style
291    pub fn set_line_style(&mut self, style: LineStyle) {
292        self.line_style = style;
293        self.dirty = true;
294        self.invalidate_gpu_data();
295    }
296
297    /// Attach marker metadata so renderers can emit hybrid line+marker plots.
298    pub fn set_marker(&mut self, marker: Option<LineMarkerAppearance>) {
299        self.marker = marker;
300        self.invalidate_marker_data();
301    }
302
303    /// Set the line join style for thick lines
304    pub fn set_line_join(&mut self, join: LineJoin) {
305        self.line_join = join;
306        self.dirty = true;
307        self.invalidate_gpu_data();
308    }
309
310    /// Set the line cap style for thick lines
311    pub fn set_line_cap(&mut self, cap: LineCap) {
312        self.line_cap = cap;
313        self.dirty = true;
314        self.invalidate_gpu_data();
315    }
316
317    /// Show or hide the plot
318    pub fn set_visible(&mut self, visible: bool) {
319        self.visible = visible;
320    }
321
322    /// Get the number of data points
323    pub fn len(&self) -> usize {
324        if !self.x_data.is_empty() {
325            self.x_data.len()
326        } else {
327            self.gpu_vertex_count.unwrap_or(0)
328        }
329    }
330
331    /// Check if the plot has no data
332    pub fn is_empty(&self) -> bool {
333        self.len() == 0
334    }
335
336    /// Generate vertices for GPU rendering
337    pub fn generate_vertices(&mut self) -> &Vec<Vertex> {
338        if self.gpu_vertices.is_some() {
339            if self.vertices.is_none() {
340                self.vertices = Some(Vec::new());
341            }
342            return self.vertices.as_ref().unwrap();
343        }
344        if self.dirty || self.vertices.is_none() {
345            if self.line_width > 1.0 {
346                // Use triangle extrusion for thicker lines; switch pipeline in render_data
347                let base_tris = match self.line_cap {
348                    LineCap::Butt => vertex_utils::create_thick_polyline_with_join(
349                        &self.x_data,
350                        &self.y_data,
351                        self.color,
352                        self.line_width,
353                        self.line_join,
354                    ),
355                    LineCap::Square => vertex_utils::create_thick_polyline_square_caps(
356                        &self.x_data,
357                        &self.y_data,
358                        self.color,
359                        self.line_width,
360                    ),
361                    LineCap::Round => vertex_utils::create_thick_polyline_round_caps(
362                        &self.x_data,
363                        &self.y_data,
364                        self.color,
365                        self.line_width,
366                        12,
367                    ),
368                };
369                let tris = match self.line_style {
370                    LineStyle::Solid => base_tris,
371                    LineStyle::Dashed | LineStyle::DashDot | LineStyle::Dotted => {
372                        vertex_utils::create_thick_polyline_dashed(
373                            &self.x_data,
374                            &self.y_data,
375                            self.color,
376                            self.line_width,
377                            self.line_style,
378                        )
379                    }
380                };
381                self.vertices = Some(tris);
382            } else {
383                let verts = match self.line_style {
384                    LineStyle::Solid => {
385                        vertex_utils::create_line_plot(&self.x_data, &self.y_data, self.color)
386                    }
387                    LineStyle::Dashed | LineStyle::DashDot => {
388                        vertex_utils::create_line_plot_dashed(
389                            &self.x_data,
390                            &self.y_data,
391                            self.color,
392                            self.line_style,
393                        )
394                    }
395                    LineStyle::Dotted => {
396                        // Render as a sequence of tiny dashes to approximate dots
397                        vertex_utils::create_line_plot_dashed(
398                            &self.x_data,
399                            &self.y_data,
400                            self.color,
401                            LineStyle::Dashed,
402                        )
403                    }
404                };
405                self.vertices = Some(verts);
406            }
407            self.dirty = false;
408        }
409        self.vertices.as_ref().unwrap()
410    }
411
412    /// Get the bounding box of the data
413    pub fn bounds(&mut self) -> BoundingBox {
414        if self.bounds.is_some() && self.x_data.is_empty() && self.y_data.is_empty() {
415            return self.bounds.unwrap_or_default();
416        }
417        if self.dirty || self.bounds.is_none() {
418            let points: Vec<Vec3> = self
419                .x_data
420                .iter()
421                .zip(self.y_data.iter())
422                .map(|(&x, &y)| Vec3::new(x as f32, y as f32, 0.0))
423                .collect();
424            self.bounds = Some(BoundingBox::from_points(&points));
425        }
426        self.bounds.unwrap()
427    }
428
429    fn pack_gpu_vertices_if_needed(
430        &mut self,
431        gpu: &GpuPackContext<'_>,
432        viewport_px: (u32, u32),
433    ) -> Result<(), String> {
434        if self.gpu_vertices.is_some() {
435            return Ok(());
436        }
437        let Some(inputs) = self.gpu_line_inputs.as_ref() else {
438            return Ok(());
439        };
440        let bounds = self
441            .bounds
442            .as_ref()
443            .ok_or_else(|| "missing line bounds".to_string())?;
444
445        let thick_px = self.line_width > 1.0;
446        let data_per_px = crate::core::data_units_per_px(bounds, viewport_px);
447        let half_width_data = if thick_px {
448            ((self.line_width.max(0.1)) * 0.5) * data_per_px
449        } else {
450            0.0
451        };
452        trace!(
453            target: "runmat_plot",
454            "line-pack: begin len={} line_width_px={} thick={} half_width_data={} viewport_px={:?} bounds=({:?}..{:?})",
455            inputs.len,
456            self.line_width,
457            thick_px,
458            half_width_data,
459            viewport_px,
460            bounds.min,
461            bounds.max
462        );
463
464        let params = crate::gpu::line::LineGpuParams {
465            color: self.color,
466            half_width_data,
467            thick: thick_px,
468            line_style: self.line_style,
469            marker_size: 1.0,
470        };
471        let packed =
472            crate::gpu::line::pack_vertices_from_xy(gpu.device, gpu.queue, inputs, &params)
473                .map_err(|e| format!("gpu line packing failed: {e}"))?;
474        trace!(
475            target: "runmat_plot",
476            "line-pack: complete max_vertices={} indirect_present={}",
477            packed.vertex_count,
478            packed.indirect.is_some()
479        );
480
481        self.gpu_vertices = Some(packed);
482        self.gpu_vertex_count = Some(self.gpu_vertices.as_ref().unwrap().vertex_count);
483        self.gpu_topology = Some(if thick_px {
484            PipelineType::Triangles
485        } else {
486            PipelineType::Lines
487        });
488        Ok(())
489    }
490
491    pub fn render_data_with_viewport_gpu(
492        &mut self,
493        viewport_px: Option<(u32, u32)>,
494        gpu: Option<&GpuPackContext<'_>>,
495    ) -> RenderData {
496        trace!(
497            target: "runmat_plot",
498            "line: render_data_with_viewport_gpu viewport_px={:?} gpu_ctx_present={} gpu_line_inputs_present={} gpu_vertices_present={}",
499            viewport_px,
500            gpu.is_some(),
501            self.gpu_line_inputs.is_some(),
502            self.gpu_vertices.is_some()
503        );
504        if self.gpu_line_inputs.is_some() && self.gpu_vertices.is_none() {
505            if let (Some(gpu), Some(vp)) = (gpu, viewport_px) {
506                // Best-effort: if packing fails, fall through and let the caller handle
507                // missing geometry (typically via a plotting error upstream).
508                let _ = self.pack_gpu_vertices_if_needed(gpu, vp);
509            }
510        }
511        self.render_data_with_viewport(viewport_px)
512    }
513
514    /// Generate complete render data for the graphics pipeline
515    pub fn render_data(&mut self) -> RenderData {
516        let using_gpu = self.gpu_vertices.is_some();
517        let gpu_vertices = self.gpu_vertices.clone();
518        let (vertices, vertex_count) = if using_gpu {
519            (Vec::new(), self.gpu_vertex_count.unwrap_or(0))
520        } else {
521            let verts = self.generate_vertices().clone();
522            let count = verts.len();
523            (verts, count)
524        };
525
526        // Encode width/style/cap/join into material for exporters:
527        // - roughness: line width
528        // - metallic: line style code (0 solid,1 dashed,2 dotted,3 dashdot)
529        // - emissive.x: cap (0 butt,1 square,2 round)
530        // - emissive.y: join (0 miter,1 bevel,2 round)
531        let style_code = match self.line_style {
532            LineStyle::Solid => 0.0,
533            LineStyle::Dashed => 1.0,
534            LineStyle::Dotted => 2.0,
535            LineStyle::DashDot => 3.0,
536        };
537        let cap_code = match self.line_cap {
538            LineCap::Butt => 0.0,
539            LineCap::Square => 1.0,
540            LineCap::Round => 2.0,
541        };
542        let join_code = match self.line_join {
543            LineJoin::Miter => 0.0,
544            LineJoin::Bevel => 1.0,
545            LineJoin::Round => 2.0,
546        };
547        let mut material = Material {
548            albedo: self.color,
549            ..Default::default()
550        };
551        material.roughness = self.line_width.max(0.0);
552        material.metallic = style_code;
553        material.emissive = Vec4::new(cap_code, join_code, -1.0, 0.0);
554
555        let draw_call = DrawCall {
556            vertex_offset: 0,
557            vertex_count,
558            index_offset: None,
559            index_count: None,
560            instance_count: 1,
561        };
562
563        // If thick polyline was generated, we must render as triangles
564        let pipeline = if using_gpu {
565            self.gpu_topology.unwrap_or(if self.line_width > 1.0 {
566                PipelineType::Triangles
567            } else {
568                PipelineType::Lines
569            })
570        } else if self.line_width > 1.0 {
571            PipelineType::Triangles
572        } else {
573            PipelineType::Lines
574        };
575        RenderData {
576            pipeline_type: pipeline,
577            vertices,
578            indices: None,
579            gpu_vertices,
580            bounds: Some(self.bounds()),
581            material,
582            draw_calls: vec![draw_call],
583            image: None,
584        }
585    }
586
587    /// Generate render data, using an optional viewport size hint (width, height in pixels).
588    ///
589    /// For thick 2D lines we build triangle geometry. The user-facing `line_width` is
590    /// expressed in *pixels*, but triangle extrusion operates in data space. When a viewport
591    /// is supplied we convert pixels → data-units using the current data range and target size.
592    pub fn render_data_with_viewport(&mut self, viewport_px: Option<(u32, u32)>) -> RenderData {
593        if self.gpu_vertices.is_some() {
594            // GPU paths already handle sizing via pipeline/state; keep existing behavior.
595            return self.render_data();
596        }
597
598        let (vertices, vertex_count, pipeline, bounds) = if self.line_width > 1.0 {
599            let bounds = self.bounds();
600            let viewport_px = viewport_px.unwrap_or((600, 400));
601            let data_per_px = crate::core::data_units_per_px(&bounds, viewport_px);
602            let width_data = (self.line_width.max(0.1)) * data_per_px;
603
604            let base_tris = match self.line_cap {
605                LineCap::Butt => vertex_utils::create_thick_polyline_with_join(
606                    &self.x_data,
607                    &self.y_data,
608                    self.color,
609                    width_data,
610                    self.line_join,
611                ),
612                LineCap::Square => vertex_utils::create_thick_polyline_square_caps(
613                    &self.x_data,
614                    &self.y_data,
615                    self.color,
616                    width_data,
617                ),
618                LineCap::Round => vertex_utils::create_thick_polyline_round_caps(
619                    &self.x_data,
620                    &self.y_data,
621                    self.color,
622                    width_data,
623                    12,
624                ),
625            };
626            let tris = match self.line_style {
627                LineStyle::Solid => base_tris,
628                LineStyle::Dashed | LineStyle::DashDot | LineStyle::Dotted => {
629                    vertex_utils::create_thick_polyline_dashed(
630                        &self.x_data,
631                        &self.y_data,
632                        self.color,
633                        width_data,
634                        self.line_style,
635                    )
636                }
637            };
638            let count = tris.len();
639            (tris, count, PipelineType::Triangles, self.bounds())
640        } else {
641            let verts = self.generate_vertices().clone();
642            let count = verts.len();
643            (verts, count, PipelineType::Lines, self.bounds())
644        };
645
646        let style_code = match self.line_style {
647            LineStyle::Solid => 0.0,
648            LineStyle::Dashed => 1.0,
649            LineStyle::Dotted => 2.0,
650            LineStyle::DashDot => 3.0,
651        };
652        let cap_code = match self.line_cap {
653            LineCap::Butt => 0.0,
654            LineCap::Square => 1.0,
655            LineCap::Round => 2.0,
656        };
657        let join_code = match self.line_join {
658            LineJoin::Miter => 0.0,
659            LineJoin::Bevel => 1.0,
660            LineJoin::Round => 2.0,
661        };
662        let mut material = Material {
663            albedo: self.color,
664            ..Default::default()
665        };
666        // Keep the user-facing width in pixels for exporters/metadata.
667        material.roughness = self.line_width.max(0.0);
668        material.metallic = style_code;
669        material.emissive = Vec4::new(cap_code, join_code, -1.0, 0.0);
670
671        let draw_call = DrawCall {
672            vertex_offset: 0,
673            vertex_count,
674            index_offset: None,
675            index_count: None,
676            instance_count: 1,
677        };
678
679        RenderData {
680            pipeline_type: pipeline,
681            vertices,
682            indices: None,
683            gpu_vertices: None,
684            bounds: Some(bounds),
685            material,
686            draw_calls: vec![draw_call],
687            image: None,
688        }
689    }
690
691    /// Generate render data representing the markers for this line plot.
692    pub fn marker_render_data(&mut self) -> Option<RenderData> {
693        let marker = self.marker.clone()?;
694        let material = Self::build_marker_material(&marker);
695
696        if let Some(gpu_vertices) = self.marker_gpu_vertices.clone() {
697            let vertex_count = gpu_vertices.vertex_count;
698            if vertex_count == 0 {
699                return None;
700            }
701            let draw_call = DrawCall {
702                vertex_offset: 0,
703                vertex_count,
704                index_offset: None,
705                index_count: None,
706                instance_count: 1,
707            };
708            return Some(RenderData {
709                pipeline_type: PipelineType::Points,
710                vertices: Vec::new(),
711                indices: None,
712                gpu_vertices: Some(gpu_vertices),
713                bounds: Some(self.bounds()),
714                material,
715                draw_calls: vec![draw_call],
716                image: None,
717            });
718        }
719
720        let vertices = self.marker_vertices_slice(&marker)?;
721        if vertices.is_empty() {
722            return None;
723        }
724        let draw_call = DrawCall {
725            vertex_offset: 0,
726            vertex_count: vertices.len(),
727            index_offset: None,
728            index_count: None,
729            instance_count: 1,
730        };
731
732        Some(RenderData {
733            pipeline_type: PipelineType::Points,
734            vertices: vertices.to_vec(),
735            indices: None,
736            gpu_vertices: None,
737            bounds: Some(self.bounds()),
738            material,
739            draw_calls: vec![draw_call],
740            image: None,
741        })
742    }
743
744    fn build_marker_material(marker: &LineMarkerAppearance) -> Material {
745        let mut material = Material {
746            albedo: marker.face_color,
747            ..Default::default()
748        };
749        if !marker.filled {
750            material.albedo.w = 0.0;
751        }
752        material.emissive = marker.edge_color;
753        material.roughness = 1.0;
754        material.metallic = marker_style_code(marker.kind);
755        material.alpha_mode = AlphaMode::Blend;
756        material
757    }
758
759    fn marker_vertices_slice(&mut self, marker: &LineMarkerAppearance) -> Option<&[Vertex]> {
760        if self.x_data.len() != self.y_data.len() || self.x_data.is_empty() {
761            return None;
762        }
763
764        if self.marker_vertices.is_none() || self.marker_dirty {
765            let mut verts = Vec::with_capacity(self.x_data.len());
766            for (&x, &y) in self.x_data.iter().zip(self.y_data.iter()) {
767                let mut vertex = Vertex::new(Vec3::new(x as f32, y as f32, 0.0), marker.face_color);
768                vertex.normal[2] = marker.size.max(1.0);
769                verts.push(vertex);
770            }
771            self.marker_vertices = Some(verts);
772            self.marker_dirty = false;
773        }
774        self.marker_vertices.as_deref()
775    }
776
777    /// Get plot statistics for debugging
778    pub fn statistics(&self) -> PlotStatistics {
779        let (min_x, max_x) = self
780            .x_data
781            .iter()
782            .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), &x| {
783                (min.min(x), max.max(x))
784            });
785        let (min_y, max_y) = self
786            .y_data
787            .iter()
788            .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), &y| {
789                (min.min(y), max.max(y))
790            });
791
792        PlotStatistics {
793            point_count: self.x_data.len(),
794            x_range: (min_x, max_x),
795            y_range: (min_y, max_y),
796            memory_usage: self.estimated_memory_usage(),
797        }
798    }
799
800    /// Estimate memory usage in bytes
801    pub fn estimated_memory_usage(&self) -> usize {
802        std::mem::size_of::<f64>() * (self.x_data.len() + self.y_data.len())
803            + self
804                .vertices
805                .as_ref()
806                .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>())
807            + self.gpu_vertex_count.unwrap_or(0) * std::mem::size_of::<Vertex>()
808    }
809}
810
811fn marker_style_code(kind: ScatterMarkerStyle) -> f32 {
812    match kind {
813        ScatterMarkerStyle::Circle => 0.0,
814        ScatterMarkerStyle::Square => 1.0,
815        ScatterMarkerStyle::Triangle => 2.0,
816        ScatterMarkerStyle::Diamond => 3.0,
817        ScatterMarkerStyle::Plus => 4.0,
818        ScatterMarkerStyle::Cross => 5.0,
819        ScatterMarkerStyle::Star => 6.0,
820        ScatterMarkerStyle::Hexagon => 7.0,
821    }
822}
823
824/// Plot performance and data statistics
825#[derive(Debug, Clone)]
826pub struct PlotStatistics {
827    pub point_count: usize,
828    pub x_range: (f64, f64),
829    pub y_range: (f64, f64),
830    pub memory_usage: usize,
831}
832
833/// MATLAB-compatible line plot creation utilities
834pub mod matlab_compat {
835    use super::*;
836
837    /// Create a simple line plot (equivalent to MATLAB's `plot(x, y)`)
838    pub fn plot(x: Vec<f64>, y: Vec<f64>) -> Result<LinePlot, String> {
839        LinePlot::new(x, y)
840    }
841
842    /// Create a line plot with specified color (`plot(x, y, 'r')`)
843    pub fn plot_with_color(x: Vec<f64>, y: Vec<f64>, color: &str) -> Result<LinePlot, String> {
844        let color_vec = parse_matlab_color(color)?;
845        Ok(LinePlot::new(x, y)?.with_style(color_vec, 1.0, LineStyle::Solid))
846    }
847
848    /// Parse MATLAB color specifications
849    fn parse_matlab_color(color: &str) -> Result<Vec4, String> {
850        match color {
851            "r" | "red" => Ok(Vec4::new(1.0, 0.0, 0.0, 1.0)),
852            "g" | "green" => Ok(Vec4::new(0.0, 1.0, 0.0, 1.0)),
853            "b" | "blue" => Ok(Vec4::new(0.0, 0.0, 1.0, 1.0)),
854            "c" | "cyan" => Ok(Vec4::new(0.0, 1.0, 1.0, 1.0)),
855            "m" | "magenta" => Ok(Vec4::new(1.0, 0.0, 1.0, 1.0)),
856            "y" | "yellow" => Ok(Vec4::new(1.0, 1.0, 0.0, 1.0)),
857            "k" | "black" => Ok(Vec4::new(0.0, 0.0, 0.0, 1.0)),
858            "w" | "white" => Ok(Vec4::new(1.0, 1.0, 1.0, 1.0)),
859            _ => Err(format!("Unknown color: {color}")),
860        }
861    }
862}
863
864#[cfg(test)]
865mod tests {
866    use super::*;
867
868    #[test]
869    fn test_line_plot_creation() {
870        let x = vec![0.0, 1.0, 2.0, 3.0];
871        let y = vec![0.0, 1.0, 0.0, 1.0];
872
873        let plot = LinePlot::new(x.clone(), y.clone()).unwrap();
874
875        assert_eq!(plot.x_data, x);
876        assert_eq!(plot.y_data, y);
877        assert_eq!(plot.len(), 4);
878        assert!(!plot.is_empty());
879        assert!(plot.visible);
880    }
881
882    #[test]
883    fn test_line_plot_data_validation() {
884        // Mismatched lengths should fail
885        let x = vec![0.0, 1.0, 2.0];
886        let y = vec![0.0, 1.0];
887        assert!(LinePlot::new(x, y).is_err());
888
889        // Empty data should fail
890        let empty_x: Vec<f64> = vec![];
891        let empty_y: Vec<f64> = vec![];
892        assert!(LinePlot::new(empty_x, empty_y).is_err());
893    }
894
895    #[test]
896    fn test_line_plot_styling() {
897        let x = vec![0.0, 1.0, 2.0];
898        let y = vec![1.0, 2.0, 1.5];
899        let color = Vec4::new(1.0, 0.0, 0.0, 1.0);
900
901        let plot = LinePlot::new(x, y)
902            .unwrap()
903            .with_style(color, 2.0, LineStyle::Dashed)
904            .with_label("Test Line");
905
906        assert_eq!(plot.color, color);
907        assert_eq!(plot.line_width, 2.0);
908        assert_eq!(plot.line_style, LineStyle::Dashed);
909        assert_eq!(plot.label, Some("Test Line".to_string()));
910    }
911
912    #[test]
913    fn test_line_plot_data_update() {
914        let mut plot = LinePlot::new(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
915
916        let new_x = vec![0.0, 0.5, 1.0, 1.5];
917        let new_y = vec![0.0, 0.25, 1.0, 2.25];
918
919        plot.update_data(new_x.clone(), new_y.clone()).unwrap();
920
921        assert_eq!(plot.x_data, new_x);
922        assert_eq!(plot.y_data, new_y);
923        assert_eq!(plot.len(), 4);
924    }
925
926    #[test]
927    fn test_line_plot_bounds() {
928        let x = vec![-1.0, 0.0, 1.0, 2.0];
929        let y = vec![-2.0, 0.0, 1.0, 3.0];
930
931        let mut plot = LinePlot::new(x, y).unwrap();
932        let bounds = plot.bounds();
933
934        assert_eq!(bounds.min.x, -1.0);
935        assert_eq!(bounds.max.x, 2.0);
936        assert_eq!(bounds.min.y, -2.0);
937        assert_eq!(bounds.max.y, 3.0);
938    }
939
940    #[test]
941    fn test_line_plot_vertex_generation() {
942        let x = vec![0.0, 1.0, 2.0];
943        let y = vec![0.0, 1.0, 0.0];
944
945        let mut plot = LinePlot::new(x, y).unwrap();
946        let vertices = plot.generate_vertices();
947
948        // Should have 2 line segments (4 vertices total)
949        assert_eq!(vertices.len(), 4);
950
951        // Check first line segment
952        assert_eq!(vertices[0].position, [0.0, 0.0, 0.0]);
953        assert_eq!(vertices[1].position, [1.0, 1.0, 0.0]);
954    }
955
956    #[test]
957    fn test_line_plot_render_data() {
958        let x = vec![0.0, 1.0, 2.0];
959        let y = vec![1.0, 2.0, 1.0];
960
961        let mut plot = LinePlot::new(x, y).unwrap();
962        let render_data = plot.render_data();
963
964        assert_eq!(render_data.pipeline_type, PipelineType::Lines);
965        assert_eq!(render_data.vertices.len(), 4); // 2 line segments
966        assert!(render_data.indices.is_none());
967        assert_eq!(render_data.draw_calls.len(), 1);
968    }
969
970    #[test]
971    fn test_line_plot_statistics() {
972        let x = vec![0.0, 1.0, 2.0, 3.0];
973        let y = vec![-1.0, 0.0, 1.0, 2.0];
974
975        let plot = LinePlot::new(x, y).unwrap();
976        let stats = plot.statistics();
977
978        assert_eq!(stats.point_count, 4);
979        assert_eq!(stats.x_range, (0.0, 3.0));
980        assert_eq!(stats.y_range, (-1.0, 2.0));
981        assert!(stats.memory_usage > 0);
982    }
983
984    #[test]
985    fn test_matlab_compat_colors() {
986        use super::matlab_compat::*;
987
988        let x = vec![0.0, 1.0];
989        let y = vec![0.0, 1.0];
990
991        let red_plot = plot_with_color(x.clone(), y.clone(), "r").unwrap();
992        assert_eq!(red_plot.color, Vec4::new(1.0, 0.0, 0.0, 1.0));
993
994        let blue_plot = plot_with_color(x.clone(), y.clone(), "blue").unwrap();
995        assert_eq!(blue_plot.color, Vec4::new(0.0, 0.0, 1.0, 1.0));
996
997        // Invalid color should fail
998        assert!(plot_with_color(x, y, "invalid").is_err());
999    }
1000
1001    #[test]
1002    fn marker_render_data_produces_point_draw_call() {
1003        let mut plot = LinePlot::new(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
1004        plot.set_marker(Some(LineMarkerAppearance {
1005            kind: ScatterMarkerStyle::Circle,
1006            size: 8.0,
1007            edge_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
1008            face_color: Vec4::new(1.0, 0.0, 0.0, 1.0),
1009            filled: true,
1010        }));
1011        let marker_data = plot.marker_render_data().expect("marker render data");
1012        assert_eq!(marker_data.pipeline_type, PipelineType::Points);
1013        assert_eq!(marker_data.draw_calls[0].vertex_count, 2);
1014    }
1015
1016    #[test]
1017    fn line_plot_handles_large_trace() {
1018        let n = 50_000;
1019        let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
1020        let y: Vec<f64> = (0..n).map(|i| (i as f64 * 0.001).sin()).collect();
1021        let mut plot = LinePlot::new(x, y).unwrap();
1022        let render_data = plot.render_data();
1023        assert_eq!(render_data.vertices.len(), (n - 1) * 2);
1024    }
1025}