1use crate::core::{
4 marker_shape_code, vertex_utils, AlphaMode, BoundingBox, DrawCall, GpuPackContext,
5 GpuVertexBuffer, Material, PipelineType, RenderData, Vertex,
6};
7use crate::gpu::errorbar::{ErrorBarGpuInputs, ErrorBarGpuParams};
8use crate::plots::line::{LineMarkerAppearance, LineStyle};
9use glam::{Vec3, Vec4};
10use log::warn;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ErrorBarOrientation {
14 Vertical,
15 Horizontal,
16 Both,
17}
18
19#[derive(Debug, Clone)]
20pub struct ErrorBar {
21 pub x: Vec<f64>,
22 pub y: Vec<f64>,
23 pub y_neg: Vec<f64>,
24 pub y_pos: Vec<f64>,
25 pub x_neg: Vec<f64>,
26 pub x_pos: Vec<f64>,
27 pub orientation: ErrorBarOrientation,
28
29 pub color: Vec4,
30 pub line_width: f32,
31 pub line_style: LineStyle,
32 pub cap_size: f32,
33 pub marker: Option<LineMarkerAppearance>,
34
35 pub label: Option<String>,
36 pub visible: bool,
37
38 vertices: Option<Vec<Vertex>>,
39 bounds: Option<BoundingBox>,
40 dirty: bool,
41 gpu_vertices: Option<GpuVertexBuffer>,
42 gpu_vertex_count: Option<usize>,
43 gpu_bounds: Option<BoundingBox>,
44 gpu_inputs: Option<ErrorBarGpuInputs>,
45 marker_vertices: Option<Vec<Vertex>>,
46 marker_gpu_vertices: Option<GpuVertexBuffer>,
47 marker_dirty: bool,
48}
49
50impl ErrorBar {
51 pub fn new_vertical(
52 x: Vec<f64>,
53 y: Vec<f64>,
54 y_neg: Vec<f64>,
55 y_pos: Vec<f64>,
56 ) -> Result<Self, String> {
57 let n = x.len();
58 if n == 0 || y.len() != n || y_neg.len() != n || y_pos.len() != n {
59 return Err("errorbar: input vectors must have equal non-zero length".to_string());
60 }
61 Ok(Self {
62 x,
63 y,
64 y_neg,
65 y_pos,
66 x_neg: vec![0.0; n],
67 x_pos: vec![0.0; n],
68 orientation: ErrorBarOrientation::Vertical,
69 color: Vec4::new(0.0, 0.0, 0.0, 1.0),
70 line_width: 1.0,
71 line_style: LineStyle::Solid,
72 cap_size: 6.0,
73 marker: Some(LineMarkerAppearance {
74 kind: crate::plots::scatter::MarkerStyle::Circle,
75 size: 6.0,
76 edge_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
77 face_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
78 filled: false,
79 }),
80 label: None,
81 visible: true,
82 vertices: None,
83 bounds: None,
84 dirty: true,
85 gpu_vertices: None,
86 gpu_vertex_count: None,
87 gpu_bounds: None,
88 gpu_inputs: None,
89 marker_vertices: None,
90 marker_gpu_vertices: None,
91 marker_dirty: true,
92 })
93 }
94
95 pub fn new_both(
96 x: Vec<f64>,
97 y: Vec<f64>,
98 x_neg: Vec<f64>,
99 x_pos: Vec<f64>,
100 y_neg: Vec<f64>,
101 y_pos: Vec<f64>,
102 ) -> Result<Self, String> {
103 let n = x.len();
104 if n == 0
105 || y.len() != n
106 || x_neg.len() != n
107 || x_pos.len() != n
108 || y_neg.len() != n
109 || y_pos.len() != n
110 {
111 return Err("errorbar: input vectors must have equal non-zero length".to_string());
112 }
113 let mut plot = Self::new_vertical(x, y, y_neg, y_pos)?;
114 plot.x_neg = x_neg;
115 plot.x_pos = x_pos;
116 plot.orientation = ErrorBarOrientation::Both;
117 Ok(plot)
118 }
119
120 pub fn with_style(
121 mut self,
122 color: Vec4,
123 line_width: f32,
124 line_style: LineStyle,
125 cap_size: f32,
126 ) -> Self {
127 self.color = color;
128 self.line_width = line_width.max(0.5);
129 self.line_style = line_style;
130 self.cap_size = cap_size.max(0.0);
131 self.dirty = true;
132 self.gpu_vertices = None;
133 self.gpu_vertex_count = None;
134 self.gpu_bounds = None;
135 self.gpu_inputs = None;
136 self.marker_gpu_vertices = None;
137 self.marker_dirty = true;
138 self
139 }
140
141 pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
142 self.label = Some(label.into());
143 self
144 }
145
146 pub fn set_visible(&mut self, visible: bool) {
147 self.visible = visible;
148 }
149
150 pub fn set_marker(&mut self, marker: Option<LineMarkerAppearance>) {
151 self.marker = marker;
152 self.marker_dirty = true;
153 if self.marker.is_none() {
154 self.marker_vertices = None;
155 self.marker_gpu_vertices = None;
156 }
157 }
158
159 pub fn set_marker_gpu_vertices(&mut self, buffer: Option<GpuVertexBuffer>) {
160 let has_gpu = buffer.is_some();
161 self.marker_gpu_vertices = buffer;
162 if has_gpu {
163 self.marker_vertices = None;
164 }
165 }
166
167 #[allow(clippy::too_many_arguments)]
168 pub fn from_gpu_buffer(
169 color: Vec4,
170 line_width: f32,
171 line_style: LineStyle,
172 cap_size: f32,
173 orientation: ErrorBarOrientation,
174 buffer: GpuVertexBuffer,
175 vertex_count: usize,
176 bounds: BoundingBox,
177 ) -> Self {
178 Self {
179 x: Vec::new(),
180 y: Vec::new(),
181 y_neg: Vec::new(),
182 y_pos: Vec::new(),
183 x_neg: Vec::new(),
184 x_pos: Vec::new(),
185 orientation,
186 color,
187 line_width,
188 line_style,
189 cap_size,
190 marker: None,
191 label: None,
192 visible: true,
193 vertices: None,
194 bounds: None,
195 dirty: false,
196 gpu_vertices: Some(buffer),
197 gpu_vertex_count: Some(vertex_count),
198 gpu_bounds: Some(bounds),
199 gpu_inputs: None,
200 marker_vertices: None,
201 marker_gpu_vertices: None,
202 marker_dirty: true,
203 }
204 }
205
206 #[allow(clippy::too_many_arguments)]
207 pub fn from_gpu_inputs(
208 color: Vec4,
209 line_width: f32,
210 line_style: LineStyle,
211 cap_size: f32,
212 orientation: ErrorBarOrientation,
213 inputs: ErrorBarGpuInputs,
214 bounds: BoundingBox,
215 ) -> Self {
216 Self {
217 x: Vec::new(),
218 y: Vec::new(),
219 y_neg: Vec::new(),
220 y_pos: Vec::new(),
221 x_neg: Vec::new(),
222 x_pos: Vec::new(),
223 orientation,
224 color,
225 line_width,
226 line_style,
227 cap_size,
228 marker: None,
229 label: None,
230 visible: true,
231 vertices: None,
232 bounds: None,
233 dirty: false,
234 gpu_vertices: None,
235 gpu_vertex_count: None,
236 gpu_bounds: Some(bounds),
237 gpu_inputs: Some(inputs),
238 marker_vertices: None,
239 marker_gpu_vertices: None,
240 marker_dirty: true,
241 }
242 }
243
244 fn cap_half_width_data(&self, bounds: &BoundingBox, viewport_px: Option<(u32, u32)>) -> f32 {
245 match viewport_px {
246 Some(vp) => {
247 let data_per_px = crate::core::data_units_per_px(bounds, vp);
248 self.cap_size.max(0.0) * 0.5 * data_per_px
249 }
250 None => self.cap_size.max(0.0) * 0.5,
251 }
252 }
253
254 fn generate_line_vertices_with_cap_half(&self, cap_half_data: f32) -> Vec<Vertex> {
255 let mut verts = Vec::new();
256 for i in 0..self.x.len() {
257 let xi = self.x[i] as f32;
258 let yi = self.y[i] as f32;
259 if !xi.is_finite() || !yi.is_finite() {
260 continue;
261 }
262 if matches!(
263 self.orientation,
264 ErrorBarOrientation::Vertical | ErrorBarOrientation::Both
265 ) {
266 let y0 = (self.y[i] - self.y_neg[i]) as f32;
267 let y1 = (self.y[i] + self.y_pos[i]) as f32;
268 if y0.is_finite() && y1.is_finite() && include_segment(i, self.line_style) {
269 verts.push(Vertex::new(Vec3::new(xi, y0, 0.0), self.color));
270 verts.push(Vertex::new(Vec3::new(xi, y1, 0.0), self.color));
271 if cap_half_data > 0.0 {
272 verts.push(Vertex::new(
273 Vec3::new(xi - cap_half_data, y0, 0.0),
274 self.color,
275 ));
276 verts.push(Vertex::new(
277 Vec3::new(xi + cap_half_data, y0, 0.0),
278 self.color,
279 ));
280 verts.push(Vertex::new(
281 Vec3::new(xi - cap_half_data, y1, 0.0),
282 self.color,
283 ));
284 verts.push(Vertex::new(
285 Vec3::new(xi + cap_half_data, y1, 0.0),
286 self.color,
287 ));
288 }
289 }
290 }
291 if matches!(
292 self.orientation,
293 ErrorBarOrientation::Horizontal | ErrorBarOrientation::Both
294 ) {
295 let x0 = (self.x[i] - self.x_neg[i]) as f32;
296 let x1 = (self.x[i] + self.x_pos[i]) as f32;
297 if x0.is_finite() && x1.is_finite() && include_segment(i, self.line_style) {
298 verts.push(Vertex::new(Vec3::new(x0, yi, 0.0), self.color));
299 verts.push(Vertex::new(Vec3::new(x1, yi, 0.0), self.color));
300 if cap_half_data > 0.0 {
301 verts.push(Vertex::new(
302 Vec3::new(x0, yi - cap_half_data, 0.0),
303 self.color,
304 ));
305 verts.push(Vertex::new(
306 Vec3::new(x0, yi + cap_half_data, 0.0),
307 self.color,
308 ));
309 verts.push(Vertex::new(
310 Vec3::new(x1, yi - cap_half_data, 0.0),
311 self.color,
312 ));
313 verts.push(Vertex::new(
314 Vec3::new(x1, yi + cap_half_data, 0.0),
315 self.color,
316 ));
317 }
318 }
319 }
320 }
321 verts
322 }
323
324 pub fn generate_vertices(&mut self) -> &Vec<Vertex> {
325 if self.gpu_vertices.is_some() {
326 if self.vertices.is_none() {
327 self.vertices = Some(Vec::new());
328 }
329 return self.vertices.as_ref().unwrap();
330 }
331 if self.dirty || self.vertices.is_none() {
332 let bounds = self.bounds();
333 let cap_half_data = self.cap_half_width_data(&bounds, None);
334 let verts = self.generate_line_vertices_with_cap_half(cap_half_data);
335 self.vertices = Some(verts);
336 self.dirty = false;
337 }
338 self.vertices.as_ref().unwrap()
339 }
340
341 pub fn marker_render_data(&mut self) -> Option<RenderData> {
342 let marker = self.marker.clone()?;
343 if let Some(gpu_vertices) = self.marker_gpu_vertices.clone() {
344 let vertex_count = gpu_vertices.vertex_count;
345 if vertex_count == 0 {
346 return None;
347 }
348 return Some(RenderData {
349 pipeline_type: PipelineType::Points,
350 vertices: Vec::new(),
351 indices: None,
352 gpu_vertices: Some(gpu_vertices),
353 bounds: None,
354 material: Material {
355 albedo: marker.face_color,
356 emissive: marker.edge_color,
357 roughness: 1.0,
358 metallic: marker_shape_code(marker.kind) as f32,
359 alpha_mode: if marker.face_color.w < 0.999 {
360 AlphaMode::Blend
361 } else {
362 AlphaMode::Opaque
363 },
364 ..Default::default()
365 },
366 draw_calls: vec![DrawCall {
367 vertex_offset: 0,
368 vertex_count,
369 index_offset: None,
370 index_count: None,
371 instance_count: 1,
372 }],
373 image: None,
374 });
375 }
376 if self.marker_dirty || self.marker_vertices.is_none() {
377 let mut vertices = Vec::new();
378 for (&x, &y) in self.x.iter().zip(self.y.iter()) {
379 let x = x as f32;
380 let y = y as f32;
381 if !x.is_finite() || !y.is_finite() {
382 continue;
383 }
384 vertices.push(Vertex {
385 position: [x, y, 0.0],
386 color: marker.face_color.to_array(),
387 normal: [0.0, 0.0, marker.size],
388 tex_coords: [0.0, 0.0],
389 });
390 }
391 self.marker_vertices = Some(vertices);
392 self.marker_dirty = false;
393 }
394 let vertices = self.marker_vertices.as_ref()?;
395 if vertices.is_empty() {
396 return None;
397 }
398 Some(RenderData {
399 pipeline_type: PipelineType::Points,
400 vertices: vertices.clone(),
401 indices: None,
402 gpu_vertices: None,
403 bounds: None,
404 material: Material {
405 albedo: marker.face_color,
406 emissive: marker.edge_color,
407 roughness: 1.0,
408 metallic: marker_shape_code(marker.kind) as f32,
409 alpha_mode: if marker.face_color.w < 0.999 {
410 AlphaMode::Blend
411 } else {
412 AlphaMode::Opaque
413 },
414 ..Default::default()
415 },
416 draw_calls: vec![DrawCall {
417 vertex_offset: 0,
418 vertex_count: vertices.len(),
419 index_offset: None,
420 index_count: None,
421 instance_count: 1,
422 }],
423 image: None,
424 })
425 }
426
427 pub fn bounds(&mut self) -> BoundingBox {
428 if let Some(bounds) = self.gpu_bounds {
429 return bounds;
430 }
431 if self.dirty || self.bounds.is_none() {
432 let mut min = Vec3::new(f32::INFINITY, f32::INFINITY, 0.0);
433 let mut max = Vec3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, 0.0);
434 for i in 0..self.x.len() {
435 let xi = self.x[i] as f32;
436 let yi = self.y[i] as f32;
437 if !xi.is_finite() || !yi.is_finite() {
438 continue;
439 }
440 min.x = min
441 .x
442 .min(xi - self.x_neg.get(i).copied().unwrap_or(0.0) as f32);
443 max.x = max
444 .x
445 .max(xi + self.x_pos.get(i).copied().unwrap_or(0.0) as f32);
446 min.y = min
447 .y
448 .min(yi - self.y_neg.get(i).copied().unwrap_or(0.0) as f32);
449 max.y = max
450 .y
451 .max(yi + self.y_pos.get(i).copied().unwrap_or(0.0) as f32);
452 }
453 if self.cap_size > 0.0 {
454 let range_bounds = BoundingBox::new(min, max);
455 let half = self.cap_half_width_data(&range_bounds, None);
456 min.x -= half;
457 max.x += half;
458 min.y -= half;
459 max.y += half;
460 }
461 if !min.x.is_finite() {
462 min = Vec3::ZERO;
463 max = Vec3::ZERO;
464 }
465 self.bounds = Some(BoundingBox::new(min, max));
466 }
467 self.bounds.unwrap()
468 }
469
470 pub fn render_data(&mut self) -> RenderData {
471 let bounds = self.bounds();
472 let (vertices, vertex_count, gpu_vertices) = if self.gpu_vertices.is_some() {
473 (
474 Vec::new(),
475 self.gpu_vertex_count.unwrap_or(0),
476 self.gpu_vertices.clone(),
477 )
478 } else {
479 let vertices = self.generate_vertices().clone();
480 let count = vertices.len();
481 (vertices, count, None)
482 };
483 RenderData {
484 pipeline_type: PipelineType::Lines,
485 vertices,
486 indices: None,
487 gpu_vertices,
488 bounds: Some(bounds),
489 material: Material {
490 albedo: self.color,
491 roughness: self.line_width,
492 ..Default::default()
493 },
494 draw_calls: vec![DrawCall {
495 vertex_offset: 0,
496 vertex_count,
497 index_offset: None,
498 index_count: None,
499 instance_count: 1,
500 }],
501 image: None,
502 }
503 }
504
505 fn orientation_code(&self) -> u32 {
506 match self.orientation {
507 ErrorBarOrientation::Vertical => 0,
508 ErrorBarOrientation::Horizontal => 1,
509 ErrorBarOrientation::Both => 2,
510 }
511 }
512
513 fn pack_gpu_vertices_if_needed(
514 &mut self,
515 gpu: &GpuPackContext<'_>,
516 viewport_px: (u32, u32),
517 ) -> Result<(), String> {
518 if self.gpu_vertices.is_some() {
519 return Ok(());
520 }
521 let Some(inputs) = self.gpu_inputs.as_ref() else {
522 return Ok(());
523 };
524 let bounds = self
525 .gpu_bounds
526 .ok_or_else(|| "errorbar: missing GPU bounds".to_string())?;
527 let data_per_px = crate::core::data_units_per_px(&bounds, viewport_px);
528 let cap_size_data = self.cap_size.max(0.0) * data_per_px;
529 let orientation = self.orientation_code();
530 let packed = crate::gpu::errorbar::pack_vertical_vertices(
531 gpu.device,
532 gpu.queue,
533 inputs,
534 &ErrorBarGpuParams {
535 color: self.color,
536 cap_size_data,
537 line_style: self.line_style,
538 orientation,
539 },
540 )?;
541 let segments_per_point = match orientation {
542 0 | 1 => 6usize,
543 _ => 12usize,
544 };
545 self.gpu_vertex_count = Some(inputs.len as usize * segments_per_point);
546 self.gpu_vertices = Some(packed);
547 Ok(())
548 }
549
550 pub fn render_data_with_viewport_gpu(
551 &mut self,
552 viewport_px: Option<(u32, u32)>,
553 gpu: Option<&GpuPackContext<'_>>,
554 ) -> RenderData {
555 if self.gpu_inputs.is_some() && self.gpu_vertices.is_none() {
556 if let (Some(gpu), Some(vp)) = (gpu, viewport_px) {
557 if let Err(err) = self.pack_gpu_vertices_if_needed(gpu, vp) {
558 warn!("errorbar gpu pack failed: {err}");
559 }
560 }
561 }
562 self.render_data_with_viewport(viewport_px)
563 }
564
565 pub fn render_data_with_viewport(&mut self, viewport_px: Option<(u32, u32)>) -> RenderData {
566 if self.gpu_vertices.is_some() {
567 return self.render_data();
568 }
569
570 let bounds = self.bounds();
571 let (vertices, vertex_count, pipeline_type) = if self.line_width > 1.0 {
572 let Some(viewport_px) = viewport_px else {
573 return self.render_data();
574 };
575 let data_per_px = crate::core::data_units_per_px(&bounds, viewport_px);
576 let width_data = self.line_width.max(0.1) * data_per_px;
577 let cap_half_data = self.cap_half_width_data(&bounds, Some(viewport_px));
578 let verts = self.generate_line_vertices_with_cap_half(cap_half_data);
579 let mut thick = Vec::new();
580 for segment in verts.chunks_exact(2) {
581 let x = [segment[0].position[0] as f64, segment[1].position[0] as f64];
582 let y = [segment[0].position[1] as f64, segment[1].position[1] as f64];
583 let color = Vec4::from_array(segment[0].color);
584 thick.extend(vertex_utils::create_thick_polyline(
585 &x, &y, color, width_data,
586 ));
587 }
588 let count = thick.len();
589 (thick, count, PipelineType::Triangles)
590 } else {
591 let verts = self.generate_vertices().clone();
592 let count = verts.len();
593 (verts, count, PipelineType::Lines)
594 };
595 RenderData {
596 pipeline_type,
597 vertices,
598 indices: None,
599 gpu_vertices: None,
600 bounds: Some(bounds),
601 material: Material {
602 albedo: self.color,
603 roughness: self.line_width.max(0.0),
604 ..Default::default()
605 },
606 draw_calls: vec![DrawCall {
607 vertex_offset: 0,
608 vertex_count,
609 index_offset: None,
610 index_count: None,
611 instance_count: 1,
612 }],
613 image: None,
614 }
615 }
616
617 pub fn estimated_memory_usage(&self) -> usize {
618 self.vertices
619 .as_ref()
620 .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>())
621 + self
622 .marker_vertices
623 .as_ref()
624 .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>())
625 }
626}
627
628fn include_segment(index: usize, style: LineStyle) -> bool {
629 match style {
630 LineStyle::Solid => true,
631 LineStyle::Dashed => (index % 4) < 2,
632 LineStyle::Dotted => index.is_multiple_of(4),
633 LineStyle::DashDot => {
634 let m = index % 6;
635 m < 2 || m == 3
636 }
637 }
638}
639
640#[cfg(test)]
641mod tests {
642 use super::*;
643
644 #[test]
645 fn errorbar_bounds_include_caps() {
646 let mut plot = ErrorBar::new_vertical(
647 vec![0.0, 1.0],
648 vec![1.0, 2.0],
649 vec![0.5, 0.5],
650 vec![0.5, 0.5],
651 )
652 .unwrap()
653 .with_style(Vec4::ONE, 1.0, LineStyle::Solid, 10.0);
654 let bounds = plot.bounds();
655 assert!(bounds.max.x > 1.0);
656 assert!(bounds.min.x < 0.0);
657 }
658
659 #[test]
660 fn thick_errorbar_use_viewport_aware_triangles() {
661 let mut plot = ErrorBar::new_vertical(
662 vec![0.0, 1.0],
663 vec![1.0, 2.0],
664 vec![0.5, 0.5],
665 vec![0.5, 0.5],
666 )
667 .unwrap()
668 .with_style(Vec4::ONE, 2.0, LineStyle::Solid, 6.0);
669 let render = plot.render_data_with_viewport(Some((600, 400)));
670 assert_eq!(render.pipeline_type, PipelineType::Triangles);
671 assert!(!render.vertices.is_empty());
672 }
673}