1use crate::context::shared_wgpu_context;
6use crate::core::{
7 vertex_utils, BoundingBox, DrawCall, GpuVertexBuffer, Material, PipelineType, RenderData,
8 Vertex,
9};
10use crate::gpu::scatter2::Scatter2GpuInputs;
11use crate::gpu::util::readback_scalar_buffer_f64;
12use crate::plots::surface::ColorMap;
13use glam::{Vec3, Vec4};
14
15#[derive(Debug, Clone)]
17pub struct ScatterPlot {
18 pub x_data: Vec<f64>,
20 pub y_data: Vec<f64>,
21 pub theta_data: Option<Vec<f64>>,
22 pub r_data: Option<Vec<f64>>,
23
24 pub color: Vec4,
26 pub edge_color: Vec4,
27 pub edge_thickness: f32,
28 pub marker_size: f32,
29 pub marker_style: MarkerStyle,
30 pub per_point_sizes: Option<Vec<f32>>, pub per_point_colors: Option<Vec<Vec4>>, pub color_values: Option<Vec<f64>>, pub color_limits: Option<(f64, f64)>,
34 pub colormap: ColorMap,
35 pub filled: bool,
36 pub edge_color_from_vertex_colors: bool,
37
38 pub label: Option<String>,
40 pub visible: bool,
41
42 vertices: Option<Vec<Vertex>>,
44 bounds: Option<BoundingBox>,
45 dirty: bool,
46 gpu_vertices: Option<GpuVertexBuffer>,
47 gpu_point_count: Option<usize>,
48 gpu_inputs: Option<Scatter2GpuInputs>,
49 gpu_has_per_point_sizes: bool,
50 gpu_has_per_point_colors: bool,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum MarkerStyle {
56 Circle,
57 Square,
58 Triangle,
59 Diamond,
60 Plus,
61 Cross,
62 Star,
63 Hexagon,
64}
65
66impl Default for MarkerStyle {
67 fn default() -> Self {
68 Self::Circle
69 }
70}
71
72#[derive(Clone, Copy, Debug)]
73pub struct ScatterGpuStyle {
74 pub color: Vec4,
75 pub edge_color: Vec4,
76 pub edge_thickness: f32,
77 pub marker_size: f32,
78 pub marker_style: MarkerStyle,
79 pub filled: bool,
80 pub has_per_point_sizes: bool,
81 pub has_per_point_colors: bool,
82 pub edge_from_vertex_colors: bool,
83}
84
85impl ScatterPlot {
86 pub async fn export_scene_xy_data(&self) -> Result<(Vec<f64>, Vec<f64>), String> {
87 if !self.x_data.is_empty() && self.x_data.len() == self.y_data.len() {
88 return Ok((self.x_data.clone(), self.y_data.clone()));
89 }
90 if !self.x_data.is_empty() || !self.y_data.is_empty() {
91 return Err(format!(
92 "scatter plot has partial CPU source data: x has {} values, y has {} values",
93 self.x_data.len(),
94 self.y_data.len()
95 ));
96 }
97
98 if let Some(inputs) = &self.gpu_inputs {
99 let context = shared_wgpu_context().ok_or_else(|| {
100 "scatter plot has GPU source data but no shared WGPU context is installed"
101 .to_string()
102 })?;
103 let len = inputs.len as usize;
104 let x = readback_scalar_buffer_f64(
105 &context.device,
106 &context.queue,
107 &inputs.x_buffer,
108 len,
109 inputs.scalar,
110 )
111 .await?;
112 let y = readback_scalar_buffer_f64(
113 &context.device,
114 &context.queue,
115 &inputs.y_buffer,
116 len,
117 inputs.scalar,
118 )
119 .await?;
120 return Ok((x, y));
121 }
122
123 if self.gpu_vertices.is_some() {
124 return Err(
125 "scatter plot has GPU render vertices but no exportable source data".to_string(),
126 );
127 }
128
129 Ok((Vec::new(), Vec::new()))
130 }
131
132 pub fn new(x_data: Vec<f64>, y_data: Vec<f64>) -> Result<Self, String> {
134 if x_data.len() != y_data.len() {
135 return Err(format!(
136 "Data length mismatch: x_data has {} points, y_data has {} points",
137 x_data.len(),
138 y_data.len()
139 ));
140 }
141
142 if x_data.is_empty() {
143 return Err("Cannot create scatter plot with empty data".to_string());
144 }
145
146 Ok(Self {
147 x_data,
148 y_data,
149 theta_data: None,
150 r_data: None,
151 color: Vec4::new(1.0, 0.2, 0.2, 1.0), edge_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
153 edge_thickness: 1.0,
154 marker_size: 12.0,
155 marker_style: MarkerStyle::default(),
156 per_point_sizes: None,
157 per_point_colors: None,
158 color_values: None,
159 color_limits: None,
160 colormap: ColorMap::Parula,
161 filled: false,
162 edge_color_from_vertex_colors: false,
163 label: None,
164 visible: true,
165 vertices: None,
166 bounds: None,
167 dirty: true,
168 gpu_vertices: None,
169 gpu_point_count: None,
170 gpu_inputs: None,
171 gpu_has_per_point_sizes: false,
172 gpu_has_per_point_colors: false,
173 })
174 }
175
176 pub fn from_gpu_buffer(
178 buffer: GpuVertexBuffer,
179 point_count: usize,
180 bounds: BoundingBox,
181 style: ScatterGpuStyle,
182 ) -> Self {
183 Self {
184 x_data: Vec::new(),
185 y_data: Vec::new(),
186 theta_data: None,
187 r_data: None,
188 color: style.color,
189 edge_color: style.edge_color,
190 edge_thickness: style.edge_thickness,
191 marker_size: style.marker_size,
192 marker_style: style.marker_style,
193 per_point_sizes: None,
194 per_point_colors: None,
195 color_values: None,
196 color_limits: None,
197 colormap: ColorMap::Parula,
198 filled: style.filled,
199 edge_color_from_vertex_colors: style.edge_from_vertex_colors,
200 label: None,
201 visible: true,
202 vertices: None,
203 bounds: Some(bounds),
204 dirty: false,
205 gpu_vertices: Some(buffer),
206 gpu_point_count: Some(point_count),
207 gpu_inputs: None,
208 gpu_has_per_point_sizes: style.has_per_point_sizes,
209 gpu_has_per_point_colors: style.has_per_point_colors,
210 }
211 }
212
213 pub fn with_gpu_source_inputs(mut self, inputs: Scatter2GpuInputs) -> Self {
214 self.gpu_inputs = Some(inputs);
215 self
216 }
217
218 pub fn with_polar_data(mut self, theta: Vec<f64>, r: Vec<f64>) -> Self {
219 self.theta_data = Some(theta);
220 self.r_data = Some(r);
221 self
222 }
223
224 fn invalidate_gpu_vertices(&mut self) {
225 self.gpu_vertices = None;
226 self.gpu_point_count = None;
227 }
228
229 fn clear_gpu_source_inputs(&mut self) {
230 self.gpu_inputs = None;
231 self.gpu_has_per_point_sizes = false;
232 self.gpu_has_per_point_colors = false;
233 }
234
235 pub fn with_style(mut self, color: Vec4, marker_size: f32, marker_style: MarkerStyle) -> Self {
237 self.color = color;
238 self.marker_size = marker_size;
239 self.marker_style = marker_style;
240 self.dirty = true;
241 self.invalidate_gpu_vertices();
242 self.gpu_has_per_point_sizes = false;
243 self.gpu_has_per_point_colors = false;
244 self
245 }
246
247 pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
249 self.label = Some(label.into());
250 self
251 }
252
253 pub fn set_face_color(&mut self, color: Vec4) {
255 self.color = color;
256 self.dirty = true;
257 self.invalidate_gpu_vertices();
258 }
259 pub fn set_edge_color(&mut self, color: Vec4) {
261 self.edge_color = color;
262 self.dirty = true;
263 self.invalidate_gpu_vertices();
264 }
265 pub fn set_edge_color_from_vertex(&mut self, enabled: bool) {
266 self.edge_color_from_vertex_colors = enabled;
267 }
268 pub fn set_edge_thickness(&mut self, px: f32) {
270 self.edge_thickness = px.max(0.0);
271 self.dirty = true;
272 self.invalidate_gpu_vertices();
273 }
274 pub fn set_sizes(&mut self, sizes: Vec<f32>) {
275 self.per_point_sizes = Some(sizes);
276 self.dirty = true;
277 self.invalidate_gpu_vertices();
278 self.gpu_has_per_point_sizes = false;
279 }
280 pub fn set_colors(&mut self, colors: Vec<Vec4>) {
281 self.per_point_colors = Some(colors);
282 self.dirty = true;
283 self.invalidate_gpu_vertices();
284 self.gpu_has_per_point_colors = false;
285 }
286 pub fn set_color_values(&mut self, values: Vec<f64>, limits: Option<(f64, f64)>) {
287 self.color_values = Some(values);
288 self.color_limits = limits;
289 self.dirty = true;
290 self.invalidate_gpu_vertices();
291 self.gpu_has_per_point_colors = false;
292 }
293 pub fn with_colormap(mut self, cmap: ColorMap) -> Self {
294 self.colormap = cmap;
295 self.dirty = true;
296 self.invalidate_gpu_vertices();
297 self
298 }
299 pub fn set_filled(&mut self, filled: bool) {
300 self.filled = filled;
301 self.dirty = true;
302 self.invalidate_gpu_vertices();
303 }
304
305 pub fn update_data(&mut self, x_data: Vec<f64>, y_data: Vec<f64>) -> Result<(), String> {
307 if x_data.len() != y_data.len() {
308 return Err(format!(
309 "Data length mismatch: x_data has {} points, y_data has {} points",
310 x_data.len(),
311 y_data.len()
312 ));
313 }
314
315 if x_data.is_empty() {
316 return Err("Cannot update with empty data".to_string());
317 }
318
319 self.x_data = x_data;
320 self.y_data = y_data;
321 self.dirty = true;
322 self.invalidate_gpu_vertices();
323 self.clear_gpu_source_inputs();
324 Ok(())
325 }
326
327 pub fn set_color(&mut self, color: Vec4) {
329 self.color = color;
330 self.dirty = true;
331 self.invalidate_gpu_vertices();
332 }
333
334 pub fn set_marker_size(&mut self, size: f32) {
336 self.marker_size = size.max(0.1); self.dirty = true;
338 self.invalidate_gpu_vertices();
339 }
340
341 pub fn set_marker_style(&mut self, style: MarkerStyle) {
343 self.marker_style = style;
344 self.dirty = true;
345 self.invalidate_gpu_vertices();
346 }
347
348 pub fn set_visible(&mut self, visible: bool) {
350 self.visible = visible;
351 }
352
353 pub fn len(&self) -> usize {
355 if !self.x_data.is_empty() {
356 self.x_data.len()
357 } else {
358 self.gpu_point_count.unwrap_or(0)
359 }
360 }
361
362 pub fn is_empty(&self) -> bool {
364 self.len() == 0
365 }
366
367 pub fn generate_vertices(&mut self) -> &Vec<Vertex> {
369 if self.gpu_vertices.is_some() {
370 if self.vertices.is_none() {
371 self.vertices = Some(Vec::new());
372 }
373 return self.vertices.as_ref().unwrap();
374 }
375 if self.dirty || self.vertices.is_none() {
376 let base_color = self.color;
377 if self.per_point_colors.is_some() || self.color_values.is_some() { }
379 let mut verts =
380 vertex_utils::create_scatter_plot(&self.x_data, &self.y_data, base_color);
381 if let Some(ref colors) = self.per_point_colors {
383 let m = colors.len().min(verts.len());
384 for i in 0..m {
385 verts[i].color = colors[i].to_array();
386 }
387 } else if let Some(ref vals) = self.color_values {
388 let n = verts.len();
389 let (mut cmin, mut cmax) = if let Some(lims) = self.color_limits {
390 lims
391 } else {
392 let mut lo = f64::INFINITY;
393 let mut hi = f64::NEG_INFINITY;
394 for &v in vals {
395 if v.is_finite() {
396 if v < lo {
397 lo = v;
398 }
399 if v > hi {
400 hi = v;
401 }
402 }
403 }
404 if !lo.is_finite() || !hi.is_finite() || hi <= lo {
405 (0.0, 1.0)
406 } else {
407 (lo, hi)
408 }
409 };
410 if !(cmin.is_finite() && cmax.is_finite()) || cmax <= cmin {
411 cmin = 0.0;
412 cmax = 1.0;
413 }
414 let denom = (cmax - cmin).max(f64::EPSILON);
415 for (i, vert) in verts.iter_mut().enumerate().take(n) {
416 let t = ((vals[i] - cmin) / denom) as f32;
417 let rgb = self.colormap.map_value(t);
418 vert.color = [rgb.x, rgb.y, rgb.z, 1.0];
419 }
420 }
421 if let Some(ref sizes) = self.per_point_sizes {
423 for (i, vert) in verts.iter_mut().enumerate() {
424 let s = sizes.get(i).copied().unwrap_or(self.marker_size);
425 vert.normal[2] = s.max(1.0);
426 }
427 } else {
428 for v in &mut verts {
429 v.normal[2] = self.marker_size.max(1.0);
430 }
431 }
432 self.vertices = Some(verts);
433 self.dirty = false;
434 }
435 self.vertices.as_ref().unwrap()
436 }
437
438 pub fn bounds(&mut self) -> BoundingBox {
440 if self.gpu_vertices.is_some() {
441 return self.bounds.unwrap_or_default();
442 }
443 if self.dirty || self.bounds.is_none() {
444 let points: Vec<Vec3> = self
445 .x_data
446 .iter()
447 .zip(self.y_data.iter())
448 .map(|(&x, &y)| Vec3::new(x as f32, y as f32, 0.0))
449 .collect();
450 self.bounds = Some(BoundingBox::from_points(&points));
451 }
452 self.bounds.unwrap()
453 }
454
455 pub fn render_data(&mut self) -> RenderData {
457 let using_gpu = self.gpu_vertices.is_some();
458 let gpu_vertices = self.gpu_vertices.clone();
459 let bounds = self.bounds();
460 let (vertices, vertex_count) = if using_gpu {
461 let count = self
462 .gpu_point_count
463 .or_else(|| gpu_vertices.as_ref().map(|buf| buf.vertex_count))
464 .unwrap_or(0);
465 (Vec::new(), count)
466 } else {
467 let verts = self.generate_vertices().clone();
468 let count = verts.len();
469 (verts, count)
470 };
471
472 let mut material = Material {
473 albedo: self.color,
474 ..Default::default()
475 };
476 let is_multi_color = if using_gpu {
478 self.gpu_has_per_point_colors
479 || self.per_point_colors.is_some()
480 || self.color_values.is_some()
481 } else if vertices.is_empty() {
482 false
483 } else {
484 let first = vertices[0].color;
485 vertices.iter().any(|v| v.color != first)
486 };
487 if is_multi_color {
488 material.albedo.w = 0.0;
489 } else if self.filled {
490 material.albedo.w = 1.0;
491 }
492 material.emissive = self.edge_color; material.roughness = self.edge_thickness; material.metallic = match self.marker_style {
495 MarkerStyle::Circle => 0.0,
496 MarkerStyle::Square => 1.0,
497 MarkerStyle::Triangle => 2.0,
498 MarkerStyle::Diamond => 3.0,
499 MarkerStyle::Plus => 4.0,
500 MarkerStyle::Cross => 5.0,
501 MarkerStyle::Star => 6.0,
502 MarkerStyle::Hexagon => 7.0,
503 };
504 let has_vertex_colors = if using_gpu {
505 self.gpu_has_per_point_colors
506 } else {
507 self.per_point_colors.is_some() || self.color_values.is_some()
508 };
509 let use_vertex_edge_color = self.edge_color_from_vertex_colors && has_vertex_colors;
510 material.emissive.w = if use_vertex_edge_color { 0.0 } else { 1.0 };
511
512 let draw_call = DrawCall {
513 vertex_offset: 0,
514 vertex_count,
515 index_offset: None,
516 index_count: None,
517 instance_count: 1,
518 };
519
520 RenderData {
521 pipeline_type: PipelineType::Points,
522 vertices,
523 indices: None,
524 gpu_vertices,
525 bounds: Some(bounds),
526 material,
527 draw_calls: vec![draw_call],
528 image: None,
529 }
530 }
531
532 pub fn statistics(&self) -> PlotStatistics {
534 let (min_x, max_x, min_y, max_y) = if !self.x_data.is_empty() {
535 let (min_x, max_x) = self
536 .x_data
537 .iter()
538 .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), &x| {
539 (min.min(x), max.max(x))
540 });
541 let (min_y, max_y) = self
542 .y_data
543 .iter()
544 .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), &y| {
545 (min.min(y), max.max(y))
546 });
547 (min_x, max_x, min_y, max_y)
548 } else if let Some(bounds) = &self.bounds {
549 (
550 bounds.min.x as f64,
551 bounds.max.x as f64,
552 bounds.min.y as f64,
553 bounds.max.y as f64,
554 )
555 } else {
556 (0.0, 0.0, 0.0, 0.0)
557 };
558
559 PlotStatistics {
560 point_count: self.len(),
561 x_range: (min_x, max_x),
562 y_range: (min_y, max_y),
563 memory_usage: self.estimated_memory_usage(),
564 }
565 }
566
567 pub fn estimated_memory_usage(&self) -> usize {
569 std::mem::size_of::<f64>() * (self.x_data.len() + self.y_data.len())
570 + self
571 .vertices
572 .as_ref()
573 .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>())
574 + self.gpu_point_count.unwrap_or(0) * std::mem::size_of::<Vertex>()
575 }
576}
577
578#[derive(Debug, Clone)]
580pub struct PlotStatistics {
581 pub point_count: usize,
582 pub x_range: (f64, f64),
583 pub y_range: (f64, f64),
584 pub memory_usage: usize,
585}
586
587pub mod matlab_compat {
589 use super::*;
590
591 pub fn scatter(x: Vec<f64>, y: Vec<f64>) -> Result<ScatterPlot, String> {
593 ScatterPlot::new(x, y)
594 }
595
596 pub fn scatter_with_style(
598 x: Vec<f64>,
599 y: Vec<f64>,
600 size: f32,
601 color: &str,
602 ) -> Result<ScatterPlot, String> {
603 let color_vec = parse_matlab_color(color)?;
604 Ok(ScatterPlot::new(x, y)?.with_style(color_vec, size, MarkerStyle::Circle))
605 }
606
607 fn parse_matlab_color(color: &str) -> Result<Vec4, String> {
609 match color {
610 "r" | "red" => Ok(Vec4::new(1.0, 0.0, 0.0, 1.0)),
611 "g" | "green" => Ok(Vec4::new(0.0, 1.0, 0.0, 1.0)),
612 "b" | "blue" => Ok(Vec4::new(0.0, 0.0, 1.0, 1.0)),
613 "c" | "cyan" => Ok(Vec4::new(0.0, 1.0, 1.0, 1.0)),
614 "m" | "magenta" => Ok(Vec4::new(1.0, 0.0, 1.0, 1.0)),
615 "y" | "yellow" => Ok(Vec4::new(1.0, 1.0, 0.0, 1.0)),
616 "k" | "black" => Ok(Vec4::new(0.0, 0.0, 0.0, 1.0)),
617 "w" | "white" => Ok(Vec4::new(1.0, 1.0, 1.0, 1.0)),
618 _ => Err(format!("Unknown color: {color}")),
619 }
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626
627 #[test]
628 fn test_scatter_plot_creation() {
629 let x = vec![0.0, 1.0, 2.0, 3.0];
630 let y = vec![0.0, 1.0, 4.0, 9.0];
631
632 let plot = ScatterPlot::new(x.clone(), y.clone()).unwrap();
633
634 assert_eq!(plot.x_data, x);
635 assert_eq!(plot.y_data, y);
636 assert_eq!(plot.len(), 4);
637 assert!(!plot.is_empty());
638 assert!(plot.visible);
639 }
640
641 #[test]
642 fn test_scatter_plot_styling() {
643 let x = vec![0.0, 1.0, 2.0];
644 let y = vec![1.0, 2.0, 1.5];
645 let color = Vec4::new(0.0, 1.0, 0.0, 1.0);
646
647 let plot = ScatterPlot::new(x, y)
648 .unwrap()
649 .with_style(color, 5.0, MarkerStyle::Square)
650 .with_label("Test Scatter");
651
652 assert_eq!(plot.color, color);
653 assert_eq!(plot.marker_size, 5.0);
654 assert_eq!(plot.marker_style, MarkerStyle::Square);
655 assert_eq!(plot.label, Some("Test Scatter".to_string()));
656 }
657
658 #[test]
659 fn test_scatter_plot_render_data() {
660 let x = vec![0.0, 1.0, 2.0];
661 let y = vec![1.0, 2.0, 1.0];
662
663 let mut plot = ScatterPlot::new(x, y).unwrap();
664 let render_data = plot.render_data();
665
666 assert_eq!(render_data.pipeline_type, PipelineType::Points);
667 assert_eq!(render_data.vertices.len(), 3); assert!(render_data.indices.is_none());
669 assert_eq!(render_data.draw_calls.len(), 1);
670 }
671
672 #[test]
673 fn test_matlab_compat_scatter() {
674 use super::matlab_compat::*;
675
676 let x = vec![0.0, 1.0];
677 let y = vec![0.0, 1.0];
678
679 let basic_scatter = scatter(x.clone(), y.clone()).unwrap();
680 assert_eq!(basic_scatter.len(), 2);
681
682 let styled_scatter = scatter_with_style(x.clone(), y.clone(), 5.0, "g").unwrap();
683 assert_eq!(styled_scatter.color, Vec4::new(0.0, 1.0, 0.0, 1.0));
684 assert_eq!(styled_scatter.marker_size, 5.0);
685 }
686}