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