From 9688345f0a996f33846d82d464b737531a6180b1 Mon Sep 17 00:00:00 2001 From: Eshaan Date: Sun, 30 Nov 2025 21:20:22 +1100 Subject: [PATCH 1/5] Add FilledArea plot item to fill between two lines 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. API: FilledArea::new(name, &xs, &ys_min, &ys_max) Useful for visualizing: - Confidence intervals - Min-max ranges - Uncertainty bands Features: - Customizable fill color (Color32) - Optional stroke around boundaries - Follows existing plot item patterns Closes #190 Add filled_area example to demo gallery - Created filled_area example with sin(x) and adjustable bounds - Added controls for delta_lower and delta_upper offsets - Integrated into demo gallery - Added workspace dependencies Fix FilledArea rendering and export - Export FilledArea in lib.rs for public use - Fix polygon rendering by using Mesh instead of convex_polygon - Creates proper triangle strips to avoid connecting first/last points - Fills area correctly between upper and lower boundaries Fix Arc compilation error and add screenshots - Wrap Mesh in Arc as required by egui Shape::Mesh API - Fix unused variable warning (ui -> _ui) - Add std::sync::Arc import - Generate screenshots using UPDATE_SNAPSHOTS=1 cargo test - Screenshots now show actual filled area demo with interactive controls screenshot fixes tags fixes lint screenshots rm add some change --- Cargo.lock | 11 ++ Cargo.toml | 1 + demo/Cargo.toml | 1 + demo/src/app.rs | 3 +- egui_plot/src/items/filled_area.rs | 207 ++++++++++++++++++++++ egui_plot/src/items/mod.rs | 1 + egui_plot/src/lib.rs | 1 + examples/filled_area/Cargo.toml | 24 +++ examples/filled_area/README.md | 22 +++ examples/filled_area/screenshot.png | 3 + examples/filled_area/screenshot_thumb.png | 3 + examples/filled_area/src/app.rs | 87 +++++++++ examples/filled_area/src/lib.rs | 41 +++++ examples/filled_area/src/main.rs | 4 + 14 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 egui_plot/src/items/filled_area.rs create mode 100644 examples/filled_area/Cargo.toml create mode 100644 examples/filled_area/README.md create mode 100644 examples/filled_area/screenshot.png create mode 100644 examples/filled_area/screenshot_thumb.png create mode 100644 examples/filled_area/src/app.rs create mode 100644 examples/filled_area/src/lib.rs create mode 100644 examples/filled_area/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 4f55886b..e20ad54a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -899,6 +899,7 @@ dependencies = [ "egui_chip", "env_logger", "examples_utils", + "filled_area", "heatmap", "histogram", "image", @@ -1350,6 +1351,16 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "filled_area" +version = "0.1.0" +dependencies = [ + "eframe", + "egui_plot", + "env_logger", + "examples_utils", +] + [[package]] name = "find-msvc-tools" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 6f935039..ccd73410 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ borrow_points = { version = "0.1.0", path = "examples/borrow_points" } box_plot = { version = "0.1.0", path = "examples/box_plot" } custom_axes = { version = "0.1.0", path = "examples/custom_axes" } custom_plot_manipulation = { version = "0.1.0", path = "examples/custom_plot_manipulation" } +filled_area = { version = "0.1.0", path = "examples/filled_area" } heatmap = { version = "0.1.0", path = "examples/heatmap" } histogram = { version = "0.1.0", path = "examples/histogram" } interaction = { version = "0.1.0", path = "examples/interaction" } diff --git a/demo/Cargo.toml b/demo/Cargo.toml index d3db9e2b..54d3b443 100644 --- a/demo/Cargo.toml +++ b/demo/Cargo.toml @@ -32,6 +32,7 @@ borrow_points.workspace = true box_plot.workspace = true custom_axes.workspace = true custom_plot_manipulation.workspace = true +filled_area.workspace = true heatmap.workspace = true histogram.workspace = true interaction.workspace = true diff --git a/demo/src/app.rs b/demo/src/app.rs index 62ad1009..5458c44f 100644 --- a/demo/src/app.rs +++ b/demo/src/app.rs @@ -29,7 +29,7 @@ impl eframe::App for DemoGallery { impl DemoGallery { // Width of a column in the thumbnails panel. // TODO(#193): I don't know what units this corresponds to, and should be - // cleaned up. + // cleaned up... const COL_WIDTH: f32 = 128.0; pub fn new(ctx: &egui::Context) -> Self { @@ -38,6 +38,7 @@ impl DemoGallery { Box::new(box_plot::BoxPlotExample::default()), Box::new(custom_axes::CustomAxesExample::default()), Box::new(custom_plot_manipulation::CustomPlotManipulationExample::default()), + Box::new(filled_area::FilledAreaExample::default()), Box::new(heatmap::HeatmapDemo::default()), Box::new(histogram::HistogramExample::default()), Box::new(interaction::InteractionExample::default()), diff --git a/egui_plot/src/items/filled_area.rs b/egui_plot/src/items/filled_area.rs new file mode 100644 index 00000000..a4d536b4 --- /dev/null +++ b/egui_plot/src/items/filled_area.rs @@ -0,0 +1,207 @@ +use std::ops::RangeInclusive; +use std::sync::Arc; + +use egui::Color32; +use egui::Id; +use egui::Mesh; +use egui::Pos2; +use egui::Shape; +use egui::Stroke; +use egui::Ui; + +use super::DEFAULT_FILL_ALPHA; +use crate::PlotBounds; +use crate::PlotGeometry; +use crate::PlotItem; +use crate::PlotItemBase; +use crate::PlotPoint; +use crate::PlotPoints; +use crate::PlotTransform; + +/// A filled area between two lines. +/// +/// Takes x-coordinates and corresponding `y_min` and `y_max` values, and fills +/// the area between them. Useful for visualizing confidence intervals, ranges, +/// and uncertainty bands. +pub struct FilledArea { + base: PlotItemBase, + + /// Lower boundary line (`x`, `y_min` pairs) + lower_line: Vec, + + /// Upper boundary line (`x`, `y_max` pairs) + upper_line: Vec, + + /// Fill color for the area + fill_color: Color32, + + /// Optional stroke for the boundaries + stroke: Option, +} + +impl FilledArea { + /// Create a new filled area between two lines. + /// + /// # Arguments + /// * `name` - Name of this plot item (shows in legend) + /// * `xs` - X coordinates + /// * `ys_min` - Lower Y values + /// * `ys_max` - Upper Y values + /// + /// All slices must have the same length. + /// + /// # Panics + /// Panics if the slices don't have the same length. + pub fn new(name: impl Into, xs: &[f64], ys_min: &[f64], ys_max: &[f64]) -> Self { + assert_eq!(xs.len(), ys_min.len(), "xs and ys_min must have the same length"); + assert_eq!(xs.len(), ys_max.len(), "xs and ys_max must have the same length"); + + let lower_line: Vec = xs + .iter() + .zip(ys_min.iter()) + .map(|(&x, &y)| PlotPoint::new(x, y)) + .collect(); + + let upper_line: Vec = xs + .iter() + .zip(ys_max.iter()) + .map(|(&x, &y)| PlotPoint::new(x, y)) + .collect(); + + Self { + base: PlotItemBase::new(name.into()), + lower_line, + upper_line, + fill_color: Color32::from_gray(128).linear_multiply(DEFAULT_FILL_ALPHA), + stroke: None, + } + } + + /// Set the fill color for the area. + #[inline] + pub fn fill_color(mut self, color: impl Into) -> Self { + self.fill_color = color.into(); + self + } + + /// Add a stroke around the boundaries of the filled area. + #[inline] + pub fn stroke(mut self, stroke: impl Into) -> Self { + self.stroke = Some(stroke.into()); + self + } + + /// Name of this plot item. + /// + /// This name will show up in the plot legend, if legends are turned on. + #[expect(clippy::needless_pass_by_value)] + #[inline] + pub fn name(mut self, name: impl ToString) -> Self { + self.base_mut().name = name.to_string(); + self + } + + /// Highlight this plot item. + #[inline] + pub fn highlight(mut self, highlight: bool) -> Self { + self.base_mut().highlight = highlight; + self + } + + /// Allow hovering this item in the plot. Default: `true`. + #[inline] + pub fn allow_hover(mut self, hovering: bool) -> Self { + self.base_mut().allow_hover = hovering; + self + } + + /// Sets the id of this plot item. + #[inline] + pub fn id(mut self, id: impl Into) -> Self { + self.base_mut().id = id.into(); + self + } +} + +impl PlotItem for FilledArea { + fn shapes(&self, _ui: &Ui, transform: &PlotTransform, shapes: &mut Vec) { + if self.lower_line.is_empty() { + return; + } + + let n = self.lower_line.len(); + + // Create a mesh for the filled area + let mut mesh = Mesh::default(); + mesh.reserve_triangles((n - 1) * 2); + mesh.reserve_vertices(n * 2); + + // Add vertices for upper and lower lines + for point in &self.upper_line { + let pos = transform.position_from_point(point); + mesh.colored_vertex(pos, self.fill_color); + } + for point in &self.lower_line { + let pos = transform.position_from_point(point); + mesh.colored_vertex(pos, self.fill_color); + } + + // Create triangles connecting upper and lower lines + for i in 0..(n - 1) { + // Each quad is split into two triangles + // Triangle 1: upper[i], lower[i], upper[i+1] + mesh.add_triangle(i as u32, (n + i) as u32, (i + 1) as u32); + // Triangle 2: lower[i], lower[i+1], upper[i+1] + mesh.add_triangle((n + i) as u32, (n + i + 1) as u32, (i + 1) as u32); + } + + shapes.push(Shape::Mesh(Arc::new(mesh))); + + // Draw optional stroke around boundaries + if let Some(stroke) = self.stroke { + // Upper boundary line + let upper_points: Vec = self + .upper_line + .iter() + .map(|point| transform.position_from_point(point)) + .collect(); + shapes.push(Shape::line(upper_points, stroke)); + + // Lower boundary line + let lower_points: Vec = self + .lower_line + .iter() + .map(|point| transform.position_from_point(point)) + .collect(); + shapes.push(Shape::line(lower_points, stroke)); + } + } + + fn initialize(&mut self, _x_range: RangeInclusive) { + // No initialization needed for explicit data + } + + fn color(&self) -> Color32 { + self.fill_color + } + + fn geometry(&self) -> PlotGeometry<'_> { + // Return all points (both min and max boundaries) for hit testing + PlotGeometry::None + } + + fn bounds(&self) -> PlotBounds { + // Calculate bounds from all points + let mut all_points = self.lower_line.clone(); + all_points.extend(self.upper_line.iter()); + PlotPoints::Owned(all_points).bounds() + } + + fn base(&self) -> &PlotItemBase { + &self.base + } + + fn base_mut(&mut self) -> &mut PlotItemBase { + &mut self.base + } +} diff --git a/egui_plot/src/items/mod.rs b/egui_plot/src/items/mod.rs index 607cd37d..2e7a0249 100644 --- a/egui_plot/src/items/mod.rs +++ b/egui_plot/src/items/mod.rs @@ -43,6 +43,7 @@ use crate::rect_elem::RectElement; mod arrows; mod bar_chart; mod box_plot; +mod filled_area; mod heatmap; mod line; mod plot_image; diff --git a/egui_plot/src/lib.rs b/egui_plot/src/lib.rs index f8e68517..3f349955 100644 --- a/egui_plot/src/lib.rs +++ b/egui_plot/src/lib.rs @@ -47,6 +47,7 @@ pub use crate::items::BoxElem; pub use crate::items::BoxPlot; pub use crate::items::BoxSpread; pub use crate::items::ClosestElem; +pub use crate::items::FilledArea; pub use crate::items::HLine; pub use crate::items::Heatmap; pub use crate::items::Line; diff --git a/examples/filled_area/Cargo.toml b/examples/filled_area/Cargo.toml new file mode 100644 index 00000000..3a5f0b82 --- /dev/null +++ b/examples/filled_area/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "filled_area" +version = "0.1.0" +license.workspace = true +edition.workspace = true +rust-version.workspace = true +publish = false + +[lints] +workspace = true + +[dependencies] +eframe = { workspace = true, features = ["default"] } +egui_plot.workspace = true +env_logger = { version = "0.11.6", default-features = false, features = [ + "auto-color", + "humantime", +] } +examples_utils.workspace = true + +[package.metadata.cargo-shear] +ignored = [ + "env_logger", +] # used by make_main! macro diff --git a/examples/filled_area/README.md b/examples/filled_area/README.md new file mode 100644 index 00000000..59cec6a8 --- /dev/null +++ b/examples/filled_area/README.md @@ -0,0 +1,22 @@ +# Filled Area Example + +This example demonstrates the `FilledArea` plot item which fills the area between two lines. + +## Features + +- Plots a sine wave with an adjustable confidence band +- Interactive controls to adjust upper and lower bounds +- Shows how to visualize uncertainty and ranges + +## Usage + +The example shows `sin(x)` with adjustable bounds: +- **delta lower**: offset for the lower boundary (`sin(x) - delta_lower`) +- **delta upper**: offset for the upper boundary (`sin(x) + delta_upper`) +- **points**: number of sampling points + +## Running + +```bash +cargo run -p filled_area +``` diff --git a/examples/filled_area/screenshot.png b/examples/filled_area/screenshot.png new file mode 100644 index 00000000..439bb8f1 --- /dev/null +++ b/examples/filled_area/screenshot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37ce32164e02f3106d97b5211d958b9130671909466c0a6eefc0461ad4255995 +size 70933 diff --git a/examples/filled_area/screenshot_thumb.png b/examples/filled_area/screenshot_thumb.png new file mode 100644 index 00000000..5a3e48e9 --- /dev/null +++ b/examples/filled_area/screenshot_thumb.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6513df7fc03a4a43821fbcd71f4590b95eef02572b19ec6ea211b9a3da01375b +size 13220 diff --git a/examples/filled_area/src/app.rs b/examples/filled_area/src/app.rs new file mode 100644 index 00000000..08d7bc58 --- /dev/null +++ b/examples/filled_area/src/app.rs @@ -0,0 +1,87 @@ +use std::f64::consts::PI; + +use eframe::egui; +use eframe::egui::Response; +use egui_plot::FilledArea; +use egui_plot::Legend; +use egui_plot::Line; +use egui_plot::Plot; +use egui_plot::PlotPoints; + +pub struct FilledAreaExample { + delta_lower: f64, + delta_upper: f64, + num_points: usize, +} + +impl Default for FilledAreaExample { + fn default() -> Self { + Self { + delta_lower: 0.5, + delta_upper: 0.5, + num_points: 100, + } + } +} + +impl FilledAreaExample { + pub fn show_controls(&mut self, ui: &mut egui::Ui) -> Response { + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.label("Lower bound offset:"); + ui.add( + egui::Slider::new(&mut self.delta_lower, 0.0..=2.0) + .text("δ lower") + .step_by(0.1), + ); + }); + ui.vertical(|ui| { + ui.label("Upper bound offset:"); + ui.add( + egui::Slider::new(&mut self.delta_upper, 0.0..=2.0) + .text("δ upper") + .step_by(0.1), + ); + }); + ui.vertical(|ui| { + ui.label("Number of points:"); + ui.add(egui::Slider::new(&mut self.num_points, 10..=500).text("points")); + }); + }) + .response + } + + pub fn show_plot(&self, ui: &mut egui::Ui) -> Response { + // Generate x values + let xs: Vec = (0..self.num_points) + .map(|i| i as f64 * 4.0 * PI / self.num_points as f64) + .collect(); + + // Generate sin(x) and bounds + let ys: Vec = xs.iter().map(|&x| x.sin()).collect(); + let ys_min: Vec = ys.iter().map(|&y| y - self.delta_lower).collect(); + let ys_max: Vec = ys.iter().map(|&y| y + self.delta_upper).collect(); + + // Create the center line + let sin_line = Line::new( + "sin(x)", + xs.iter() + .zip(ys.iter()) + .map(|(&x, &y)| [x, y]) + .collect::>(), + ) + .color(egui::Color32::from_rgb(200, 100, 100)); + + // Create the filled area + let filled_area = FilledArea::new("sin(x) +/- deltas", &xs, &ys_min, &ys_max) + .fill_color(egui::Color32::from_rgba_unmultiplied(100, 200, 100, 50)); + + Plot::new("Filled Area Demo") + .legend(Legend::default()) + .show(ui, |plot_ui| { + plot_ui.add(filled_area); + plot_ui.line(sin_line); + }) + .response + } +} diff --git a/examples/filled_area/src/lib.rs b/examples/filled_area/src/lib.rs new file mode 100644 index 00000000..a9594529 --- /dev/null +++ b/examples/filled_area/src/lib.rs @@ -0,0 +1,41 @@ +#![doc = include_str!("../README.md")] + +use eframe::egui; +use examples_utils::PlotExample; + +mod app; +pub use app::FilledAreaExample; + +impl PlotExample for FilledAreaExample { + fn name(&self) -> &'static str { + "filled_area" + } + + fn title(&self) -> &'static str { + "Filled Area Demo" + } + + fn description(&self) -> &'static str { + "This example demonstrates how to create filled areas between two lines. It shows a sine wave with an adjustable confidence band around it, useful for visualizing uncertainty, ranges, and confidence intervals." + } + + fn tags(&self) -> &'static [&'static str] { + &["filled_area", "confidence_interval"] + } + + fn thumbnail_bytes(&self) -> &'static [u8] { + include_bytes!("../screenshot_thumb.png") + } + + fn code_bytes(&self) -> &'static [u8] { + include_bytes!("./app.rs") + } + + fn show_ui(&mut self, ui: &mut egui::Ui) -> egui::Response { + self.show_plot(ui) + } + + fn show_controls(&mut self, ui: &mut egui::Ui) -> egui::Response { + self.show_controls(ui) + } +} diff --git a/examples/filled_area/src/main.rs b/examples/filled_area/src/main.rs new file mode 100644 index 00000000..4b867172 --- /dev/null +++ b/examples/filled_area/src/main.rs @@ -0,0 +1,4 @@ +use examples_utils::make_main; +use filled_area::FilledAreaExample; + +make_main!(FilledAreaExample); From 635de148d672afdaed1499806788f06f6410b044 Mon Sep 17 00:00:00 2001 From: Michal Sustr Date: Sun, 30 Nov 2025 20:46:23 +0100 Subject: [PATCH 2/5] corrupt --- examples/filled_area/screenshot.png | 3 --- examples/filled_area/screenshot_thumb.png | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 examples/filled_area/screenshot.png diff --git a/examples/filled_area/screenshot.png b/examples/filled_area/screenshot.png deleted file mode 100644 index 439bb8f1..00000000 --- a/examples/filled_area/screenshot.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:37ce32164e02f3106d97b5211d958b9130671909466c0a6eefc0461ad4255995 -size 70933 diff --git a/examples/filled_area/screenshot_thumb.png b/examples/filled_area/screenshot_thumb.png index 5a3e48e9..06388162 100644 --- a/examples/filled_area/screenshot_thumb.png +++ b/examples/filled_area/screenshot_thumb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6513df7fc03a4a43821fbcd71f4590b95eef02572b19ec6ea211b9a3da01375b -size 13220 +oid sha256:87428fc522803d31065e7bce3cf03fe475096631e5e07bbd7a0fde60c4cf25c7 +size 2 From 838b60863748d3eb8f5490f6687245855106d76d Mon Sep 17 00:00:00 2001 From: Michal Sustr Date: Sun, 30 Nov 2025 20:46:40 +0100 Subject: [PATCH 3/5] try again --- examples/filled_area/screenshot.png | 3 +++ examples/filled_area/screenshot_thumb.png | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 examples/filled_area/screenshot.png diff --git a/examples/filled_area/screenshot.png b/examples/filled_area/screenshot.png new file mode 100644 index 00000000..439bb8f1 --- /dev/null +++ b/examples/filled_area/screenshot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37ce32164e02f3106d97b5211d958b9130671909466c0a6eefc0461ad4255995 +size 70933 diff --git a/examples/filled_area/screenshot_thumb.png b/examples/filled_area/screenshot_thumb.png index 06388162..5a3e48e9 100644 --- a/examples/filled_area/screenshot_thumb.png +++ b/examples/filled_area/screenshot_thumb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87428fc522803d31065e7bce3cf03fe475096631e5e07bbd7a0fde60c4cf25c7 -size 2 +oid sha256:6513df7fc03a4a43821fbcd71f4590b95eef02572b19ec6ea211b9a3da01375b +size 13220 From ec3482c50f9fbb1d4d944f18e9622aae87ec6ede Mon Sep 17 00:00:00 2001 From: Michal Sustr Date: Thu, 4 Dec 2025 21:33:01 +0100 Subject: [PATCH 4/5] rebase fixes --- egui_plot/src/items/filled_area.rs | 16 ++++++++-------- egui_plot/src/items/mod.rs | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/egui_plot/src/items/filled_area.rs b/egui_plot/src/items/filled_area.rs index a4d536b4..0ecf56d8 100644 --- a/egui_plot/src/items/filled_area.rs +++ b/egui_plot/src/items/filled_area.rs @@ -9,14 +9,14 @@ use egui::Shape; use egui::Stroke; use egui::Ui; -use super::DEFAULT_FILL_ALPHA; -use crate::PlotBounds; -use crate::PlotGeometry; -use crate::PlotItem; -use crate::PlotItemBase; -use crate::PlotPoint; -use crate::PlotPoints; -use crate::PlotTransform; +use crate::axis::PlotTransform; +use crate::bounds::PlotBounds; +use crate::bounds::PlotPoint; +use crate::colors::DEFAULT_FILL_ALPHA; +use crate::data::PlotPoints; +use crate::items::PlotGeometry; +use crate::items::PlotItem; +use crate::items::PlotItemBase; /// A filled area between two lines. /// diff --git a/egui_plot/src/items/mod.rs b/egui_plot/src/items/mod.rs index 2e7a0249..0eb4ed83 100644 --- a/egui_plot/src/items/mod.rs +++ b/egui_plot/src/items/mod.rs @@ -26,6 +26,7 @@ pub use crate::items::bar_chart::BarChart; pub use crate::items::box_plot::BoxElem; pub use crate::items::box_plot::BoxPlot; pub use crate::items::box_plot::BoxSpread; +pub use crate::items::filled_area::FilledArea; pub use crate::items::heatmap::Heatmap; pub use crate::items::line::HLine; pub use crate::items::line::VLine; From b7c4a0ea128a1b3525e58945db6811d2c9584f73 Mon Sep 17 00:00:00 2001 From: Michal Sustr Date: Thu, 4 Dec 2025 21:42:27 +0100 Subject: [PATCH 5/5] any pngs --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 23175d51..9dcac637 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -147,4 +147,4 @@ jobs: if: always() with: name: test-results - path: "*.png" + path: "**/*.png"