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