Skip to main content

runmat_plot/plots/
errorbar.rs

1//! Error bar plot implementation.
2
3use 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}