Skip to content
Open
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
9 changes: 9 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2597,6 +2597,15 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"

[[package]]
name = "plot_span"
version = "0.1.0"
dependencies = [
"eframe",
"egui_plot",
"env_logger",
]

[[package]]
name = "png"
version = "0.18.0"
Expand Down
2 changes: 2 additions & 0 deletions egui_plot/src/items/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub use points::Points;
pub use polygon::Polygon;
use rect_elem::RectElement;
pub use series::Line;
pub use span::Span;
pub use text::Text;
pub use values::ClosestElem;
pub use values::LineStyle;
Expand All @@ -51,6 +52,7 @@ mod points;
mod polygon;
mod rect_elem;
mod series;
mod span;
mod text;
mod values;

Expand Down
294 changes: 294 additions & 0 deletions egui_plot/src/items/span.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
use crate::{Axis, utils::find_name_candidate};
use std::{f32::consts::PI, ops::RangeInclusive};

use egui::{
Align2, Color32, Pos2, Rect, Shape, Stroke, TextStyle, Ui, Vec2,
epaint::{PathStroke, TextShape},
pos2,
};
use emath::TSTransform;

use super::{
LineStyle, PlotBounds, PlotGeometry, PlotItem, PlotItemBase, PlotPoint, PlotTransform, rect_elem::highlighted_color,
};

/// Padding between the label of the span and both the edge of the view and the span borders.
/// For example, for a horizontal span, this is the padding between the top of the span label
/// and the top edge of the plot view, but also the margin between the left/right edges of the span
/// and the span label.
const LABEL_PADDING: f32 = 4.0;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add doc comment, which sides the padding applies to (I think all?)


/// A span covering a range on either axis.
#[derive(Clone, Debug, PartialEq)]
pub struct Span {
base: PlotItemBase,
axis: Axis,
range: RangeInclusive<f64>,
fill: Color32,
border_stroke: Stroke,
border_style: LineStyle,
label_align: Align2,
}

