1use 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#[derive(Debug, Clone)]
16pub struct LinePlot {
17 pub x_data: Vec<f64>,
19 pub y_data: Vec<f64>,
20
21 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 pub label: Option<String>,
31 pub visible: bool,
32
33 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum LineStyle {
66 Solid,
67 Dashed,
68 Dotted,
69 DashDot,
70}
71
72#[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#[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 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), 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 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 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 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 pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
250 self.label = Some(label.into());
251 self
252 }
253
254 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 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 pub fn set_line_width(&mut self, width: f32) {
285 self.line_width = width.max(0.1); self.dirty = true;
287 self.invalidate_gpu_data();
288 }
289
290 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 pub fn set_marker(&mut self, marker: Option<LineMarkerAppearance>) {
299 self.marker = marker;
300 self.invalidate_marker_data();
301 }
302
303 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 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 pub fn set_visible(&mut self, visible: bool) {
319 self.visible = visible;
320 }
321
322 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 pub fn is_empty(&self) -> bool {
333 self.len() == 0
334 }
335
336 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 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 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 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, ¶ms)
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 let _ = self.pack_gpu_vertices_if_needed(gpu, vp);
509 }
510 }
511 self.render_data_with_viewport(viewport_px)
512 }
513
514 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 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 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 pub fn render_data_with_viewport(&mut self, viewport_px: Option<(u32, u32)>) -> RenderData {
593 if self.gpu_vertices.is_some() {
594 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 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 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 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 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#[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
833pub mod matlab_compat {
835 use super::*;
836
837 pub fn plot(x: Vec<f64>, y: Vec<f64>) -> Result<LinePlot, String> {
839 LinePlot::new(x, y)
840 }
841
842 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 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 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 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 assert_eq!(vertices.len(), 4);
950
951 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); 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 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}