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:
parent
d8b2b50780
commit
7c5a2d60c5
5 changed files with 283 additions and 43 deletions
|
@ -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`.
|
||||
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue