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
This commit is contained in:
Sven Niederberger 2021-07-06 20:15:04 +02:00 committed by GitHub
parent d8b2b50780
commit 7c5a2d60c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 283 additions and 43 deletions

View file

@ -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`.

View file

@ -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<Pos2>,
mut stroke: Stroke,
highlight: bool,
shapes: &mut Vec<Shape>,
) {
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<Stroke>) -> 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<Shape>) {
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<f64>) {}
@ -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<Stroke>) -> 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<Shape>) {
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<f64>) {}
@ -409,8 +520,8 @@ pub enum MarkerShape {
impl MarkerShape {
/// Get a vector containing all marker shapes.
pub fn all() -> Vec<Self> {
vec![
pub fn all() -> impl Iterator<Item = MarkerShape> {
[
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<f32>,
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<Shape>) {
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<f64>) {
@ -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<Shape>) {
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<f64>) {

View file

@ -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};

View file

@ -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<Points> {
MarkerShape::all()
.into_iter()
.enumerate()
.map(|(i, marker)| {
let y_offset = i as f32 * 0.5 + 1.0;

View file

@ -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<Pos2>, stroke: impl Into<Stroke>) -> 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<Color32>,
spacing: f32,
radius: f32,
) -> Vec<Self> {
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<Stroke>,
dash_length: f32,
gap_length: f32,
) -> Vec<Self> {
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<Pos2>,
@ -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<Shape>,
) {
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<Shape>,
) {
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 {