|
| 1 | +use std::ops::RangeInclusive; |
| 2 | +use std::sync::Arc; |
| 3 | + |
| 4 | +use egui::Color32; |
| 5 | +use egui::Id; |
| 6 | +use egui::Mesh; |
| 7 | +use egui::Pos2; |
| 8 | +use egui::Shape; |
| 9 | +use egui::Stroke; |
| 10 | +use egui::Ui; |
| 11 | + |
| 12 | +use crate::axis::PlotTransform; |
| 13 | +use crate::bounds::PlotBounds; |
| 14 | +use crate::bounds::PlotPoint; |
| 15 | +use crate::colors::DEFAULT_FILL_ALPHA; |
| 16 | +use crate::data::PlotPoints; |
| 17 | +use crate::items::PlotGeometry; |
| 18 | +use crate::items::PlotItem; |
| 19 | +use crate::items::PlotItemBase; |
| 20 | + |
| 21 | +/// A filled area between two lines. |
| 22 | +/// |
| 23 | +/// Takes x-coordinates and corresponding `y_min` and `y_max` values, and fills |
| 24 | +/// the area between them. Useful for visualizing confidence intervals, ranges, |
| 25 | +/// and uncertainty bands. |
| 26 | +pub struct FilledArea { |
| 27 | + base: PlotItemBase, |
| 28 | + |
| 29 | + /// Lower boundary line (`x`, `y_min` pairs) |
| 30 | + lower_line: Vec<PlotPoint>, |
| 31 | + |
| 32 | + /// Upper boundary line (`x`, `y_max` pairs) |
| 33 | + upper_line: Vec<PlotPoint>, |
| 34 | + |
| 35 | + /// Fill color for the area |
| 36 | + fill_color: Color32, |
| 37 | + |
| 38 | + /// Optional stroke for the boundaries |
| 39 | + stroke: Option<Stroke>, |
| 40 | +} |
| 41 | + |
| 42 | +impl FilledArea { |
| 43 | + /// Create a new filled area between two lines. |
| 44 | + /// |
| 45 | + /// # Arguments |
| 46 | + /// * `name` - Name of this plot item (shows in legend) |
| 47 | + /// * `xs` - X coordinates |
| 48 | + /// * `ys_min` - Lower Y values |
| 49 | + /// * `ys_max` - Upper Y values |
| 50 | + /// |
| 51 | + /// All slices must have the same length. |
| 52 | + /// |
| 53 | + /// # Panics |
| 54 | + /// Panics if the slices don't have the same length. |
| 55 | + pub fn new(name: impl Into<String>, xs: &[f64], ys_min: &[f64], ys_max: &[f64]) -> Self { |
| 56 | + assert_eq!(xs.len(), ys_min.len(), "xs and ys_min must have the same length"); |
| 57 | + assert_eq!(xs.len(), ys_max.len(), "xs and ys_max must have the same length"); |
| 58 | + |
| 59 | + let lower_line: Vec<PlotPoint> = xs |
| 60 | + .iter() |
| 61 | + .zip(ys_min.iter()) |
| 62 | + .map(|(&x, &y)| PlotPoint::new(x, y)) |
| 63 | + .collect(); |
| 64 | + |
| 65 | + let upper_line: Vec<PlotPoint> = xs |
| 66 | + .iter() |
| 67 | + .zip(ys_max.iter()) |
| 68 | + .map(|(&x, &y)| PlotPoint::new(x, y)) |
| 69 | + .collect(); |
| 70 | + |
| 71 | + Self { |
| 72 | + base: PlotItemBase::new(name.into()), |
| 73 | + lower_line, |
| 74 | + upper_line, |
| 75 | + fill_color: Color32::from_gray(128).linear_multiply(DEFAULT_FILL_ALPHA), |
| 76 | + stroke: None, |
| 77 | + } |
| 78 | + } |
| 79 | + |
| 80 | + /// Set the fill color for the area. |
| 81 | + #[inline] |
| 82 | + pub fn fill_color(mut self, color: impl Into<Color32>) -> Self { |
| 83 | + self.fill_color = color.into(); |
| 84 | + self |
| 85 | + } |
| 86 | + |
| 87 | + /// Add a stroke around the boundaries of the filled area. |
| 88 | + #[inline] |
| 89 | + pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self { |
| 90 | + self.stroke = Some(stroke.into()); |
| 91 | + self |
| 92 | + } |
| 93 | + |
| 94 | + /// Name of this plot item. |
| 95 | + /// |
| 96 | + /// This name will show up in the plot legend, if legends are turned on. |
| 97 | + #[expect(clippy::needless_pass_by_value)] |
| 98 | + #[inline] |
| 99 | + pub fn name(mut self, name: impl ToString) -> Self { |
| 100 | + self.base_mut().name = name.to_string(); |
| 101 | + self |
| 102 | + } |
| 103 | + |
| 104 | + /// Highlight this plot item. |
| 105 | + #[inline] |
| 106 | + pub fn highlight(mut self, highlight: bool) -> Self { |
| 107 | + self.base_mut().highlight = highlight; |
| 108 | + self |
| 109 | + } |
| 110 | + |
| 111 | + /// Allow hovering this item in the plot. Default: `true`. |
| 112 | + #[inline] |
| 113 | + pub fn allow_hover(mut self, hovering: bool) -> Self { |
| 114 | + self.base_mut().allow_hover = hovering; |
| 115 | + self |
| 116 | + } |
| 117 | + |
| 118 | + /// Sets the id of this plot item. |
| 119 | + #[inline] |
| 120 | + pub fn id(mut self, id: impl Into<Id>) -> Self { |
| 121 | + self.base_mut().id = id.into(); |
| 122 | + self |
| 123 | + } |
| 124 | +} |
| 125 | + |
| 126 | +impl PlotItem for FilledArea { |
| 127 | + fn shapes(&self, _ui: &Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) { |
| 128 | + if self.lower_line.is_empty() { |
| 129 | + return; |
| 130 | + } |
| 131 | + |
| 132 | + let n = self.lower_line.len(); |
| 133 | + |
| 134 | + // Create a mesh for the filled area |
| 135 | + let mut mesh = Mesh::default(); |
| 136 | + mesh.reserve_triangles((n - 1) * 2); |
| 137 | + mesh.reserve_vertices(n * 2); |
| 138 | + |
| 139 | + // Add vertices for upper and lower lines |
| 140 | + for point in &self.upper_line { |
| 141 | + let pos = transform.position_from_point(point); |
| 142 | + mesh.colored_vertex(pos, self.fill_color); |
| 143 | + } |
| 144 | + for point in &self.lower_line { |
| 145 | + let pos = transform.position_from_point(point); |
| 146 | + mesh.colored_vertex(pos, self.fill_color); |
| 147 | + } |
| 148 | + |
| 149 | + // Create triangles connecting upper and lower lines |
| 150 | + for i in 0..(n - 1) { |
| 151 | + // Each quad is split into two triangles |
| 152 | + // Triangle 1: upper[i], lower[i], upper[i+1] |
| 153 | + mesh.add_triangle(i as u32, (n + i) as u32, (i + 1) as u32); |
| 154 | + // Triangle 2: lower[i], lower[i+1], upper[i+1] |
| 155 | + mesh.add_triangle((n + i) as u32, (n + i + 1) as u32, (i + 1) as u32); |
| 156 | + } |
| 157 | + |
| 158 | + shapes.push(Shape::Mesh(Arc::new(mesh))); |
| 159 | + |
| 160 | + // Draw optional stroke around boundaries |
| 161 | + if let Some(stroke) = self.stroke { |
| 162 | + // Upper boundary line |
| 163 | + let upper_points: Vec<Pos2> = self |
| 164 | + .upper_line |
| 165 | + .iter() |
| 166 | + .map(|point| transform.position_from_point(point)) |
| 167 | + .collect(); |
| 168 | + shapes.push(Shape::line(upper_points, stroke)); |
| 169 | + |
| 170 | + // Lower boundary line |
| 171 | + let lower_points: Vec<Pos2> = self |
| 172 | + .lower_line |
| 173 | + .iter() |
| 174 | + .map(|point| transform.position_from_point(point)) |
| 175 | + .collect(); |
| 176 | + shapes.push(Shape::line(lower_points, stroke)); |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + fn initialize(&mut self, _x_range: RangeInclusive<f64>) { |
| 181 | + // No initialization needed for explicit data |
| 182 | + } |
| 183 | + |
| 184 | + fn color(&self) -> Color32 { |
| 185 | + self.fill_color |
| 186 | + } |
| 187 | + |
| 188 | + fn geometry(&self) -> PlotGeometry<'_> { |
| 189 | + // Return all points (both min and max boundaries) for hit testing |
| 190 | + PlotGeometry::None |
| 191 | + } |
| 192 | + |
| 193 | + fn bounds(&self) -> PlotBounds { |
| 194 | + // Calculate bounds from all points |
| 195 | + let mut all_points = self.lower_line.clone(); |
| 196 | + all_points.extend(self.upper_line.iter()); |
| 197 | + PlotPoints::Owned(all_points).bounds() |
| 198 | + } |
| 199 | + |
| 200 | + fn base(&self) -> &PlotItemBase { |
| 201 | + &self.base |
| 202 | + } |
| 203 | + |
| 204 | + fn base_mut(&mut self) -> &mut PlotItemBase { |
| 205 | + &mut self.base |
| 206 | + } |
| 207 | +} |
0 commit comments