Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,4 @@ jobs:
if: always()
with:
name: test-results
path: "*.png"
path: "**/*.png"
11 changes: 11 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,7 @@ dependencies = [
"egui_chip",
"env_logger",
"examples_utils",
"filled_area",
"heatmap",
"histogram",
"image",
Expand Down Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
1 change: 1 addition & 0 deletions demo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion demo/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()),
Expand Down
207 changes: 207 additions & 0 deletions egui_plot/src/items/filled_area.rs
Original file line number Diff line number Diff line change
@@ -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 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.
///
/// 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<PlotPoint>,

/// Upper boundary line (`x`, `y_max` pairs)
upper_line: Vec<PlotPoint>,

/// Fill color for the area
fill_color: Color32,

/// Optional stroke for the boundaries
stroke: Option<Stroke>,
}

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<String>, 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<PlotPoint> = xs
.iter()
.zip(ys_min.iter())
.map(|(&x, &y)| PlotPoint::new(x, y))
.collect();

let upper_line: Vec<PlotPoint> = 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<Color32>) -> 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<Stroke>) -> 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<Id>) -> Self {
self.base_mut().id = id.into();
self
}
}

impl PlotItem for FilledArea {
fn shapes(&self, _ui: &Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
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<Pos2> = 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<Pos2> = 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<f64>) {
// 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
}
}
2 changes: 2 additions & 0 deletions egui_plot/src/items/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -43,6 +44,7 @@ use crate::rect_elem::RectElement;
mod arrows;
mod bar_chart;
mod box_plot;
mod filled_area;
mod heatmap;
mod line;
mod plot_image;
Expand Down
1 change: 1 addition & 0 deletions egui_plot/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
24 changes: 24 additions & 0 deletions examples/filled_area/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions examples/filled_area/README.md
Original file line number Diff line number Diff line change
@@ -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
```
3 changes: 3 additions & 0 deletions examples/filled_area/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions examples/filled_area/screenshot_thumb.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading