From 7c5a2d60c5d000f193bc7b16c5a53c059fee8259 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Tue, 6 Jul 2021 20:15:04 +0200 Subject: [PATCH] Plot: Line styles (#482) * added new line styles * update changelog * fix #524 Add missing functions to `HLine` and `VLine` * add functions for creating points and dashes from a line * apply suggestions * clippy fix * address comments --- CHANGELOG.md | 2 + egui/src/widgets/plot/items.rs | 201 ++++++++++++++++++----- egui/src/widgets/plot/mod.rs | 6 +- egui_demo_lib/src/apps/demo/plot_demo.rs | 28 +++- epaint/src/shape.rs | 89 +++++++++- 5 files changed, 283 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c617dea..183e59ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [ ## Unreleased ### Added ⭐ +* Plot: + * [Line styles](https://github.com/emilk/egui/pull/482) * [Progress bar](https://github.com/emilk/egui/pull/519) * `Grid::num_columns`: allow the last column to take up the rest of the space of the parent `Ui`. diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index d2dafea8..fffd15e8 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -34,6 +34,93 @@ impl Value { // ---------------------------------------------------------------------------- +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum LineStyle { + Solid, + Dotted { spacing: f32 }, + Dashed { length: f32 }, +} + +impl LineStyle { + pub fn dashed_loose() -> Self { + Self::Dashed { length: 10.0 } + } + + pub fn dashed_dense() -> Self { + Self::Dashed { length: 5.0 } + } + + pub fn dotted_loose() -> Self { + Self::Dotted { spacing: 10.0 } + } + + pub fn dotted_dense() -> Self { + Self::Dotted { spacing: 5.0 } + } + + fn style_line( + &self, + line: Vec, + mut stroke: Stroke, + highlight: bool, + shapes: &mut Vec, + ) { + match line.len() { + 0 => {} + 1 => { + let mut radius = stroke.width / 2.0; + if highlight { + radius *= 2f32.sqrt(); + } + shapes.push(Shape::circle_filled(line[0], radius, stroke.color)); + } + _ => { + match self { + LineStyle::Solid => { + if highlight { + stroke.width *= 2.0; + } + shapes.push(Shape::line(line, stroke)); + } + LineStyle::Dotted { spacing } => { + // Take the stroke width for the radius even though it's not "correct", otherwise + // the dots would become too small. + let mut radius = stroke.width; + if highlight { + radius *= 2f32.sqrt(); + } + shapes.extend(Shape::dotted_line(&line, stroke.color, *spacing, radius)) + } + LineStyle::Dashed { length } => { + if highlight { + stroke.width *= 2.0; + } + let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875 + shapes.extend(Shape::dashed_line( + &line, + stroke, + *length, + length * golden_ratio, + )) + } + } + } + } + } +} + +impl ToString for LineStyle { + fn to_string(&self) -> String { + match self { + LineStyle::Solid => "Solid".into(), + LineStyle::Dotted { spacing } => format!("Dotted{}Px", spacing), + LineStyle::Dashed { length } => format!("Dashed{}Px", length), + } + } +} + +// ---------------------------------------------------------------------------- + /// A horizontal line in a plot, filling the full width #[derive(Clone, Debug, PartialEq)] pub struct HLine { @@ -41,6 +128,7 @@ pub struct HLine { pub(super) stroke: Stroke, pub(super) name: String, pub(super) highlight: bool, + pub(super) style: LineStyle, } impl HLine { @@ -50,10 +138,17 @@ impl HLine { stroke: Stroke::new(1.0, Color32::TRANSPARENT), name: String::default(), highlight: false, + style: LineStyle::Solid, } } - /// Set the stroke. + /// Highlight this line in the plot by scaling up the line. + pub fn highlight(mut self) -> Self { + self.highlight = true; + self + } + + /// Add a stroke. pub fn stroke(mut self, stroke: impl Into) -> Self { self.stroke = stroke.into(); self @@ -71,6 +166,12 @@ impl HLine { self } + /// Set the line's style. Default is `LineStyle::Solid`. + pub fn style(mut self, style: LineStyle) -> Self { + self.style = style; + self + } + /// Name of this horizontal line. /// /// This name will show up in the plot legend, if legends are turned on. @@ -88,18 +189,16 @@ impl PlotItem for HLine { fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { let HLine { y, - mut stroke, + stroke, highlight, + style, .. } = self; - if *highlight { - stroke.width *= 2.0; - } - let points = [ + let points = vec![ transform.position_from_value(&Value::new(transform.bounds().min[0], *y)), transform.position_from_value(&Value::new(transform.bounds().max[0], *y)), ]; - shapes.push(Shape::line_segment(points, stroke)); + style.style_line(points, *stroke, *highlight, shapes); } fn initialize(&mut self, _x_range: RangeInclusive) {} @@ -139,6 +238,7 @@ pub struct VLine { pub(super) stroke: Stroke, pub(super) name: String, pub(super) highlight: bool, + pub(super) style: LineStyle, } impl VLine { @@ -148,10 +248,17 @@ impl VLine { stroke: Stroke::new(1.0, Color32::TRANSPARENT), name: String::default(), highlight: false, + style: LineStyle::Solid, } } - /// Set the stroke. + /// Highlight this line in the plot by scaling up the line. + pub fn highlight(mut self) -> Self { + self.highlight = true; + self + } + + /// Add a stroke. pub fn stroke(mut self, stroke: impl Into) -> Self { self.stroke = stroke.into(); self @@ -169,6 +276,12 @@ impl VLine { self } + /// Set the line's style. Default is `LineStyle::Solid`. + pub fn style(mut self, style: LineStyle) -> Self { + self.style = style; + self + } + /// Name of this vertical line. /// /// This name will show up in the plot legend, if legends are turned on. @@ -186,18 +299,16 @@ impl PlotItem for VLine { fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { let VLine { x, - mut stroke, + stroke, highlight, + style, .. } = self; - if *highlight { - stroke.width *= 2.0; - } - let points = [ + let points = vec![ transform.position_from_value(&Value::new(*x, transform.bounds().min[1])), transform.position_from_value(&Value::new(*x, transform.bounds().max[1])), ]; - shapes.push(Shape::line_segment(points, stroke)); + style.style_line(points, *stroke, *highlight, shapes) } fn initialize(&mut self, _x_range: RangeInclusive) {} @@ -409,8 +520,8 @@ pub enum MarkerShape { impl MarkerShape { /// Get a vector containing all marker shapes. - pub fn all() -> Vec { - vec![ + pub fn all() -> impl Iterator { + [ Self::Circle, Self::Diamond, Self::Square, @@ -422,6 +533,8 @@ impl MarkerShape { Self::Right, Self::Asterisk, ] + .iter() + .copied() } } @@ -432,6 +545,7 @@ pub struct Line { pub(super) name: String, pub(super) highlight: bool, pub(super) fill: Option, + pub(super) style: LineStyle, } impl Line { @@ -442,10 +556,11 @@ impl Line { name: Default::default(), highlight: false, fill: None, + style: LineStyle::Solid, } } - /// Highlight this line in the plot by scaling up the line and marker size. + /// Highlight this line in the plot by scaling up the line. pub fn highlight(mut self) -> Self { self.highlight = true; self @@ -475,6 +590,12 @@ impl Line { self } + /// Set the line's style. Default is `LineStyle::Solid`. + pub fn style(mut self, style: LineStyle) -> Self { + self.style = style; + self + } + /// Name of this line. /// /// This name will show up in the plot legend, if legends are turned on. @@ -499,19 +620,13 @@ impl PlotItem for Line { fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { let Self { series, - mut stroke, + stroke, highlight, mut fill, + style, .. } = self; - let mut fill_alpha = DEFAULT_FILL_ALPHA; - - if *highlight { - stroke.width *= 2.0; - fill_alpha = (2.0 * fill_alpha).at_most(1.0); - } - let values_tf: Vec<_> = series .values .iter() @@ -524,6 +639,10 @@ impl PlotItem for Line { fill = None; } if let Some(y_reference) = fill { + let mut fill_alpha = DEFAULT_FILL_ALPHA; + if *highlight { + fill_alpha = (2.0 * fill_alpha).at_most(1.0); + } let y = transform .position_from_value(&Value::new(0.0, y_reference)) .y; @@ -554,13 +673,7 @@ impl PlotItem for Line { mesh.colored_vertex(pos2(last.x, y), fill_color); shapes.push(Shape::Mesh(mesh)); } - - let line_shape = if n_values > 1 { - Shape::line(values_tf, stroke) - } else { - Shape::circle_filled(values_tf[0], stroke.width / 2.0, stroke.color) - }; - shapes.push(line_shape); + style.style_line(values_tf, *stroke, *highlight, shapes); } fn initialize(&mut self, x_range: RangeInclusive) { @@ -599,6 +712,7 @@ pub struct Polygon { pub(super) name: String, pub(super) highlight: bool, pub(super) fill_alpha: f32, + pub(super) style: LineStyle, } impl Polygon { @@ -609,6 +723,7 @@ impl Polygon { name: Default::default(), highlight: false, fill_alpha: DEFAULT_FILL_ALPHA, + style: LineStyle::Solid, } } @@ -643,6 +758,12 @@ impl Polygon { self } + /// Set the outline's style. Default is `LineStyle::Solid`. + pub fn style(mut self, style: LineStyle) -> Self { + self.style = style; + self + } + /// Name of this polygon. /// /// This name will show up in the plot legend, if legends are turned on. @@ -660,18 +781,18 @@ impl PlotItem for Polygon { fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { let Self { series, - mut stroke, + stroke, highlight, mut fill_alpha, + style, .. } = self; if *highlight { - stroke.width *= 2.0; fill_alpha = (2.0 * fill_alpha).at_most(1.0); } - let values_tf: Vec<_> = series + let mut values_tf: Vec<_> = series .values .iter() .map(|v| transform.position_from_value(v)) @@ -679,9 +800,15 @@ impl PlotItem for Polygon { let fill = Rgba::from(stroke.color).to_opaque().multiply(fill_alpha); - let shape = Shape::convex_polygon(values_tf, fill, stroke); - + let shape = Shape::Path { + points: values_tf.clone(), + closed: true, + fill: fill.into(), + stroke: Stroke::none(), + }; shapes.push(shape); + values_tf.push(*values_tf.first().unwrap()); + style.style_line(values_tf, *stroke, *highlight, shapes); } fn initialize(&mut self, x_range: RangeInclusive) { diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 3b7a3608..e383f79c 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -7,8 +7,10 @@ mod transform; use std::collections::HashSet; use items::PlotItem; -pub use items::{Arrows, Line, MarkerShape, PlotImage, Points, Polygon, Text, Value, Values}; -pub use items::{HLine, VLine}; +pub use items::{ + Arrows, HLine, Line, LineStyle, MarkerShape, PlotImage, Points, Polygon, Text, VLine, Value, + Values, +}; use legend::LegendWidget; pub use legend::{Corner, Legend}; use transform::{Bounds, ScreenTransform}; diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 939dd576..f4d23b9c 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -1,7 +1,7 @@ use egui::*; use plot::{ - Arrows, Corner, HLine, Legend, Line, MarkerShape, Plot, PlotImage, Points, Polygon, Text, - VLine, Value, Values, + Arrows, Corner, HLine, Legend, Line, LineStyle, MarkerShape, Plot, PlotImage, Points, Polygon, + Text, VLine, Value, Values, }; use std::f64::consts::TAU; @@ -13,6 +13,7 @@ struct LineDemo { circle_center: Pos2, square: bool, proportional: bool, + line_style: LineStyle, } impl Default for LineDemo { @@ -24,6 +25,7 @@ impl Default for LineDemo { circle_center: Pos2::new(0.0, 0.0), square: false, proportional: true, + line_style: LineStyle::Solid, } } } @@ -37,6 +39,7 @@ impl LineDemo { circle_center, square, proportional, + line_style, .. } = self; @@ -73,6 +76,23 @@ impl LineDemo { ui.checkbox(proportional, "Proportional data axes") .on_hover_text("Tick are the same size on both axes."); }); + ui.vertical(|ui| { + ComboBox::from_label("Line style") + .selected_text(line_style.to_string()) + .show_ui(ui, |ui| { + [ + LineStyle::Solid, + LineStyle::dashed_dense(), + LineStyle::dashed_loose(), + LineStyle::dotted_dense(), + LineStyle::dotted_loose(), + ] + .iter() + .for_each(|style| { + ui.selectable_value(line_style, *style, style.to_string()); + }); + }); + }); }); } @@ -88,6 +108,7 @@ impl LineDemo { }); Line::new(Values::from_values_iter(circle)) .color(Color32::from_rgb(100, 200, 100)) + .style(self.line_style) .name("circle") } @@ -99,6 +120,7 @@ impl LineDemo { 512, )) .color(Color32::from_rgb(200, 100, 100)) + .style(self.line_style) .name("wave") } @@ -110,6 +132,7 @@ impl LineDemo { 256, )) .color(Color32::from_rgb(100, 150, 250)) + .style(self.line_style) .name("x = sin(2t), y = sin(3t)") } } @@ -158,7 +181,6 @@ impl Default for MarkerDemo { impl MarkerDemo { fn markers(&self) -> Vec { MarkerShape::all() - .into_iter() .enumerate() .map(|(i, marker)| { let y_offset = i as f32 * 0.5 + 1.0; diff --git a/epaint/src/shape.rs b/epaint/src/shape.rs index aa46c4d4..ff38093d 100644 --- a/epaint/src/shape.rs +++ b/epaint/src/shape.rs @@ -66,7 +66,7 @@ impl Shape { /// A line through many points. /// - /// Use [`Self::line_segment`] instead if your line only connect two points. + /// Use [`Self::line_segment`] instead if your line only connects two points. pub fn line(points: Vec, stroke: impl Into) -> Self { Self::Path { points, @@ -86,6 +86,30 @@ impl Shape { } } + /// Turn a line into equally spaced dots. + pub fn dotted_line( + points: &[Pos2], + color: impl Into, + spacing: f32, + radius: f32, + ) -> Vec { + let mut shapes = Vec::new(); + points_from_line(points, spacing, radius, color.into(), &mut shapes); + shapes + } + + /// Turn a line into dashes. + pub fn dashed_line( + points: &[Pos2], + stroke: impl Into, + dash_length: f32, + gap_length: f32, + ) -> Vec { + let mut shapes = Vec::new(); + dashes_from_line(points, stroke.into(), dash_length, gap_length, &mut shapes); + shapes + } + /// A convex polygon with a fill and optional stroke. pub fn convex_polygon( points: Vec, @@ -161,6 +185,69 @@ impl Shape { } } +/// Creates equally spaced filled circles from a line. +fn points_from_line( + line: &[Pos2], + spacing: f32, + radius: f32, + color: Color32, + shapes: &mut Vec, +) { + let mut position_on_segment = 0.0; + line.windows(2).for_each(|window| { + let start = window[0]; + let end = window[1]; + let vector = end - start; + let segment_length = vector.length(); + while position_on_segment < segment_length { + let new_point = start + vector * (position_on_segment / segment_length); + shapes.push(Shape::circle_filled(new_point, radius, color)); + position_on_segment += spacing; + } + position_on_segment -= segment_length; + }); +} + +/// Creates dashes from a line. +fn dashes_from_line( + line: &[Pos2], + stroke: Stroke, + dash_length: f32, + gap_length: f32, + shapes: &mut Vec, +) { + let mut position_on_segment = 0.0; + let mut drawing_dash = false; + line.windows(2).for_each(|window| { + let start = window[0]; + let end = window[1]; + let vector = end - start; + let segment_length = vector.length(); + while position_on_segment < segment_length { + let new_point = start + vector * (position_on_segment / segment_length); + if drawing_dash { + // This is the end point. + if let Shape::Path { points, .. } = shapes.last_mut().unwrap() { + points.push(new_point); + } + position_on_segment += gap_length; + } else { + // Start a new dash. + shapes.push(Shape::line(vec![new_point], stroke)); + position_on_segment += dash_length; + } + drawing_dash = !drawing_dash; + } + // If the segment ends and the dash is not finished, add the segment's end point. + if drawing_dash { + if let Shape::Path { points, .. } = shapes.last_mut().unwrap() { + points.push(end); + } + } + position_on_segment -= segment_length; + }); +} + /// ## Operations impl Shape { pub fn mesh(mesh: Mesh) -> Self {