Skip to content

Commit e6fb403

Browse files
Add FilledArea plot item (#202)
This adds a new FilledArea plot item that fills the area between two lines defined by x values and corresponding y_min and y_max values. Useful for visualizing: - Confidence intervals - Min-max ranges - Uncertainty bands Features: - Customizable fill color (Color32) - Optional stroke around boundaries - Follows existing plot item patterns # Added items to the public API ``` +pub struct egui_plot::FilledArea +impl egui_plot::FilledArea ``` --------- Co-authored-by: Eshaan <[email protected]>
1 parent b066072 commit e6fb403

File tree

16 files changed

+415
-4
lines changed

16 files changed

+415
-4
lines changed

.github/workflows/rust.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ jobs:
144144

145145
- name: Upload artifacts
146146
uses: actions/upload-artifact@v4
147-
if: always()
147+
if: failure()
148148
with:
149149
name: test-results
150-
path: "*.png"
150+
path: "**/*.png"

Cargo.lock

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,7 @@ dependencies = [
899899
"egui_chip",
900900
"env_logger",
901901
"examples_utils",
902+
"filled_area",
902903
"heatmap",
903904
"histogram",
904905
"image",
@@ -1350,6 +1351,16 @@ dependencies = [
13501351
"simd-adler32",
13511352
]
13521353

1354+
[[package]]
1355+
name = "filled_area"
1356+
version = "0.1.0"
1357+
dependencies = [
1358+
"eframe",
1359+
"egui_plot",
1360+
"env_logger",
1361+
"examples_utils",
1362+
]
1363+
13531364
[[package]]
13541365
name = "find-msvc-tools"
13551366
version = "0.1.1"

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ borrow_points = { version = "0.1.0", path = "examples/borrow_points" }
2626
box_plot = { version = "0.1.0", path = "examples/box_plot" }
2727
custom_axes = { version = "0.1.0", path = "examples/custom_axes" }
2828
custom_plot_manipulation = { version = "0.1.0", path = "examples/custom_plot_manipulation" }
29+
filled_area = { version = "0.1.0", path = "examples/filled_area" }
2930
heatmap = { version = "0.1.0", path = "examples/heatmap" }
3031
histogram = { version = "0.1.0", path = "examples/histogram" }
3132
interaction = { version = "0.1.0", path = "examples/interaction" }

demo/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ borrow_points.workspace = true
3232
box_plot.workspace = true
3333
custom_axes.workspace = true
3434
custom_plot_manipulation.workspace = true
35+
filled_area.workspace = true
3536
heatmap.workspace = true
3637
histogram.workspace = true
3738
interaction.workspace = true

demo/src/app.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ impl eframe::App for DemoGallery {
2929
impl DemoGallery {
3030
// Width of a column in the thumbnails panel.
3131
// TODO(#193): I don't know what units this corresponds to, and should be
32-
// cleaned up.
32+
// cleaned up...
3333
const COL_WIDTH: f32 = 128.0;
3434

3535
pub fn new(ctx: &egui::Context) -> Self {
@@ -38,6 +38,7 @@ impl DemoGallery {
3838
Box::new(box_plot::BoxPlotExample::default()),
3939
Box::new(custom_axes::CustomAxesExample::default()),
4040
Box::new(custom_plot_manipulation::CustomPlotManipulationExample::default()),
41+
Box::new(filled_area::FilledAreaExample::default()),
4142
Box::new(heatmap::HeatmapDemo::default()),
4243
Box::new(histogram::HistogramExample::default()),
4344
Box::new(interaction::InteractionExample::default()),

egui_plot/src/items/filled_area.rs

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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+
}

egui_plot/src/items/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub use crate::items::bar_chart::BarChart;
2626
pub use crate::items::box_plot::BoxElem;
2727
pub use crate::items::box_plot::BoxPlot;
2828
pub use crate::items::box_plot::BoxSpread;
29+
pub use crate::items::filled_area::FilledArea;
2930
pub use crate::items::heatmap::Heatmap;
3031
pub use crate::items::line::HLine;
3132
pub use crate::items::line::VLine;
@@ -43,6 +44,7 @@ use crate::rect_elem::RectElement;
4344
mod arrows;
4445
mod bar_chart;
4546
mod box_plot;
47+
mod filled_area;
4648
mod heatmap;
4749
mod line;
4850
mod plot_image;

egui_plot/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub use crate::items::BoxElem;
4747
pub use crate::items::BoxPlot;
4848
pub use crate::items::BoxSpread;
4949
pub use crate::items::ClosestElem;
50+
pub use crate::items::FilledArea;
5051
pub use crate::items::HLine;
5152
pub use crate::items::Heatmap;
5253
pub use crate::items::Line;

examples/filled_area/Cargo.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "filled_area"
3+
version = "0.1.0"
4+
license.workspace = true
5+
edition.workspace = true
6+
rust-version.workspace = true
7+
publish = false
8+
9+
[lints]
10+
workspace = true
11+
12+
[dependencies]
13+
eframe = { workspace = true, features = ["default"] }
14+
egui_plot.workspace = true
15+
env_logger = { version = "0.11.6", default-features = false, features = [
16+
"auto-color",
17+
"humantime",
18+
] }
19+
examples_utils.workspace = true
20+
21+
[package.metadata.cargo-shear]
22+
ignored = [
23+
"env_logger",
24+
] # used by make_main! macro

examples/filled_area/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Filled Area Example
2+
3+
This example demonstrates the `FilledArea` plot item which fills the area between two lines.
4+
5+
## Features
6+
7+
- Plots a sine wave with an adjustable confidence band
8+
- Interactive controls to adjust upper and lower bounds
9+
- Shows how to visualize uncertainty and ranges
10+
11+
## Usage
12+
13+
The example shows `sin(x)` with adjustable bounds:
14+
- **delta lower**: offset for the lower boundary (`sin(x) - delta_lower`)
15+
- **delta upper**: offset for the upper boundary (`sin(x) + delta_upper`)
16+
- **points**: number of sampling points
17+
18+
## Running
19+
20+
```bash
21+
cargo run -p filled_area
22+
```

0 commit comments

Comments
 (0)