impl Span {
/// Create a new span covering the provided range on the X axis by default.
pub fn new(name: impl Into<String>, range: impl Into<RangeInclusive<f64>>) -> Self {
Self {
base: PlotItemBase::new(name.into()),
axis: Axis::X,
range: range.into(),
fill: Color32::TRANSPARENT,
border_stroke: Stroke::new(1.0, Color32::TRANSPARENT),
border_style: LineStyle::Solid,
label_align: Align2::CENTER_TOP,
}
}

/// Select which axis the span applies to. This also sets the label alignment.
/// If you want a different label alignment, you need to set it by calling `label_align` after
/// this call.
#[inline]
pub fn axis(mut self, axis: Axis) -> Self {
self.axis = axis;
match axis {
Axis::X => self.label_align = Align2::CENTER_TOP,
Axis::Y => self.label_align = Align2::LEFT_CENTER,
}
self
}

/// Set the range.
#[inline]
pub fn range(mut self, range: impl Into<RangeInclusive<f64>>) -> Self {
self.range = range.into();
self
}

/// Set the background fill color for the span.
#[inline]
pub fn fill(mut self, color: impl Into<Color32>) -> Self {
self.fill = color.into();
self
}

/// Set the stroke used for both span borders.
#[inline]
pub fn border(mut self, stroke: impl Into<Stroke>) -> Self {
self.border_stroke = stroke.into();
self
}

/// Convenience for updating the span border width.
#[inline]
pub fn border_width(mut self, width: impl Into<f32>) -> Self {
self.border_stroke.width = width.into();
self
}

/// Convenience for updating the span border color.
#[inline]
pub fn border_color(mut self, color: impl Into<Color32>) -> Self {
self.border_stroke.color = color.into();
self
}

/// Set the style for the span borders. Defaults to `LineStyle::Solid`.
#[inline]
pub fn border_style(mut self, style: LineStyle) -> Self {
self.border_style = style;
self
}

/// Set the label alignment within the span.
/// This should be called after any calls to `axis` as that would overwrite the label alignment
#[inline]
pub fn label_align(mut self, align: Align2) -> Self {
self.label_align = align;
self
}

#[inline]
pub(crate) fn fill_color(&self) -> Color32 {
self.fill
}

#[inline]
pub(crate) fn border_color_value(&self) -> Color32 {
self.border_stroke.color
}

fn range_sorted(&self) -> (f64, f64) {
let start = *self.range.start();
let end = *self.range.end();
if start <= end { (start, end) } else { (end, start) }
}

fn hline_points(value: f64, transform: &PlotTransform) -> Vec<Pos2> {
vec![
transform.position_from_point(&PlotPoint::new(transform.bounds().min[0], value)),
transform.position_from_point(&PlotPoint::new(transform.bounds().max[0], value)),
]
}

fn vline_points(value: f64, transform: &PlotTransform) -> Vec<Pos2> {
vec![
transform.position_from_point(&PlotPoint::new(value, transform.bounds().min[1])),
transform.position_from_point(&PlotPoint::new(value, transform.bounds().max[1])),
]
}

fn draw_border(&self, value: f64, stroke: Stroke, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
if stroke.color == Color32::TRANSPARENT || stroke.width <= 0.0 || !value.is_finite() {
return;
}

let line = match self.axis {
Axis::X => Self::vline_points(value, transform),
Axis::Y => Self::hline_points(value, transform),
};

self.border_style
.style_line(line, PathStroke::new(stroke.width, stroke.color), false, shapes);
}

fn available_width_for_name(&self, rect: &Rect) -> f32 {
match self.axis {
Axis::X => (rect.width() - 2.0 * LABEL_PADDING).max(0.0),
Axis::Y => (rect.height() - 2.0 * LABEL_PADDING).max(0.0),
}
}

fn draw_name(&self, ui: &Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>, span_rect: &Rect) {
let frame = *transform.frame();
let visible_rect = span_rect.intersect(frame);

let available_width = self.available_width_for_name(&visible_rect);
if available_width <= 0.0 {
return;
}

let font_id = TextStyle::Body.resolve(ui.style());
let text_color = ui.visuals().text_color();
let painter = ui.painter();

let name = find_name_candidate(&self.base.name, available_width, painter, &font_id);

let galley = painter.layout_no_wrap(name, font_id, text_color);

if galley.is_empty() {
return;
}

// Place text center point at origin and rotate for Y-axis.
let mut text_shape = match self.axis {
Axis::X => TextShape::new(pos2(-galley.size().x / 2.0, -galley.size().y / 2.0), galley, text_color),

// For spans on the Y axis we rotate the text by 90° around its center point
Axis::Y => TextShape::new(pos2(-galley.size().x / 2.0, -galley.size().y / 2.0), galley, text_color)
.with_angle_and_anchor(-PI / 2.0, Align2::CENTER_CENTER),
};

// Take into account the rotation of the text when calculating its position
let text_rect = text_shape.visual_bounding_rect();
let (width, height) = (text_rect.width(), text_rect.height());

// Calculate the position of the text based on the label alignment
let text_pos_x = match self.label_align {
Align2::LEFT_CENTER | Align2::LEFT_TOP | Align2::LEFT_BOTTOM => visible_rect.left() + LABEL_PADDING,
Align2::CENTER_CENTER | Align2::CENTER_TOP | Align2::CENTER_BOTTOM => visible_rect.center().x - width / 2.0,
Align2::RIGHT_CENTER | Align2::RIGHT_TOP | Align2::RIGHT_BOTTOM => {
visible_rect.right() - LABEL_PADDING - width
}
};

let text_pos_y = match self.label_align {
Align2::LEFT_TOP | Align2::CENTER_TOP | Align2::RIGHT_TOP => visible_rect.top() + LABEL_PADDING,
Align2::LEFT_CENTER | Align2::CENTER_CENTER | Align2::RIGHT_CENTER => {
visible_rect.center().y - height / 2.0
}
Align2::LEFT_BOTTOM | Align2::CENTER_BOTTOM | Align2::RIGHT_BOTTOM => {
visible_rect.bottom() - LABEL_PADDING - height
}
};

// Make sure to add half the width/height since the text position is at the center of the text shape
let text_pos = pos2(text_pos_x + width / 2.0, text_pos_y + height / 2.0);

text_shape.transform(TSTransform::from_translation(Vec2::new(text_pos.x, text_pos.y)));

shapes.push(text_shape.into());
}
}

impl PlotItem for Span {
fn shapes(&self, ui: &Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
let (range_min, range_max) = self.range_sorted();

let mut stroke = self.border_stroke;
let mut fill = self.fill;
if self.base.highlight {
(stroke, fill) = highlighted_color(stroke, fill);
}

let range_min = range_min.clamp(
transform.bounds().min[self.axis as usize],
transform.bounds().max[self.axis as usize],
);
let range_max = range_max.clamp(
transform.bounds().min[self.axis as usize],
transform.bounds().max[self.axis as usize],
);

let span_rect = match self.axis {
Axis::X => transform.rect_from_values(
&PlotPoint::new(range_min, transform.bounds().min[1]),
&PlotPoint::new(range_max, transform.bounds().max[1]),
),
Axis::Y => transform.rect_from_values(
&PlotPoint::new(transform.bounds().min[0], range_min),
&PlotPoint::new(transform.bounds().max[0], range_max),
),
};

if fill != Color32::TRANSPARENT && span_rect.is_positive() {
shapes.push(Shape::rect_filled(span_rect, 0.0, fill));
}

let mut border_values = vec![range_min, range_max];
if (range_max - range_min).abs() <= f64::EPSILON {
border_values.truncate(1);
}

for value in border_values {
self.draw_border(value, stroke, transform, shapes);
}

self.draw_name(ui, transform, shapes, &span_rect);
}

fn initialize(&mut self, _x_range: RangeInclusive<f64>) {}

fn color(&self) -> Color32 {
if self.fill != Color32::TRANSPARENT {
self.fill
} else {
self.border_stroke.color
}
}

fn geometry(&self) -> PlotGeometry<'_> {
PlotGeometry::None
}

fn bounds(&self) -> PlotBounds {
PlotBounds::NOTHING
}

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/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mod memory;
mod plot;
mod plot_ui;
mod transform;
mod utils;

use std::cmp::Ordering;
use std::ops::RangeInclusive;
Expand Down Expand Up @@ -54,6 +55,7 @@ pub use crate::items::PlotPoint;
pub use crate::items::PlotPoints;
pub use crate::items::Points;
pub use crate::items::Polygon;
pub use crate::items::Span;
pub use crate::items::Text;
pub use crate::items::VLine;
pub use crate::legend::ColorConflictHandling;
Expand Down
21 changes: 21 additions & 0 deletions egui_plot/src/plot_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::PlotBounds;
use crate::PlotItem;
use crate::PlotPoint;
use crate::PlotTransform;
use crate::Span;

/// Provides methods to interact with a plot while building it. It is the single
/// argument of the closure provided to [`Plot::show`]. See [`Plot`] for an
Expand Down Expand Up @@ -249,6 +250,26 @@ impl<'a> PlotUi<'a> {
self.items.push(Box::new(vline));
}

/// Add an axis-aligned span.
///
/// Spans fill the space between two values on one axis. If both the fill and border colors
/// are transparent, a color is auto-assigned.
pub fn span(&mut self, mut span: Span) {
let fill_is_transparent = span.fill_color() == Color32::TRANSPARENT;
let border_is_transparent = span.border_color_value() == Color32::TRANSPARENT;

// If no color was provided, automatically assign a color to the span
if fill_is_transparent && border_is_transparent {
let auto_color = self.auto_color();
span = span.fill(auto_color.gamma_multiply(0.15)).border_color(auto_color);
} else if border_is_transparent && !fill_is_transparent {
let fill_color = span.fill_color();
span = span.border_color(fill_color);
}

self.items.push(Box::new(span));
}

/// Add a box plot diagram.
pub fn box_plot(&mut self, mut box_plot: crate::BoxPlot) {
if box_plot.boxes.is_empty() {
Expand Down
Loading