From 2ced76e1323a8a383bbc7f0cc571adc479b509fb Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sun, 9 May 2021 11:58:58 +0200 Subject: [PATCH 01/50] initial work on markers --- egui/src/widgets/plot/items.rs | 304 +++++++++++++++++++++++ egui/src/widgets/plot/mod.rs | 68 +++-- egui_demo_lib/src/apps/demo/plot_demo.rs | 44 +++- 3 files changed, 388 insertions(+), 28 deletions(-) diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index d0b4c72c8d7..ae70793771e 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -73,13 +73,303 @@ struct ExplicitGenerator { // ---------------------------------------------------------------------------- +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub(crate) enum MarkerShape { + Circle, + Diamond, + Square, + Cross, + Plus, + Up, + Down, + Left, + Right, + Asterisk, +} + +#[derive(Debug, Clone, Copy)] +pub struct Marker { + pub(crate) shape: MarkerShape, + pub(crate) stroke: Color32, + pub(crate) fill: Color32, + pub(crate) size: f32, +} + +impl Default for Marker { + fn default() -> Self { + Self { + shape: MarkerShape::Circle, + stroke: Color32::TRANSPARENT, + fill: Color32::TRANSPARENT, + size: 1.0, + } + } +} + +impl Marker { + pub fn all() -> Vec { + vec![ + Self::circle(), + Self::diamond(), + Self::square(), + Self::cross(), + Self::plus(), + Self::up(), + Self::down(), + Self::left(), + Self::right(), + Self::asterisk(), + ] + } + + pub fn circle() -> Self { + Self { + shape: MarkerShape::Circle, + ..Default::default() + } + } + + pub fn diamond() -> Self { + Self { + shape: MarkerShape::Diamond, + ..Default::default() + } + } + + pub fn square() -> Self { + Self { + shape: MarkerShape::Square, + ..Default::default() + } + } + + pub fn cross() -> Self { + Self { + shape: MarkerShape::Cross, + ..Default::default() + } + } + + pub fn plus() -> Self { + Self { + shape: MarkerShape::Plus, + ..Default::default() + } + } + + pub fn up() -> Self { + Self { + shape: MarkerShape::Up, + ..Default::default() + } + } + + pub fn down() -> Self { + Self { + shape: MarkerShape::Down, + ..Default::default() + } + } + + pub fn left() -> Self { + Self { + shape: MarkerShape::Left, + ..Default::default() + } + } + + pub fn right() -> Self { + Self { + shape: MarkerShape::Right, + ..Default::default() + } + } + + pub fn asterisk() -> Self { + Self { + shape: MarkerShape::Asterisk, + ..Default::default() + } + } + + pub fn stroke(mut self, stroke: Color32) -> Self { + self.stroke = stroke; + self + } + + pub fn fill(mut self, fill: Color32) -> Self { + self.fill = fill; + self + } + + pub fn size(mut self, size: f32) -> Self { + self.size = size; + self + } + + pub(crate) fn get_shapes(&self, position: &Pos2) -> Vec { + let sqrt_3 = 3f32.sqrt(); + let frac_sqrt_3_2 = 3f32.sqrt() / 2.0; + let frac_1_sqrt_2 = 1.0 / 2f32.sqrt(); + + let Self { + fill, + shape, + stroke, + size, + } = *self; + + let stroke = Stroke::new(size / 10.0, stroke); + + let tf = |offset: Vec| -> Vec { + offset + .into_iter() + .map(|offset| *position + size * offset) + .collect() + }; + + match shape { + MarkerShape::Circle => { + vec![Shape::Circle { + radius: size, + center: *position, + fill, + stroke, + }] + } + MarkerShape::Diamond => { + let offsets = vec![ + vec2(1.0, 0.0), + vec2(0.0, -1.0), + vec2(-1.0, 0.0), + vec2(0.0, 1.0), + ]; + let points = tf(offsets); + vec![Shape::Path { + points, + closed: true, + fill, + stroke, + }] + } + MarkerShape::Square => { + let offsets = vec![ + vec2(frac_1_sqrt_2, frac_1_sqrt_2), + vec2(frac_1_sqrt_2, -frac_1_sqrt_2), + vec2(-frac_1_sqrt_2, -frac_1_sqrt_2), + vec2(-frac_1_sqrt_2, frac_1_sqrt_2), + ]; + let points = tf(offsets); + vec![Shape::Path { + points, + closed: true, + fill, + stroke, + }] + } + MarkerShape::Cross => { + vec![ + Shape::line( + tf(vec![ + vec2(-frac_1_sqrt_2, -frac_1_sqrt_2), + vec2(frac_1_sqrt_2, frac_1_sqrt_2), + ]), + stroke, + ), + Shape::line( + tf(vec![ + vec2(frac_1_sqrt_2, -frac_1_sqrt_2), + vec2(-frac_1_sqrt_2, frac_1_sqrt_2), + ]), + stroke, + ), + ] + } + MarkerShape::Plus => { + vec![ + Shape::line(tf(vec![vec2(-1.0, 0.0), vec2(1.0, 0.0)]), stroke), + Shape::line(tf(vec![vec2(0.0, -1.0), vec2(0.0, 1.0)]), stroke), + ] + } + MarkerShape::Up => { + let offsets = vec![ + vec2(0.0, -1.0), + vec2(-0.5 * sqrt_3, 0.5), + vec2(0.5 * sqrt_3, 0.5), + ]; + let points = tf(offsets); + vec![Shape::Path { + points, + closed: true, + fill, + stroke, + }] + } + MarkerShape::Down => { + let offsets = vec![ + vec2(0.0, 1.0), + vec2(-0.5 * sqrt_3, -0.5), + vec2(0.5 * sqrt_3, -0.5), + ]; + let points = tf(offsets); + vec![Shape::Path { + points, + closed: true, + fill, + stroke, + }] + } + MarkerShape::Left => { + let offsets = vec![ + vec2(-1.0, 0.0), + vec2(0.5, -0.5 * sqrt_3), + vec2(0.5, 0.5 * sqrt_3), + ]; + let points = tf(offsets); + vec![Shape::Path { + points, + closed: true, + fill, + stroke, + }] + } + MarkerShape::Right => { + let offsets = vec![ + vec2(1.0, 0.0), + vec2(-0.5, -0.5 * sqrt_3), + vec2(-0.5, 0.5 * sqrt_3), + ]; + let points = tf(offsets); + vec![Shape::Path { + points, + closed: true, + fill, + stroke, + }] + } + MarkerShape::Asterisk => { + let vertical = tf(vec![vec2(0.0, -1.0), vec2(0.0, 1.0)]); + let diagonal1 = tf(vec![vec2(-frac_sqrt_3_2, 0.5), vec2(frac_sqrt_3_2, -0.5)]); + let diagonal2 = tf(vec![vec2(-frac_sqrt_3_2, -0.5), vec2(frac_sqrt_3_2, 0.5)]); + vec![ + Shape::line(vertical, stroke), + Shape::line(diagonal1, stroke), + Shape::line(diagonal2, stroke), + ] + } + } + } +} + /// A series of values forming a path. pub struct Curve { pub(crate) values: Vec, generator: Option, pub(crate) bounds: Bounds, + pub(crate) marker: Option, pub(crate) stroke: Stroke, pub(crate) name: String, + pub(crate) highlight: bool, } impl Curve { @@ -88,8 +378,10 @@ impl Curve { values: Vec::new(), generator: None, bounds: Bounds::NOTHING, + marker: None, stroke: Stroke::new(2.0, Color32::TRANSPARENT), name: Default::default(), + highlight: false, } } @@ -196,12 +488,24 @@ impl Curve { Self::from_values(values) } + /// Highlight this curve. + 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 } + /// Add a marker. + pub fn marker(mut self, marker: Marker) -> Self { + self.marker = Some(marker); + self + } + /// Stroke width. A high value means the plot thickens. pub fn width(mut self, width: f32) -> Self { self.stroke.width = width; diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 39a38aa9363..9bcfee9685f 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -6,7 +6,7 @@ mod transform; use std::collections::{BTreeMap, HashSet}; -pub use items::{Curve, Value}; +pub use items::{Curve, Marker, Value}; use items::{HLine, VLine}; use transform::{Bounds, ScreenTransform}; @@ -120,6 +120,11 @@ impl Plot { self } + /// Add multiple data curves. + pub fn curves(self, curves: Vec) -> Self { + curves.into_iter().fold(self, Self::curve) + } + /// Add a horizontal line. /// Can be useful e.g. to show min/max bounds or similar. /// Always fills the full width of the plot. @@ -365,12 +370,9 @@ impl Widget for Plot { .values() .filter(|entry| entry.hovered) .for_each(|entry| { - curves - .iter_mut() - .filter(|curve| curve.name == entry.text) - .for_each(|curve| { - curve.stroke.width *= 2.0; - }); + curves.iter_mut().for_each(|curve| { + curve.highlight |= curve.name == entry.text; + }); }); // Remove deselected curves. @@ -509,21 +511,45 @@ impl Prepared { } for curve in &self.curves { - let stroke = curve.stroke; - let values = &curve.values; - let shape = if values.len() == 1 { - let point = transform.position_from_value(&values[0]); - Shape::circle_filled(point, stroke.width / 2.0, stroke.color) + let Curve { + values, + mut stroke, + mut marker, + .. + } = curve; + + if curve.highlight { + stroke.width *= 2.0; + if let Some(marker) = &mut marker { + marker.size *= 1.2; + } + } + + let values_tf: Vec<_> = values + .iter() + .map(|v| transform.position_from_value(v)) + .collect(); + + let marker_shapes: Option> = marker.map(|marker| { + values_tf + .iter() + .flat_map(|pos| marker.get_shapes(pos)) + .collect() + }); + + if values_tf.len() > 1 { + shapes.push(Shape::line(values_tf, stroke)); } else { - Shape::line( - values - .iter() - .map(|v| transform.position_from_value(v)) - .collect(), - stroke, - ) - }; - shapes.push(shape); + shapes.push(Shape::circle_filled( + values_tf[0], + stroke.width / 2.0, + stroke.color, + )); + } + + if let Some(marker_shapes) = marker_shapes { + shapes.extend(marker_shapes.into_iter()); + } } if let Some(pointer) = response.hover_pos() { diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 7e6aa12e2e1..01b93c76a74 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -1,4 +1,4 @@ -use egui::plot::{Curve, Plot, Value}; +use egui::plot::{Curve, Marker, Plot, Value}; use egui::*; use std::f64::consts::TAU; @@ -110,7 +110,7 @@ impl PlotDemo { }); Curve::from_values_iter(circle) .color(Color32::from_rgb(100, 200, 100)) - .name("circle") + .name("Circle") } fn sin(&self) -> Curve { @@ -129,11 +129,29 @@ impl PlotDemo { Curve::from_parametric_callback( move |t| ((2.0 * t + time).sin(), (3.0 * t).sin()), 0.0..=TAU, - 512, + 100, ) .color(Color32::from_rgb(100, 150, 250)) .name("x = sin(2t), y = sin(3t)") } + + fn markers(&self) -> Vec { + Marker::all() + .into_iter() + .enumerate() + .map(|(i, marker)| { + let y_offset = i as f32 * 0.5 + 1.0; + Curve::from_values(vec![ + Value::new(1.0, 0.0 + y_offset), + Value::new(2.0, 0.5 + y_offset), + Value::new(3.0, 0.0 + y_offset), + Value::new(4.0, 0.5 + y_offset), + ]) + .marker(marker.size(7.5).stroke(Color32::WHITE)) + .name("Markers") + }) + .collect() + } } impl super::View for PlotDemo { @@ -145,18 +163,30 @@ impl super::View for PlotDemo { self.time += ui.input().unstable_dt.at_most(1.0 / 30.0) as f64; }; - let mut plot = Plot::new("Demo Plot") + let mut plot = Plot::new("Curves Demo") .curve(self.circle()) .curve(self.sin()) .curve(self.thingy()) - .show_legend(self.legend) - .min_size(Vec2::new(200.0, 200.0)); + .width(300.0) + .height(300.0) + .show_legend(self.legend); if self.square { plot = plot.view_aspect(1.0); } if self.proportional { plot = plot.data_aspect(1.0); } - ui.add(plot); + + let markers_plot = Plot::new("Markers Demo") + .curves(self.markers()) + .width(300.0) + .height(300.0) + .data_aspect(1.0) + .show_legend(self.legend); + + ui.horizontal(|ui| { + ui.add(plot); + ui.add(markers_plot); + }); } } From 0d09622425d156316ed9738275f69da809e00e44 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sun, 9 May 2021 12:11:46 +0200 Subject: [PATCH 02/50] clippy fix --- egui_demo_lib/src/apps/demo/plot_demo.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 01b93c76a74..fcc793526e9 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -135,7 +135,7 @@ impl PlotDemo { .name("x = sin(2t), y = sin(3t)") } - fn markers(&self) -> Vec { + fn markers() -> Vec { Marker::all() .into_iter() .enumerate() @@ -178,7 +178,7 @@ impl super::View for PlotDemo { } let markers_plot = Plot::new("Markers Demo") - .curves(self.markers()) + .curves(Self::markers()) .width(300.0) .height(300.0) .data_aspect(1.0) From 10ffbfcae1c2d7fc88dbbfb39dfbe8961fde93c7 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Tue, 11 May 2021 09:02:08 +0200 Subject: [PATCH 03/50] simplify marker --- egui/src/widgets/plot/items.rs | 79 +++++++++++++----------- egui/src/widgets/plot/mod.rs | 44 +++++++++---- egui_demo_lib/src/apps/demo/plot_demo.rs | 9 ++- 3 files changed, 81 insertions(+), 51 deletions(-) diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index ae70793771e..1c6eb20f469 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -90,18 +90,18 @@ pub(crate) enum MarkerShape { #[derive(Debug, Clone, Copy)] pub struct Marker { pub(crate) shape: MarkerShape, - pub(crate) stroke: Color32, - pub(crate) fill: Color32, - pub(crate) size: f32, + pub(crate) color: Color32, + pub(crate) filled: bool, + pub(crate) radius: f32, } impl Default for Marker { fn default() -> Self { Self { shape: MarkerShape::Circle, - stroke: Color32::TRANSPARENT, - fill: Color32::TRANSPARENT, - size: 1.0, + color: Color32::TRANSPARENT, + filled: true, + radius: 1.0, } } } @@ -192,18 +192,21 @@ impl Marker { } } - pub fn stroke(mut self, stroke: Color32) -> Self { - self.stroke = stroke; + /// Set the marker's color. Defaults to the curve's color. + pub fn color(mut self, color: Color32) -> Self { + self.color = color; self } - pub fn fill(mut self, fill: Color32) -> Self { - self.fill = fill; + /// Whether to fill the marker. + pub fn filled(mut self, filled: bool) -> Self { + self.filled = filled; self } - pub fn size(mut self, size: f32) -> Self { - self.size = size; + /// The maximum extent of the marker around its position. + pub fn radius(mut self, radius: f32) -> Self { + self.radius = radius; self } @@ -213,26 +216,30 @@ impl Marker { let frac_1_sqrt_2 = 1.0 / 2f32.sqrt(); let Self { - fill, + color, + filled, shape, - stroke, - size, + radius, } = *self; - let stroke = Stroke::new(size / 10.0, stroke); + let stroke_size = radius / 5.0; let tf = |offset: Vec| -> Vec { offset .into_iter() - .map(|offset| *position + size * offset) + .map(|offset| *position + radius * offset) .collect() }; + let default_stroke = Stroke::new(stroke_size, color); + let stroke = (!filled).then(|| default_stroke).unwrap_or_default(); + let fill = filled.then(|| color).unwrap_or_default(); + match shape { MarkerShape::Circle => { vec![Shape::Circle { - radius: size, center: *position, + radius, fill, stroke, }] @@ -268,27 +275,25 @@ impl Marker { }] } MarkerShape::Cross => { + let diagonal1 = tf(vec![ + vec2(-frac_1_sqrt_2, -frac_1_sqrt_2), + vec2(frac_1_sqrt_2, frac_1_sqrt_2), + ]); + let diagonal2 = tf(vec![ + vec2(frac_1_sqrt_2, -frac_1_sqrt_2), + vec2(-frac_1_sqrt_2, frac_1_sqrt_2), + ]); vec![ - Shape::line( - tf(vec![ - vec2(-frac_1_sqrt_2, -frac_1_sqrt_2), - vec2(frac_1_sqrt_2, frac_1_sqrt_2), - ]), - stroke, - ), - Shape::line( - tf(vec![ - vec2(frac_1_sqrt_2, -frac_1_sqrt_2), - vec2(-frac_1_sqrt_2, frac_1_sqrt_2), - ]), - stroke, - ), + Shape::line(diagonal1, default_stroke), + Shape::line(diagonal2, default_stroke), ] } MarkerShape::Plus => { + let horizontal = tf(vec![vec2(-1.0, 0.0), vec2(1.0, 0.0)]); + let vertical = tf(vec![vec2(0.0, -1.0), vec2(0.0, 1.0)]); vec![ - Shape::line(tf(vec![vec2(-1.0, 0.0), vec2(1.0, 0.0)]), stroke), - Shape::line(tf(vec![vec2(0.0, -1.0), vec2(0.0, 1.0)]), stroke), + Shape::line(horizontal, default_stroke), + Shape::line(vertical, default_stroke), ] } MarkerShape::Up => { @@ -352,9 +357,9 @@ impl Marker { let diagonal1 = tf(vec![vec2(-frac_sqrt_3_2, 0.5), vec2(frac_sqrt_3_2, -0.5)]); let diagonal2 = tf(vec![vec2(-frac_sqrt_3_2, -0.5), vec2(frac_sqrt_3_2, 0.5)]); vec![ - Shape::line(vertical, stroke), - Shape::line(diagonal1, stroke), - Shape::line(diagonal2, stroke), + Shape::line(vertical, default_stroke), + Shape::line(diagonal1, default_stroke), + Shape::line(diagonal2, default_stroke), ] } } diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 9bcfee9685f..9b28a666971 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -100,21 +100,26 @@ impl Plot { } } - fn auto_color(&mut self, color: &mut Color32) { - if *color == Color32::TRANSPARENT { - let i = self.next_auto_color_idx; - self.next_auto_color_idx += 1; - let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875 - let h = i as f32 * golden_ratio; - *color = Hsva::new(h, 0.85, 0.5, 1.0).into(); // TODO: OkLab or some other perspective color space - } + fn auto_color(&mut self) -> Color32 { + let i = self.next_auto_color_idx; + self.next_auto_color_idx += 1; + let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875 + let h = i as f32 * golden_ratio; + Hsva::new(h, 0.85, 0.5, 1.0).into() // TODO: OkLab or some other perspective color space } /// Add a data curve. /// You can add multiple curves. pub fn curve(mut self, mut curve: Curve) -> Self { if !curve.no_data() { - self.auto_color(&mut curve.stroke.color); + if curve.stroke.color == Color32::TRANSPARENT { + curve.stroke.color = self.auto_color(); + } + if let Some(marker) = &mut curve.marker { + if marker.color == Color32::TRANSPARENT { + marker.color = curve.stroke.color; + } + } self.curves.push(curve); } self @@ -129,7 +134,9 @@ impl Plot { /// Can be useful e.g. to show min/max bounds or similar. /// Always fills the full width of the plot. pub fn hline(mut self, mut hline: HLine) -> Self { - self.auto_color(&mut hline.stroke.color); + if hline.stroke.color == Color32::TRANSPARENT { + hline.stroke.color = self.auto_color(); + } self.hlines.push(hline); self } @@ -138,7 +145,9 @@ impl Plot { /// Can be useful e.g. to show min/max bounds or similar. /// Always fills the full height of the plot. pub fn vline(mut self, mut vline: VLine) -> Self { - self.auto_color(&mut vline.stroke.color); + if vline.stroke.color == Color32::TRANSPARENT { + vline.stroke.color = self.auto_color(); + } self.vlines.push(vline); self } @@ -345,7 +354,16 @@ impl Widget for Plot { entry.color = ui.visuals().noninteractive().fg_stroke.color } }) - .or_insert_with(|| LegendEntry::new(text, curve.stroke.color, checked)); + .or_insert_with(|| { + let color = if curve.stroke.color != Color32::TRANSPARENT { + curve.stroke.color + } else if let Some(marker) = curve.marker { + marker.color + } else { + ui.visuals().noninteractive().fg_stroke.color + }; + LegendEntry::new(text, color, checked) + }); }); // Show the legend. @@ -521,7 +539,7 @@ impl Prepared { if curve.highlight { stroke.width *= 2.0; if let Some(marker) = &mut marker { - marker.size *= 1.2; + marker.radius *= 1.2; } } diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index fcc793526e9..049cd48cb6d 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -132,6 +132,13 @@ impl PlotDemo { 100, ) .color(Color32::from_rgb(100, 150, 250)) + // .stroke(Stroke::none()) + .marker( + Marker::circle() + .filled(true) + .radius(2.0) + .color(Color32::BLUE), + ) .name("x = sin(2t), y = sin(3t)") } @@ -147,7 +154,7 @@ impl PlotDemo { Value::new(3.0, 0.0 + y_offset), Value::new(4.0, 0.5 + y_offset), ]) - .marker(marker.size(7.5).stroke(Color32::WHITE)) + .marker(marker.radius(7.5)) .name("Markers") }) .collect() From 4ad97f3d1e88d4765fe605c3f267ede2eac17c45 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Tue, 11 May 2021 12:52:18 +0200 Subject: [PATCH 04/50] use option for color --- egui/src/widgets/plot/items.rs | 27 ++++++++++------ egui/src/widgets/plot/mod.rs | 39 ++++++++++-------------- egui_demo_lib/src/apps/demo/plot_demo.rs | 20 ++++++------ 3 files changed, 43 insertions(+), 43 deletions(-) diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index 1c6eb20f469..7fc2fef685b 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -90,7 +90,7 @@ pub(crate) enum MarkerShape { #[derive(Debug, Clone, Copy)] pub struct Marker { pub(crate) shape: MarkerShape, - pub(crate) color: Color32, + pub(crate) color: Option, pub(crate) filled: bool, pub(crate) radius: f32, } @@ -99,7 +99,7 @@ impl Default for Marker { fn default() -> Self { Self { shape: MarkerShape::Circle, - color: Color32::TRANSPARENT, + color: None, filled: true, radius: 1.0, } @@ -194,7 +194,7 @@ impl Marker { /// Set the marker's color. Defaults to the curve's color. pub fn color(mut self, color: Color32) -> Self { - self.color = color; + self.color = Some(color); self } @@ -222,6 +222,11 @@ impl Marker { radius, } = *self; + if color.is_none() { + return Vec::new(); + } + let color = color.unwrap(); + let stroke_size = radius / 5.0; let tf = |offset: Vec| -> Vec { @@ -372,7 +377,8 @@ pub struct Curve { generator: Option, pub(crate) bounds: Bounds, pub(crate) marker: Option, - pub(crate) stroke: Stroke, + pub(crate) color: Option, + pub(crate) width: f32, pub(crate) name: String, pub(crate) highlight: bool, } @@ -384,7 +390,8 @@ impl Curve { generator: None, bounds: Bounds::NOTHING, marker: None, - stroke: Stroke::new(2.0, Color32::TRANSPARENT), + color: None, + width: 1.0, name: Default::default(), highlight: false, } @@ -501,7 +508,9 @@ impl Curve { /// Add a stroke. pub fn stroke(mut self, stroke: impl Into) -> Self { - self.stroke = stroke.into(); + let stroke: Stroke = stroke.into(); + self.color = Some(stroke.color); + self.width = stroke.width; self } @@ -513,13 +522,13 @@ impl Curve { /// Stroke width. A high value means the plot thickens. pub fn width(mut self, width: f32) -> Self { - self.stroke.width = width; + self.width = width; self } - /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. + /// Stroke color. pub fn color(mut self, color: impl Into) -> Self { - self.stroke.color = color.into(); + self.color = Some(color.into()); self } diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 7412fb664da..0a4a34f3340 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -112,13 +112,9 @@ impl Plot { /// You can add multiple curves. pub fn curve(mut self, mut curve: Curve) -> Self { if !curve.no_data() { - if curve.stroke.color == Color32::TRANSPARENT { - curve.stroke.color = self.auto_color(); - } + let curve_color = curve.color.get_or_insert_with(|| self.auto_color()); if let Some(marker) = &mut curve.marker { - if marker.color == Color32::TRANSPARENT { - marker.color = curve.stroke.color; - } + marker.color.get_or_insert(*curve_color); } self.curves.push(curve); } @@ -341,6 +337,7 @@ impl Widget for Plot { // Collect the legend entries. If multiple curves have the same name, they share a // checkbox. If their colors don't match, we pick a neutral color for the checkbox. let mut legend_entries: BTreeMap = BTreeMap::new(); + let neutral_color = ui.visuals().noninteractive().fg_stroke.color; curves .iter() .filter(|curve| !curve.name.is_empty()) @@ -350,18 +347,15 @@ impl Widget for Plot { legend_entries .entry(curve.name.clone()) .and_modify(|entry| { - if entry.color != curve.stroke.color { - entry.color = ui.visuals().noninteractive().fg_stroke.color + if Some(entry.color) != curve.color { + entry.color = neutral_color } }) .or_insert_with(|| { - let color = if curve.stroke.color != Color32::TRANSPARENT { - curve.stroke.color - } else if let Some(marker) = curve.marker { - marker.color - } else { - ui.visuals().noninteractive().fg_stroke.color - }; + let color = curve + .color + .or(curve.marker.and_then(|marker| marker.color)) + .unwrap_or(neutral_color); LegendEntry::new(text, color, checked) }); }); @@ -530,13 +524,16 @@ impl Prepared { for curve in &self.curves { let Curve { values, - mut stroke, + color, + mut width, mut marker, .. } = curve; + let color = color.unwrap_or(Color32::TRANSPARENT); + if curve.highlight { - stroke.width *= 2.0; + width *= 2.0; if let Some(marker) = &mut marker { marker.radius *= 1.2; } @@ -555,13 +552,9 @@ impl Prepared { }); if values_tf.len() > 1 { - shapes.push(Shape::line(values_tf, stroke)); + shapes.push(Shape::line(values_tf, Stroke::new(width, color))); } else { - shapes.push(Shape::circle_filled( - values_tf[0], - stroke.width / 2.0, - stroke.color, - )); + shapes.push(Shape::circle_filled(values_tf[0], width / 2.0, color)); } if let Some(marker_shapes) = marker_shapes { diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 987048f9c5f..476459fb219 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -11,6 +11,7 @@ pub struct PlotDemo { square: bool, legend: bool, proportional: bool, + fill_markers: bool, } impl Default for PlotDemo { @@ -23,6 +24,7 @@ impl Default for PlotDemo { square: false, legend: true, proportional: true, + fill_markers: true, } } } @@ -58,6 +60,7 @@ impl PlotDemo { square, legend, proportional, + fill_markers, } = self; ui.horizontal(|ui| { @@ -92,6 +95,7 @@ impl PlotDemo { ui.checkbox(square, "square view"); ui.checkbox(legend, "legend"); ui.checkbox(proportional, "proportional data axes"); + ui.checkbox(fill_markers, "fill markers"); }); }); @@ -110,7 +114,7 @@ impl PlotDemo { }); Curve::from_values_iter(circle) .color(Color32::from_rgb(100, 200, 100)) - .name("Circle") + .name("circle") } fn sin(&self) -> Curve { @@ -132,17 +136,11 @@ impl PlotDemo { 100, ) .color(Color32::from_rgb(100, 150, 250)) - // .stroke(Stroke::none()) - .marker( - Marker::circle() - .filled(true) - .radius(2.0) - .color(Color32::BLUE), - ) + .marker(Marker::circle().radius(2.0)) .name("x = sin(2t), y = sin(3t)") } - fn markers() -> Vec { + fn markers(&self) -> Vec { Marker::all() .into_iter() .enumerate() @@ -154,7 +152,7 @@ impl PlotDemo { Value::new(3.0, 0.0 + y_offset), Value::new(4.0, 0.5 + y_offset), ]) - .marker(marker.radius(7.5)) + .marker(marker.radius(7.5).filled(self.fill_markers)) .name("Markers") }) .collect() @@ -185,7 +183,7 @@ impl super::View for PlotDemo { } let markers_plot = Plot::new("Markers Demo") - .curves(Self::markers()) + .curves(self.markers()) .width(300.0) .height(300.0) .data_aspect(1.0) From 5fb88200dcbb7f9aa11a4cc6b419ee0700dbc255 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Tue, 11 May 2021 13:05:14 +0200 Subject: [PATCH 05/50] prepare for more demo plots --- egui_demo_lib/src/apps/demo/plot_demo.rs | 103 ++++++++++++++--------- 1 file changed, 63 insertions(+), 40 deletions(-) diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 476459fb219..31430c8baf6 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -3,7 +3,7 @@ use egui::*; use std::f64::consts::TAU; #[derive(PartialEq)] -pub struct PlotDemo { +struct CurveDemo { animate: bool, time: f64, circle_radius: f64, @@ -11,10 +11,15 @@ pub struct PlotDemo { square: bool, legend: bool, proportional: bool, - fill_markers: bool, } -impl Default for PlotDemo { +#[derive(PartialEq, Default)] +pub struct PlotDemo { + curve_demo: CurveDemo, + marker_demo: MarkerDemo, +} + +impl Default for CurveDemo { fn default() -> Self { Self { animate: true, @@ -24,7 +29,6 @@ impl Default for PlotDemo { square: false, legend: true, proportional: true, - fill_markers: true, } } } @@ -44,14 +48,8 @@ impl super::Demo for PlotDemo { } } -impl PlotDemo { +impl CurveDemo { fn options_ui(&mut self, ui: &mut Ui) { - ui.vertical_centered(|ui| { - egui::reset_button(ui, self); - ui.add(crate::__egui_github_link_file!()); - }); - ui.separator(); - let Self { animate, time: _, @@ -60,7 +58,7 @@ impl PlotDemo { square, legend, proportional, - fill_markers, + .. } = self; ui.horizontal(|ui| { @@ -95,7 +93,6 @@ impl PlotDemo { ui.checkbox(square, "square view"); ui.checkbox(legend, "legend"); ui.checkbox(proportional, "proportional data axes"); - ui.checkbox(fill_markers, "fill markers"); }); }); @@ -139,35 +136,15 @@ impl PlotDemo { .marker(Marker::circle().radius(2.0)) .name("x = sin(2t), y = sin(3t)") } - - fn markers(&self) -> Vec { - Marker::all() - .into_iter() - .enumerate() - .map(|(i, marker)| { - let y_offset = i as f32 * 0.5 + 1.0; - Curve::from_values(vec![ - Value::new(1.0, 0.0 + y_offset), - Value::new(2.0, 0.5 + y_offset), - Value::new(3.0, 0.0 + y_offset), - Value::new(4.0, 0.5 + y_offset), - ]) - .marker(marker.radius(7.5).filled(self.fill_markers)) - .name("Markers") - }) - .collect() - } } -impl super::View for PlotDemo { - fn ui(&mut self, ui: &mut Ui) { +impl Widget for &mut CurveDemo { + fn ui(self, ui: &mut Ui) -> Response { self.options_ui(ui); - if self.animate { ui.ctx().request_repaint(); self.time += ui.input().unstable_dt.at_most(1.0 / 30.0) as f64; }; - let mut plot = Plot::new("Curves Demo") .curve(self.circle()) .curve(self.sin()) @@ -181,17 +158,63 @@ impl super::View for PlotDemo { if self.proportional { plot = plot.data_aspect(1.0); } + ui.add(plot) + } +} + +#[derive(PartialEq)] +struct MarkerDemo { + fill_markers: bool, +} + +impl Default for MarkerDemo { + fn default() -> Self { + Self { fill_markers: true } + } +} + +impl MarkerDemo { + fn markers(&self) -> Vec { + Marker::all() + .into_iter() + .enumerate() + .map(|(i, marker)| { + let y_offset = i as f32 * 0.5 + 1.0; + Curve::from_values(vec![ + Value::new(1.0, 0.0 + y_offset), + Value::new(2.0, 0.5 + y_offset), + Value::new(3.0, 0.0 + y_offset), + Value::new(4.0, 0.5 + y_offset), + ]) + .marker(marker.radius(7.5).filled(self.fill_markers)) + .name("Markers") + }) + .collect() + } +} + +impl Widget for &mut MarkerDemo { + fn ui(self, ui: &mut Ui) -> Response { + ui.checkbox(&mut self.fill_markers, "fill markers"); let markers_plot = Plot::new("Markers Demo") .curves(self.markers()) .width(300.0) .height(300.0) - .data_aspect(1.0) - .show_legend(self.legend); + .data_aspect(1.0); - ui.horizontal(|ui| { - ui.add(plot); - ui.add(markers_plot); + ui.add(markers_plot) + } +} + +impl super::View for PlotDemo { + fn ui(&mut self, ui: &mut Ui) { + ui.vertical_centered(|ui| { + egui::reset_button(ui, self); + ui.add(crate::__egui_github_link_file!()); }); + + ui.collapsing("Curves", |ui| ui.add(&mut self.curve_demo)); + ui.collapsing("Markers", |ui| ui.add(&mut self.marker_demo)); } } From ee5fd49a658034facc4aed1a8cefe4ffff3943fa Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Tue, 11 May 2021 14:23:33 +0200 Subject: [PATCH 06/50] more improvements for markers --- egui/src/widgets/plot/items.rs | 8 +++- egui/src/widgets/plot/mod.rs | 28 ++++++----- egui_demo_lib/src/apps/demo/plot_demo.rs | 60 +++++++++++++++++++----- 3 files changed, 73 insertions(+), 23 deletions(-) diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index 7fc2fef685b..7b1439d5c39 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -101,7 +101,7 @@ impl Default for Marker { shape: MarkerShape::Circle, color: None, filled: true, - radius: 1.0, + radius: 2.0, } } } @@ -541,4 +541,10 @@ impl Curve { self.name = name.to_string(); self } + + pub(crate) fn get_defining_color(&self) -> Option { + self.color + .filter(|color| color.a() != 0) + .or_else(|| self.marker.and_then(|marker| marker.color)) + } } diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 0a4a34f3340..ff9fb989fbd 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -111,13 +111,22 @@ impl Plot { /// Add a data curve. /// You can add multiple curves. pub fn curve(mut self, mut curve: Curve) -> Self { - if !curve.no_data() { - let curve_color = curve.color.get_or_insert_with(|| self.auto_color()); - if let Some(marker) = &mut curve.marker { - marker.color.get_or_insert(*curve_color); - } - self.curves.push(curve); + if curve.no_data() { + return self; + }; + + let curve_color = curve.color.get_or_insert_with(|| self.auto_color()); + if let Some(marker) = &mut curve.marker { + marker.color.get_or_insert_with(|| { + if *curve_color != Color32::TRANSPARENT { + *curve_color + } else { + self.auto_color() + } + }); } + self.curves.push(curve); + self } @@ -347,15 +356,12 @@ impl Widget for Plot { legend_entries .entry(curve.name.clone()) .and_modify(|entry| { - if Some(entry.color) != curve.color { + if Some(entry.color) != curve.get_defining_color() { entry.color = neutral_color } }) .or_insert_with(|| { - let color = curve - .color - .or(curve.marker.and_then(|marker| marker.color)) - .unwrap_or(neutral_color); + let color = curve.get_defining_color().unwrap_or(neutral_color); LegendEntry::new(text, color, checked) }); }); diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 31430c8baf6..5b4da243b63 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -86,7 +86,7 @@ impl CurveDemo { }); }); - ui.vertical(|ui| { + ui.horizontal(|ui| { ui.style_mut().wrap = Some(false); ui.checkbox(animate, "animate"); ui.add_space(8.0); @@ -133,7 +133,7 @@ impl CurveDemo { 100, ) .color(Color32::from_rgb(100, 150, 250)) - .marker(Marker::circle().radius(2.0)) + .marker(Marker::default()) .name("x = sin(2t), y = sin(3t)") } } @@ -149,7 +149,6 @@ impl Widget for &mut CurveDemo { .curve(self.circle()) .curve(self.sin()) .curve(self.thingy()) - .width(300.0) .height(300.0) .show_legend(self.legend); if self.square { @@ -164,12 +163,24 @@ impl Widget for &mut CurveDemo { #[derive(PartialEq)] struct MarkerDemo { + show_markers: bool, + show_lines: bool, fill_markers: bool, + marker_radius: f32, + custom_marker_color: bool, + marker_color: Color32, } impl Default for MarkerDemo { fn default() -> Self { - Self { fill_markers: true } + Self { + show_markers: true, + show_lines: true, + fill_markers: true, + marker_radius: 5.0, + custom_marker_color: false, + marker_color: Color32::GRAY, + } } } @@ -180,14 +191,28 @@ impl MarkerDemo { .enumerate() .map(|(i, marker)| { let y_offset = i as f32 * 0.5 + 1.0; - Curve::from_values(vec![ + let mut curve = Curve::from_values(vec![ Value::new(1.0, 0.0 + y_offset), Value::new(2.0, 0.5 + y_offset), Value::new(3.0, 0.0 + y_offset), Value::new(4.0, 0.5 + y_offset), + Value::new(5.0, 0.0 + y_offset), + Value::new(6.0, 0.5 + y_offset), ]) - .marker(marker.radius(7.5).filled(self.fill_markers)) - .name("Markers") + .name("Marker Lines"); + + if self.show_markers { + let mut marker = marker.filled(self.fill_markers).radius(self.marker_radius); + if self.custom_marker_color { + marker = marker.color(self.marker_color); + } + curve = curve.marker(marker); + } + + if !self.show_lines { + curve = curve.color(Color32::TRANSPARENT); + } + curve }) .collect() } @@ -195,14 +220,28 @@ impl MarkerDemo { impl Widget for &mut MarkerDemo { fn ui(self, ui: &mut Ui) -> Response { - ui.checkbox(&mut self.fill_markers, "fill markers"); + ui.horizontal(|ui| { + ui.checkbox(&mut self.show_lines, "show lines"); + ui.checkbox(&mut self.show_markers, "show markers"); + }); + ui.horizontal(|ui| { + ui.checkbox(&mut self.fill_markers, "fill markers"); + ui.add( + egui::DragValue::new(&mut self.marker_radius) + .speed(0.1) + .clamp_range(0.0..=f32::INFINITY) + .prefix("marker radius: "), + ); + ui.checkbox(&mut self.custom_marker_color, "custom marker color"); + if self.custom_marker_color { + ui.color_edit_button_srgba(&mut self.marker_color); + } + }); let markers_plot = Plot::new("Markers Demo") .curves(self.markers()) - .width(300.0) .height(300.0) .data_aspect(1.0); - ui.add(markers_plot) } } @@ -213,7 +252,6 @@ impl super::View for PlotDemo { egui::reset_button(ui, self); ui.add(crate::__egui_github_link_file!()); }); - ui.collapsing("Curves", |ui| ui.add(&mut self.curve_demo)); ui.collapsing("Markers", |ui| ui.add(&mut self.marker_demo)); } From 99cba8a3eff8d2036c78e2c90cfe8fd146137409 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Tue, 11 May 2021 16:46:32 +0200 Subject: [PATCH 07/50] some small adjustments --- egui/src/widgets/plot/items.rs | 13 ++++--- egui/src/widgets/plot/mod.rs | 16 +++++---- egui_demo_lib/src/apps/demo/plot_demo.rs | 43 ++++++++++++------------ 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index 7b1439d5c39..bcadff8c9d7 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -90,8 +90,11 @@ pub(crate) enum MarkerShape { #[derive(Debug, Clone, Copy)] pub struct Marker { pub(crate) shape: MarkerShape, + /// Color of the marker. `None` means that it will be picked automatically. pub(crate) color: Option, + /// Whether to fill the marker. Does not apply to all types. pub(crate) filled: bool, + /// The maximum extent of the marker from its center. pub(crate) radius: f32, } @@ -107,6 +110,7 @@ impl Default for Marker { } impl Marker { + /// Get a vector containing a marker of each shape. pub fn all() -> Vec { vec![ Self::circle(), @@ -204,7 +208,7 @@ impl Marker { self } - /// The maximum extent of the marker around its position. + /// Set the maximum extent of the marker around its position. pub fn radius(mut self, radius: f32) -> Self { self.radius = radius; self @@ -500,7 +504,7 @@ impl Curve { Self::from_values(values) } - /// Highlight this curve. + /// Highlight this curve in the plot by scaling up the line and marker size. pub fn highlight(mut self) -> Self { self.highlight = true; self @@ -514,7 +518,7 @@ impl Curve { self } - /// Add a marker. + /// Add a marker for all data points. pub fn marker(mut self, marker: Marker) -> Self { self.marker = Some(marker); self @@ -542,7 +546,8 @@ impl Curve { self } - pub(crate) fn get_defining_color(&self) -> Option { + /// Return the color by which the curve can be identified. + pub(crate) fn get_color(&self) -> Option { self.color .filter(|color| color.a() != 0) .or_else(|| self.marker.and_then(|marker| marker.color)) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index ff9fb989fbd..4b73053d649 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -115,11 +115,13 @@ impl Plot { return self; }; - let curve_color = curve.color.get_or_insert_with(|| self.auto_color()); + // Give the stroke an automatic color if no color has been assigned. + let stroke_color = curve.color.get_or_insert_with(|| self.auto_color()); + // Give the marker the same color as the stroke if no color has been assigned. if let Some(marker) = &mut curve.marker { marker.color.get_or_insert_with(|| { - if *curve_color != Color32::TRANSPARENT { - *curve_color + if *stroke_color != Color32::TRANSPARENT { + *stroke_color } else { self.auto_color() } @@ -139,7 +141,7 @@ impl Plot { /// Can be useful e.g. to show min/max bounds or similar. /// Always fills the full width of the plot. pub fn hline(mut self, mut hline: HLine) -> Self { - if hline.stroke.color == Color32::TRANSPARENT { + if hline.stroke.color.a() == 0 { hline.stroke.color = self.auto_color(); } self.hlines.push(hline); @@ -150,7 +152,7 @@ impl Plot { /// Can be useful e.g. to show min/max bounds or similar. /// Always fills the full height of the plot. pub fn vline(mut self, mut vline: VLine) -> Self { - if vline.stroke.color == Color32::TRANSPARENT { + if vline.stroke.color.a() == 0 { vline.stroke.color = self.auto_color(); } self.vlines.push(vline); @@ -356,12 +358,12 @@ impl Widget for Plot { legend_entries .entry(curve.name.clone()) .and_modify(|entry| { - if Some(entry.color) != curve.get_defining_color() { + if Some(entry.color) != curve.get_color() { entry.color = neutral_color } }) .or_insert_with(|| { - let color = curve.get_defining_color().unwrap_or(neutral_color); + let color = curve.get_color().unwrap_or(neutral_color); LegendEntry::new(text, color, checked) }); }); diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 5b4da243b63..69aae8d6add 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -13,12 +13,6 @@ struct CurveDemo { proportional: bool, } -#[derive(PartialEq, Default)] -pub struct PlotDemo { - curve_demo: CurveDemo, - marker_demo: MarkerDemo, -} - impl Default for CurveDemo { fn default() -> Self { Self { @@ -33,21 +27,6 @@ impl Default for CurveDemo { } } -impl super::Demo for PlotDemo { - fn name(&self) -> &'static str { - "🗠 Plot" - } - - fn show(&mut self, ctx: &CtxRef, open: &mut bool) { - use super::View; - Window::new(self.name()) - .open(open) - .default_size(vec2(400.0, 400.0)) - .scroll(false) - .show(ctx, |ui| self.ui(ui)); - } -} - impl CurveDemo { fn options_ui(&mut self, ui: &mut Ui) { let Self { @@ -208,7 +187,6 @@ impl MarkerDemo { } curve = curve.marker(marker); } - if !self.show_lines { curve = curve.color(Color32::TRANSPARENT); } @@ -246,6 +224,27 @@ impl Widget for &mut MarkerDemo { } } +#[derive(PartialEq, Default)] +pub struct PlotDemo { + curve_demo: CurveDemo, + marker_demo: MarkerDemo, +} + +impl super::Demo for PlotDemo { + fn name(&self) -> &'static str { + "🗠 Plot" + } + + fn show(&mut self, ctx: &CtxRef, open: &mut bool) { + use super::View; + Window::new(self.name()) + .open(open) + .default_size(vec2(400.0, 400.0)) + .scroll(false) + .show(ctx, |ui| self.ui(ui)); + } +} + impl super::View for PlotDemo { fn ui(&mut self, ui: &mut Ui) { ui.vertical_centered(|ui| { From 10e56e0788f7f33cbbe44bd7084f5463a2b766c7 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Fri, 14 May 2021 20:55:12 +0200 Subject: [PATCH 08/50] better highlighting --- egui/src/widgets/plot/mod.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 4b73053d649..7ce011a7969 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -8,12 +8,18 @@ use std::collections::{BTreeMap, HashSet}; pub use items::{Curve, Marker, Value}; use items::{HLine, VLine}; +use legend::LegendEntry; use transform::{Bounds, ScreenTransform}; use crate::*; use color::Hsva; -use self::legend::LegendEntry; +/// Make the color lighter, used for highlighting. +fn brighten(color: &mut Color32) { + let mut hsva = Hsva::from(*color); + hsva.v = (hsva.v * 2.0).at_most(1.0); + *color = hsva.into(); +} // ---------------------------------------------------------------------------- @@ -535,15 +541,20 @@ impl Prepared { color, mut width, mut marker, + highlight, .. } = curve; - let color = color.unwrap_or(Color32::TRANSPARENT); + let mut color = color.unwrap_or(Color32::TRANSPARENT); - if curve.highlight { - width *= 2.0; + if *highlight { + width *= 1.25; + brighten(&mut color); if let Some(marker) = &mut marker { - marker.radius *= 1.2; + marker.radius *= 1.25; + if let Some(marker_color) = &mut marker.color { + brighten(marker_color); + } } } From 75c5c1ac7787a692779b47aa2cf33922f5757b80 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Fri, 14 May 2021 21:07:50 +0200 Subject: [PATCH 09/50] don't draw transparent lines --- egui/src/widgets/plot/mod.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 7ce011a7969..10ebbac91a6 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -570,10 +570,13 @@ impl Prepared { .collect() }); - if values_tf.len() > 1 { - shapes.push(Shape::line(values_tf, Stroke::new(width, color))); - } else { - shapes.push(Shape::circle_filled(values_tf[0], width / 2.0, color)); + if color.a() != 0 { + let line_shape = if values_tf.len() > 1 { + Shape::line(values_tf, Stroke::new(width, color)) + } else { + Shape::circle_filled(values_tf[0], width / 2.0, color) + }; + shapes.push(line_shape); } if let Some(marker_shapes) = marker_shapes { From 90622a56e7542340d3742b71cda78c8a94633351 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Fri, 14 May 2021 21:15:00 +0200 Subject: [PATCH 10/50] use transparent color instead of option --- egui/src/widgets/plot/items.rs | 19 ++++++++++--------- egui/src/widgets/plot/mod.rs | 12 +++++------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index bcadff8c9d7..0fdd635f270 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -90,8 +90,8 @@ pub(crate) enum MarkerShape { #[derive(Debug, Clone, Copy)] pub struct Marker { pub(crate) shape: MarkerShape, - /// Color of the marker. `None` means that it will be picked automatically. - pub(crate) color: Option, + /// Color of the marker. `Color32::TRANSPARENT` means that it will be picked automatically. + pub(crate) color: Color32, /// Whether to fill the marker. Does not apply to all types. pub(crate) filled: bool, /// The maximum extent of the marker from its center. @@ -102,7 +102,7 @@ impl Default for Marker { fn default() -> Self { Self { shape: MarkerShape::Circle, - color: None, + color: Color32::TRANSPARENT, filled: true, radius: 2.0, } @@ -198,7 +198,7 @@ impl Marker { /// Set the marker's color. Defaults to the curve's color. pub fn color(mut self, color: Color32) -> Self { - self.color = Some(color); + self.color = color; self } @@ -226,10 +226,9 @@ impl Marker { radius, } = *self; - if color.is_none() { + if color == Color32::TRANSPARENT { return Vec::new(); } - let color = color.unwrap(); let stroke_size = radius / 5.0; @@ -548,8 +547,10 @@ impl Curve { /// Return the color by which the curve can be identified. pub(crate) fn get_color(&self) -> Option { - self.color - .filter(|color| color.a() != 0) - .or_else(|| self.marker.and_then(|marker| marker.color)) + self.color.filter(|color| color.a() != 0).or_else(|| { + self.marker + .map(|marker| marker.color) + .filter(|color| *color != Color32::TRANSPARENT) + }) } } diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 10ebbac91a6..dddc182cc26 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -125,13 +125,13 @@ impl Plot { let stroke_color = curve.color.get_or_insert_with(|| self.auto_color()); // Give the marker the same color as the stroke if no color has been assigned. if let Some(marker) = &mut curve.marker { - marker.color.get_or_insert_with(|| { - if *stroke_color != Color32::TRANSPARENT { + if marker.color == Color32::TRANSPARENT { + marker.color = if *stroke_color != Color32::TRANSPARENT { *stroke_color } else { self.auto_color() - } - }); + }; + } } self.curves.push(curve); @@ -552,9 +552,7 @@ impl Prepared { brighten(&mut color); if let Some(marker) = &mut marker { marker.radius *= 1.25; - if let Some(marker_color) = &mut marker.color { - brighten(marker_color); - } + brighten(&mut marker.color); } } From 729ce17244e0bf6146373a34ab8452d820495691 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Fri, 14 May 2021 21:25:33 +0200 Subject: [PATCH 11/50] don't brighten curves when highlighting --- egui/src/widgets/plot/mod.rs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index dddc182cc26..9aabacc98ce 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -14,13 +14,6 @@ use transform::{Bounds, ScreenTransform}; use crate::*; use color::Hsva; -/// Make the color lighter, used for highlighting. -fn brighten(color: &mut Color32) { - let mut hsva = Hsva::from(*color); - hsva.v = (hsva.v * 2.0).at_most(1.0); - *color = hsva.into(); -} - // ---------------------------------------------------------------------------- /// Information about the plot that has to persist between frames. @@ -545,14 +538,12 @@ impl Prepared { .. } = curve; - let mut color = color.unwrap_or(Color32::TRANSPARENT); + let color = color.unwrap_or(Color32::TRANSPARENT); if *highlight { - width *= 1.25; - brighten(&mut color); + width *= 1.5; if let Some(marker) = &mut marker { marker.radius *= 1.25; - brighten(&mut marker.color); } } From 7ccc02cd2f1cc63a36fcdfa6e2fcb8904379a9ba Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sun, 16 May 2021 10:59:30 +0200 Subject: [PATCH 12/50] Initial changes to lengend: * Font options * Position options * Internal cleanup --- egui/src/widgets/plot/items.rs | 2 +- egui/src/widgets/plot/legend.rs | 207 +++++++++++++++++++---- egui/src/widgets/plot/mod.rs | 92 ++++------ egui_demo_lib/src/apps/demo/plot_demo.rs | 68 +++++++- 4 files changed, 262 insertions(+), 107 deletions(-) diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index 0fdd635f270..8dc266772cd 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -538,7 +538,7 @@ impl Curve { /// Name of this curve. /// /// If a curve is given a name it will show up in the plot legend - /// (if legends are turned on). + /// (if legends are turned on). Multiple curves may share the same name. #[allow(clippy::needless_pass_by_value)] pub fn name(mut self, name: impl ToString) -> Self { self.name = name.to_string(); diff --git a/egui/src/widgets/plot/legend.rs b/egui/src/widgets/plot/legend.rs index af3754f1d6b..7b7aaa2386b 100644 --- a/egui/src/widgets/plot/legend.rs +++ b/egui/src/widgets/plot/legend.rs @@ -1,18 +1,60 @@ -use std::string::String; +use std::{ + collections::{BTreeMap, HashSet}, + string::String, +}; +use super::Curve; use crate::*; -pub(crate) struct LegendEntry { - pub text: String, - pub color: Color32, - pub checked: bool, - pub hovered: bool, +/// Where to place the plot legend. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum LegendPosition { + TopLeft, + TopRight, + BottomLeft, + BottomRight, +} + +impl LegendPosition { + pub fn all() -> impl Iterator { + [ + LegendPosition::TopLeft, + LegendPosition::TopRight, + LegendPosition::BottomLeft, + LegendPosition::BottomRight, + ] + .iter() + .copied() + } +} + +/// The configuration for a plot legend. +#[derive(Clone, Copy, PartialEq)] +pub struct Legend { + pub text_style: TextStyle, + pub position: LegendPosition, +} + +impl Default for Legend { + fn default() -> Self { + Self { + text_style: TextStyle::Body, + position: LegendPosition::TopRight, + } + } +} + +struct LegendEntry { + config: Legend, + color: Option, + checked: bool, + hovered: bool, } impl LegendEntry { - pub fn new(text: String, color: Color32, checked: bool) -> Self { + fn new(config: Legend, color: Option, checked: bool) -> Self { Self { - text, + config, color, checked, hovered: false, @@ -20,62 +62,155 @@ impl LegendEntry { } } -impl Widget for &mut LegendEntry { +impl Widget for (&String, &mut LegendEntry) { fn ui(self, ui: &mut Ui) -> Response { - let LegendEntry { - checked, + let ( text, - color, - .. - } = self; - let icon_width = ui.spacing().icon_width; - let icon_spacing = ui.spacing().icon_spacing; - let padding = vec2(2.0, 2.0); - let total_extra = padding + vec2(icon_width + icon_spacing, 0.0) + padding; + LegendEntry { + config, + color, + checked, + hovered, + }, + ) = self; - let text_style = TextStyle::Button; - let galley = ui.fonts().layout_no_wrap(text_style, text.clone()); + let galley = ui.fonts().layout_no_wrap(config.text_style, text.clone()); - let mut desired_size = total_extra + galley.size; - desired_size = desired_size.at_least(ui.spacing().interact_size); - desired_size.y = desired_size.y.at_least(icon_width); + let icon_size = galley.size.y; + let icon_spacing = icon_size / 5.0; + let total_extra = vec2(icon_size + icon_spacing, 0.0); + let desired_size = total_extra + galley.size; let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click()); - let rect = rect.shrink2(padding); response.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, &galley.text)); let visuals = ui.style().interact(&response); - let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); + let icon_position_x = match config.position { + LegendPosition::BottomLeft | LegendPosition::TopLeft => rect.left() + icon_size / 2.0, + LegendPosition::BottomRight | LegendPosition::TopRight => { + rect.right() - icon_size / 2.0 + } + }; + let icon_position = pos2(icon_position_x, rect.center().y); + let icon_rect = Rect::from_center_size(icon_position, vec2(icon_size, icon_size)); let painter = ui.painter(); painter.add(Shape::Circle { - center: big_icon_rect.center(), - radius: big_icon_rect.width() / 2.0 + visuals.expansion, + center: icon_rect.center(), + radius: icon_size * 0.5, fill: visuals.bg_fill, stroke: visuals.bg_stroke, }); if *checked { + let neutral_color = ui.visuals().noninteractive().fg_stroke.color; painter.add(Shape::Circle { - center: small_icon_rect.center(), - radius: small_icon_rect.width() * 0.8, - fill: *color, + center: icon_rect.center(), + radius: icon_size * 0.4, + fill: color.unwrap_or(neutral_color), stroke: Default::default(), }); } - let text_position = pos2( - rect.left() + padding.x + icon_width + icon_spacing, - rect.center().y - 0.5 * galley.size.y, - ); + let text_position_x = match config.position { + LegendPosition::BottomLeft | LegendPosition::TopLeft => { + rect.left() + icon_size + icon_spacing + } + LegendPosition::BottomRight | LegendPosition::TopRight => { + rect.right() - icon_size - icon_spacing - galley.size.x + } + }; + let text_position = pos2(text_position_x, rect.center().y - 0.5 * galley.size.y); painter.galley(text_position, galley, visuals.text_color()); - self.checked ^= response.clicked_by(PointerButton::Primary); - self.hovered = response.hovered(); + *checked ^= response.clicked_by(PointerButton::Primary); + *hovered = response.hovered(); response } } + +pub(crate) struct LegendWidget { + rect: Rect, + entries: BTreeMap, + config: Legend, +} + +impl LegendWidget { + /// Create a new legend from curves, the names of curves that are hidden and the style of the + /// text. Returns `None` if the legend has no entries. + pub fn try_new( + rect: Rect, + config: Legend, + curves: &[Curve], + hidden_curves: &HashSet, + ) -> Option { + // Collect the legend entries. If multiple curves have the same name, they share a + // checkbox. If their colors don't match, we pick a neutral color for the checkbox. + let mut entries: BTreeMap = BTreeMap::new(); + curves + .iter() + .filter(|curve| !curve.name.is_empty()) + .for_each(|curve| { + entries + .entry(curve.name.clone()) + .and_modify(|entry| { + if entry.color != curve.get_color() { + entry.color = None + } + }) + .or_insert_with(|| { + let color = curve.get_color(); + let checked = !hidden_curves.contains(&curve.name); + LegendEntry::new(config, color, checked) + }); + }); + (!entries.is_empty()).then(|| Self { + rect, + entries, + config, + }) + } + + // Get the names of the hidden curves. + pub fn get_hidden_curves(&self) -> HashSet { + self.entries + .iter() + .filter(|(_, entry)| !entry.checked) + .map(|(name, _)| name.clone()) + .collect() + } + + // Get the name of the hovered curve. + pub fn get_hovered_entry_name(&self) -> Option<&str> { + self.entries + .iter() + .find(|(_, entry)| entry.hovered) + .map(|(name, _)| name.as_str()) + } +} + +impl Widget for &mut LegendWidget { + fn ui(self, ui: &mut Ui) -> Response { + let main_dir = match self.config.position { + LegendPosition::TopLeft | LegendPosition::TopRight => Direction::TopDown, + LegendPosition::BottomLeft | LegendPosition::BottomRight => Direction::BottomUp, + }; + let cross_align = match self.config.position { + LegendPosition::TopLeft | LegendPosition::BottomLeft => Align::LEFT, + LegendPosition::TopRight | LegendPosition::BottomRight => Align::RIGHT, + }; + let layout = Layout::from_main_dir_and_cross_align(main_dir, cross_align); + let legend_pad = 2.0; + let legend_rect = self.rect.shrink(legend_pad); + let mut legend_ui = ui.child_ui(legend_rect, layout); + self.entries + .iter_mut() + .map(|entry| legend_ui.add(entry)) + .reduce(|r1, r2| r1.union(r2)) + .unwrap() + } +} diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 9aabacc98ce..0a81a6f9d78 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -4,11 +4,12 @@ mod items; mod legend; mod transform; -use std::collections::{BTreeMap, HashSet}; +use std::collections::HashSet; pub use items::{Curve, Marker, Value}; use items::{HLine, VLine}; -use legend::LegendEntry; +use legend::LegendWidget; +pub use legend::{Legend, LegendPosition}; use transform::{Bounds, ScreenTransform}; use crate::*; @@ -66,7 +67,7 @@ pub struct Plot { show_x: bool, show_y: bool, - show_legend: bool, + legend_config: Option, } impl Plot { @@ -95,7 +96,7 @@ impl Plot { show_x: true, show_y: true, - show_legend: true, + legend_config: None, } } @@ -258,9 +259,15 @@ impl Plot { self } - /// Whether to show a legend including all named curves. Default: `true`. + #[deprecated = "Use `Plot::legend` instead"] pub fn show_legend(mut self, show: bool) -> Self { - self.show_legend = show; + self.legend_config = show.then(Legend::default); + self + } + + /// Show a legend including all named curves. + pub fn legend(mut self, legend: Legend) -> Self { + self.legend_config = Some(legend); self } } @@ -286,7 +293,7 @@ impl Widget for Plot { view_aspect, mut show_x, mut show_y, - show_legend, + legend_config, } = self; let plot_id = ui.make_persistent_id(name); @@ -341,64 +348,25 @@ impl Widget for Plot { stroke: ui.visuals().window_stroke(), }); - // --- Legend --- - - if show_legend { - // Collect the legend entries. If multiple curves have the same name, they share a - // checkbox. If their colors don't match, we pick a neutral color for the checkbox. - let mut legend_entries: BTreeMap = BTreeMap::new(); - let neutral_color = ui.visuals().noninteractive().fg_stroke.color; - curves - .iter() - .filter(|curve| !curve.name.is_empty()) - .for_each(|curve| { - let checked = !hidden_curves.contains(&curve.name); - let text = curve.name.clone(); - legend_entries - .entry(curve.name.clone()) - .and_modify(|entry| { - if Some(entry.color) != curve.get_color() { - entry.color = neutral_color - } - }) - .or_insert_with(|| { - let color = curve.get_color().unwrap_or(neutral_color); - LegendEntry::new(text, color, checked) - }); - }); - - // Show the legend. - let mut legend_ui = ui.child_ui(rect, Layout::top_down(Align::LEFT)); - legend_entries.values_mut().for_each(|entry| { - let response = legend_ui.add(entry); - if response.hovered() { - show_x = false; - show_y = false; - } - }); - - // Get the names of the hidden curves. - hidden_curves = legend_entries - .values() - .filter(|entry| !entry.checked) - .map(|entry| entry.text.clone()) - .collect(); - - // Highlight the hovered curves. - legend_entries - .values() - .filter(|entry| entry.hovered) - .for_each(|entry| { + // Legend + if let Some(mut legend) = legend_config + .and_then(|config| LegendWidget::try_new(rect, config, &curves, &hidden_curves)) + { + if ui.add(&mut legend).hovered() { + show_x = false; + show_y = false; + // Highlight the hovered curve(s). + if let Some(hovered_entry) = legend.get_hovered_entry_name() { curves.iter_mut().for_each(|curve| { - curve.highlight |= curve.name == entry.text; + curve.highlight |= curve.name == hovered_entry; }); - }); - - // Remove deselected curves. - curves.retain(|curve| !hidden_curves.contains(&curve.name)); + } + } + // Update the hidden curves. + hidden_curves = legend.get_hidden_curves(); } - - // --- + // Remove the deselected curves. + curves.retain(|curve| !hidden_curves.contains(&curve.name)); auto_bounds |= response.double_clicked_by(PointerButton::Primary); diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 69aae8d6add..74da21e0f6d 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -1,5 +1,5 @@ -use egui::plot::{Curve, Marker, Plot, Value}; use egui::*; +use plot::{Curve, Legend, LegendPosition, Marker, Plot, Value}; use std::f64::consts::TAU; #[derive(PartialEq)] @@ -9,7 +9,6 @@ struct CurveDemo { circle_radius: f64, circle_center: Pos2, square: bool, - legend: bool, proportional: bool, } @@ -21,7 +20,6 @@ impl Default for CurveDemo { circle_radius: 1.5, circle_center: Pos2::new(0.0, 0.0), square: false, - legend: true, proportional: true, } } @@ -35,7 +33,6 @@ impl CurveDemo { circle_radius, circle_center, square, - legend, proportional, .. } = self; @@ -65,12 +62,10 @@ impl CurveDemo { }); }); - ui.horizontal(|ui| { + ui.vertical(|ui| { ui.style_mut().wrap = Some(false); ui.checkbox(animate, "animate"); - ui.add_space(8.0); ui.checkbox(square, "square view"); - ui.checkbox(legend, "legend"); ui.checkbox(proportional, "proportional data axes"); }); }); @@ -129,7 +124,7 @@ impl Widget for &mut CurveDemo { .curve(self.sin()) .curve(self.thingy()) .height(300.0) - .show_legend(self.legend); + .legend(Legend::default()); if self.square { plot = plot.view_aspect(1.0); } @@ -219,15 +214,71 @@ impl Widget for &mut MarkerDemo { let markers_plot = Plot::new("Markers Demo") .curves(self.markers()) .height(300.0) + .legend(Legend::default()) .data_aspect(1.0); ui.add(markers_plot) } } +#[derive(PartialEq)] +struct LegendDemo { + config: Legend, +} + +impl Default for LegendDemo { + fn default() -> Self { + Self { + config: Legend::default(), + } + } +} + +impl LegendDemo { + fn line_with_slope(slope: f64) -> Curve { + Curve::from_explicit_callback(move |x| slope * x, f64::NEG_INFINITY..=f64::INFINITY, 100) + } + fn sin() -> Curve { + Curve::from_explicit_callback(move |x| x.sin(), f64::NEG_INFINITY..=f64::INFINITY, 100) + } + fn cos() -> Curve { + Curve::from_explicit_callback(move |x| x.cos(), f64::NEG_INFINITY..=f64::INFINITY, 100) + } +} + +impl Widget for &mut LegendDemo { + fn ui(self, ui: &mut Ui) -> Response { + let LegendDemo { config } = self; + + ui.label("Text Style:"); + ui.horizontal(|ui| { + TextStyle::all().for_each(|style| { + ui.selectable_value(&mut config.text_style, style, format!("{:?}", style)); + }); + }); + ui.label("Position:"); + ui.horizontal(|ui| { + LegendPosition::all().for_each(|position| { + ui.selectable_value(&mut config.position, position, format!("{:?}", position)); + }); + }); + let legend_plot = Plot::new("Legend Demo") + .curve(LegendDemo::line_with_slope(0.5).name("lines")) + .curve(LegendDemo::line_with_slope(1.0).name("lines")) + .curve(LegendDemo::line_with_slope(2.0).name("lines")) + .curve(LegendDemo::sin().name("sin(x)")) + .curve(LegendDemo::cos().name("cos(x)")) + .height(300.0) + .legend(*config) + .data_aspect(1.0); + ui.add(legend_plot) + } +} + #[derive(PartialEq, Default)] pub struct PlotDemo { curve_demo: CurveDemo, marker_demo: MarkerDemo, + legend_demo: LegendDemo, } impl super::Demo for PlotDemo { @@ -253,5 +304,6 @@ impl super::View for PlotDemo { }); ui.collapsing("Curves", |ui| ui.add(&mut self.curve_demo)); ui.collapsing("Markers", |ui| ui.add(&mut self.marker_demo)); + ui.collapsing("Legend", |ui| ui.add(&mut self.legend_demo)); } } From 15820d7c9b7f9bf14f2bbf469f280e02617c329e Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sun, 16 May 2021 12:47:38 +0200 Subject: [PATCH 13/50] draw legend on top of curves --- egui/src/widgets/plot/legend.rs | 10 ++++- egui/src/widgets/plot/mod.rs | 76 ++++++++++++++++++++------------- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/egui/src/widgets/plot/legend.rs b/egui/src/widgets/plot/legend.rs index 7b7aaa2386b..3d34764dca5 100644 --- a/egui/src/widgets/plot/legend.rs +++ b/egui/src/widgets/plot/legend.rs @@ -7,6 +7,7 @@ use super::Curve; use crate::*; /// Where to place the plot legend. +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] #[derive(Debug, Clone, Copy, PartialEq)] pub enum LegendPosition { TopLeft, @@ -29,6 +30,7 @@ impl LegendPosition { } /// The configuration for a plot legend. +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] #[derive(Clone, Copy, PartialEq)] pub struct Legend { pub text_style: TextStyle, @@ -44,6 +46,8 @@ impl Default for Legend { } } +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +#[derive(Clone)] struct LegendEntry { config: Legend, color: Option, @@ -133,6 +137,8 @@ impl Widget for (&String, &mut LegendEntry) { } } +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +#[derive(Clone)] pub(crate) struct LegendWidget { rect: Rect, entries: BTreeMap, @@ -185,11 +191,11 @@ impl LegendWidget { } // Get the name of the hovered curve. - pub fn get_hovered_entry_name(&self) -> Option<&str> { + pub fn get_hovered_entry_name(&self) -> Option { self.entries .iter() .find(|(_, entry)| entry.hovered) - .map(|(name, _)| name.as_str()) + .map(|(name, _)| name.to_string()) } } diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 0a81a6f9d78..0e6d719dcec 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -24,6 +24,7 @@ struct PlotMemory { bounds: Bounds, auto_bounds: bool, hidden_curves: HashSet, + hovered_entry: Option, } // ---------------------------------------------------------------------------- @@ -291,8 +292,8 @@ impl Widget for Plot { min_size, data_aspect, view_aspect, - mut show_x, - mut show_y, + show_x, + show_y, legend_config, } = self; @@ -304,6 +305,7 @@ impl Widget for Plot { bounds: min_auto_bounds, auto_bounds: !min_auto_bounds.is_valid(), hidden_curves: HashSet::new(), + hovered_entry: None, }) .clone(); @@ -311,6 +313,7 @@ impl Widget for Plot { mut bounds, mut auto_bounds, mut hidden_curves, + mut hovered_entry, } = memory; // Determine the size of the plot in the UI @@ -348,26 +351,21 @@ impl Widget for Plot { stroke: ui.visuals().window_stroke(), }); - // Legend - if let Some(mut legend) = legend_config - .and_then(|config| LegendWidget::try_new(rect, config, &curves, &hidden_curves)) - { - if ui.add(&mut legend).hovered() { - show_x = false; - show_y = false; - // Highlight the hovered curve(s). - if let Some(hovered_entry) = legend.get_hovered_entry_name() { - curves.iter_mut().for_each(|curve| { - curve.highlight |= curve.name == hovered_entry; - }); - } - } - // Update the hidden curves. - hidden_curves = legend.get_hidden_curves(); - } // Remove the deselected curves. curves.retain(|curve| !hidden_curves.contains(&curve.name)); + // Highlight the hovered curves. + if let Some(hovered_name) = &hovered_entry { + curves + .iter_mut() + .filter(|entry| &entry.name == hovered_name) + .for_each(|entry| entry.highlight = true); + } + + // Legend + let mut legend = legend_config + .and_then(|config| LegendWidget::try_new(rect, config, &curves, &hidden_curves)); + auto_bounds |= response.double_clicked_by(PointerButton::Primary); // Set bounds automatically based on content. @@ -432,22 +430,29 @@ impl Widget for Plot { let bounds = *transform.bounds(); - let prepared = Prepared { + let mut prepared = Prepared { curves, hlines, vlines, show_x, show_y, transform, + legend: legend.as_mut(), }; prepared.ui(ui, &response); + if let Some(legend) = legend { + hidden_curves = legend.get_hidden_curves(); + hovered_entry = legend.get_hovered_entry_name(); + } + ui.memory().id_data.insert( plot_id, PlotMemory { bounds, auto_bounds, hidden_curves, + hovered_entry, }, ); @@ -459,20 +464,20 @@ impl Widget for Plot { } } -struct Prepared { +struct Prepared<'a> { curves: Vec, hlines: Vec, vlines: Vec, show_x: bool, show_y: bool, transform: ScreenTransform, + legend: Option<&'a mut LegendWidget>, } -impl Prepared { - fn ui(&self, ui: &mut Ui, response: &Response) { - let Self { transform, .. } = self; - +impl Prepared<'_> { + fn ui(&mut self, ui: &mut Ui, response: &Response) { let mut shapes = Vec::new(); + let transform = &self.transform; for d in 0..2 { self.paint_axis(ui, d, &mut shapes); @@ -541,11 +546,20 @@ impl Prepared { } } + let painter_rect = ui.painter().sub_region(*transform.frame()); + + painter_rect.extend(shapes); + if let Some(pointer) = response.hover_pos() { - self.hover(ui, pointer, &mut shapes); + painter_rect.extend(self.hover(ui, pointer)); } - ui.painter().sub_region(*transform.frame()).extend(shapes); + if let Some(legend) = self.legend.as_deref_mut() { + if ui.add(legend).hovered() { + self.show_x = false; + self.show_y = false; + } + } } fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec) { @@ -637,7 +651,7 @@ impl Prepared { } } - fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec) { + fn hover(&self, ui: &Ui, pointer: Pos2) -> Vec { let Self { transform, show_x, @@ -646,8 +660,10 @@ impl Prepared { .. } = self; + let mut shapes = Vec::new(); + if !show_x && !show_y { - return; + return Vec::new(); } let interact_radius: f32 = 16.0; @@ -731,5 +747,7 @@ impl Prepared { TextStyle::Body, ui.visuals().text_color(), )); + + shapes } } From 1d7b252192b7014492ec0e45c00d9dada84e7670 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sun, 16 May 2021 23:25:40 +0200 Subject: [PATCH 14/50] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9b5db207d4..03d8923b79d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [ ## Unreleased - +* [Line markers for plots](https://github.com/emilk/egui/pull/363). ## 0.12.0 - 2021-05-10 - Multitouch, user memory, window pivots, and improved plots From eafff6a4c4be03eba7cd745f99772c0a2fe4bc72 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sun, 16 May 2021 23:48:24 +0200 Subject: [PATCH 15/50] fix legend checkboxes --- egui/src/widgets/plot/mod.rs | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 0e6d719dcec..13565544c40 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -351,6 +351,10 @@ impl Widget for Plot { stroke: ui.visuals().window_stroke(), }); + // Legend + let legend = legend_config + .and_then(|config| LegendWidget::try_new(rect, config, &curves, &hidden_curves)); + // Remove the deselected curves. curves.retain(|curve| !hidden_curves.contains(&curve.name)); @@ -362,10 +366,6 @@ impl Widget for Plot { .for_each(|entry| entry.highlight = true); } - // Legend - let mut legend = legend_config - .and_then(|config| LegendWidget::try_new(rect, config, &curves, &hidden_curves)); - auto_bounds |= response.double_clicked_by(PointerButton::Primary); // Set bounds automatically based on content. @@ -430,16 +430,16 @@ impl Widget for Plot { let bounds = *transform.bounds(); - let mut prepared = Prepared { + let prepared = Prepared { curves, hlines, vlines, show_x, show_y, transform, - legend: legend.as_mut(), + legend, }; - prepared.ui(ui, &response); + let legend = prepared.ui(ui, &response); if let Some(legend) = legend { hidden_curves = legend.get_hidden_curves(); @@ -464,18 +464,18 @@ impl Widget for Plot { } } -struct Prepared<'a> { +struct Prepared { curves: Vec, hlines: Vec, vlines: Vec, show_x: bool, show_y: bool, transform: ScreenTransform, - legend: Option<&'a mut LegendWidget>, + legend: Option, } -impl Prepared<'_> { - fn ui(&mut self, ui: &mut Ui, response: &Response) { +impl Prepared { + fn ui(mut self, ui: &mut Ui, response: &Response) -> Option { let mut shapes = Vec::new(); let transform = &self.transform; @@ -550,16 +550,18 @@ impl Prepared<'_> { painter_rect.extend(shapes); - if let Some(pointer) = response.hover_pos() { - painter_rect.extend(self.hover(ui, pointer)); - } - - if let Some(legend) = self.legend.as_deref_mut() { + if let Some(legend) = &mut self.legend { if ui.add(legend).hovered() { self.show_x = false; self.show_y = false; } } + + if let Some(pointer) = response.hover_pos() { + painter_rect.extend(self.hover(ui, pointer)); + } + + self.legend } fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec) { From 3cb3a14b83feff5a6f849cba30faa83e50c4e490 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Mon, 17 May 2021 00:20:54 +0200 Subject: [PATCH 16/50] simplify legend --- egui/src/widgets/plot/mod.rs | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 13565544c40..0efad9c39d8 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -292,8 +292,8 @@ impl Widget for Plot { min_size, data_aspect, view_aspect, - show_x, - show_y, + mut show_x, + mut show_y, legend_config, } = self; @@ -354,10 +354,13 @@ impl Widget for Plot { // Legend let legend = legend_config .and_then(|config| LegendWidget::try_new(rect, config, &curves, &hidden_curves)); - + // Don't show hover cursor when hovering over legend. + if hovered_entry.is_some() { + show_x = false; + show_y = false; + } // Remove the deselected curves. curves.retain(|curve| !hidden_curves.contains(&curve.name)); - // Highlight the hovered curves. if let Some(hovered_name) = &hovered_entry { curves @@ -437,11 +440,11 @@ impl Widget for Plot { show_x, show_y, transform, - legend, }; - let legend = prepared.ui(ui, &response); + prepared.ui(ui, &response); - if let Some(legend) = legend { + if let Some(mut legend) = legend { + ui.add(&mut legend); hidden_curves = legend.get_hidden_curves(); hovered_entry = legend.get_hovered_entry_name(); } @@ -471,11 +474,10 @@ struct Prepared { show_x: bool, show_y: bool, transform: ScreenTransform, - legend: Option, } impl Prepared { - fn ui(mut self, ui: &mut Ui, response: &Response) -> Option { + fn ui(self, ui: &mut Ui, response: &Response) { let mut shapes = Vec::new(); let transform = &self.transform; @@ -550,18 +552,9 @@ impl Prepared { painter_rect.extend(shapes); - if let Some(legend) = &mut self.legend { - if ui.add(legend).hovered() { - self.show_x = false; - self.show_y = false; - } - } - if let Some(pointer) = response.hover_pos() { painter_rect.extend(self.hover(ui, pointer)); } - - self.legend } fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec) { From 4d4f579725c0c46532ad9fd3e809861ba2df30f8 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Mon, 17 May 2021 21:06:49 +0200 Subject: [PATCH 17/50] remove unnecessary derives --- egui/src/widgets/plot/legend.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/egui/src/widgets/plot/legend.rs b/egui/src/widgets/plot/legend.rs index 3d34764dca5..e747bfed1c1 100644 --- a/egui/src/widgets/plot/legend.rs +++ b/egui/src/widgets/plot/legend.rs @@ -7,7 +7,6 @@ use super::Curve; use crate::*; /// Where to place the plot legend. -#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] #[derive(Debug, Clone, Copy, PartialEq)] pub enum LegendPosition { TopLeft, @@ -30,7 +29,6 @@ impl LegendPosition { } /// The configuration for a plot legend. -#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] #[derive(Clone, Copy, PartialEq)] pub struct Legend { pub text_style: TextStyle, @@ -46,7 +44,6 @@ impl Default for Legend { } } -#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] #[derive(Clone)] struct LegendEntry { config: Legend, @@ -137,7 +134,6 @@ impl Widget for (&String, &mut LegendEntry) { } } -#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] #[derive(Clone)] pub(crate) struct LegendWidget { rect: Rect, From ef6dc6e196d409995749ffe3febf0f76187487c6 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Mon, 17 May 2021 21:49:32 +0200 Subject: [PATCH 18/50] remove config from legend entries --- egui/src/widgets/plot/legend.rs | 60 +++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/egui/src/widgets/plot/legend.rs b/egui/src/widgets/plot/legend.rs index e747bfed1c1..d0428112f18 100644 --- a/egui/src/widgets/plot/legend.rs +++ b/egui/src/widgets/plot/legend.rs @@ -46,16 +46,14 @@ impl Default for Legend { #[derive(Clone)] struct LegendEntry { - config: Legend, color: Option, checked: bool, hovered: bool, } impl LegendEntry { - fn new(config: Legend, color: Option, checked: bool) -> Self { + fn new(color: Option, checked: bool) -> Self { Self { - config, color, checked, hovered: false, @@ -68,14 +66,15 @@ impl Widget for (&String, &mut LegendEntry) { let ( text, LegendEntry { - config, color, checked, hovered, }, ) = self; - let galley = ui.fonts().layout_no_wrap(config.text_style, text.clone()); + let galley = ui + .fonts() + .layout_no_wrap(ui.style().body_text_style, text.clone()); let icon_size = galley.size.y; let icon_spacing = icon_size / 5.0; @@ -87,12 +86,12 @@ impl Widget for (&String, &mut LegendEntry) { response.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, &galley.text)); let visuals = ui.style().interact(&response); + let flipped = ui.layout().cross_align() == Align::RIGHT; - let icon_position_x = match config.position { - LegendPosition::BottomLeft | LegendPosition::TopLeft => rect.left() + icon_size / 2.0, - LegendPosition::BottomRight | LegendPosition::TopRight => { - rect.right() - icon_size / 2.0 - } + let icon_position_x = if flipped { + rect.right() - icon_size / 2.0 + } else { + rect.left() + icon_size / 2.0 }; let icon_position = pos2(icon_position_x, rect.center().y); let icon_rect = Rect::from_center_size(icon_position, vec2(icon_size, icon_size)); @@ -116,14 +115,12 @@ impl Widget for (&String, &mut LegendEntry) { }); } - let text_position_x = match config.position { - LegendPosition::BottomLeft | LegendPosition::TopLeft => { - rect.left() + icon_size + icon_spacing - } - LegendPosition::BottomRight | LegendPosition::TopRight => { - rect.right() - icon_size - icon_spacing - galley.size.x - } + let text_position_x = if flipped { + rect.right() - icon_size - icon_spacing - galley.size.x + } else { + rect.left() + icon_size + icon_spacing }; + let text_position = pos2(text_position_x, rect.center().y - 0.5 * galley.size.y); painter.galley(text_position, galley, visuals.text_color()); @@ -167,7 +164,7 @@ impl LegendWidget { .or_insert_with(|| { let color = curve.get_color(); let checked = !hidden_curves.contains(&curve.name); - LegendEntry::new(config, color, checked) + LegendEntry::new(color, checked) }); }); (!entries.is_empty()).then(|| Self { @@ -197,22 +194,33 @@ impl LegendWidget { impl Widget for &mut LegendWidget { fn ui(self, ui: &mut Ui) -> Response { - let main_dir = match self.config.position { + let LegendWidget { + rect, + entries, + config, + } = self; + + let main_dir = match config.position { LegendPosition::TopLeft | LegendPosition::TopRight => Direction::TopDown, LegendPosition::BottomLeft | LegendPosition::BottomRight => Direction::BottomUp, }; - let cross_align = match self.config.position { + let cross_align = match config.position { LegendPosition::TopLeft | LegendPosition::BottomLeft => Align::LEFT, LegendPosition::TopRight | LegendPosition::BottomRight => Align::RIGHT, }; let layout = Layout::from_main_dir_and_cross_align(main_dir, cross_align); let legend_pad = 2.0; - let legend_rect = self.rect.shrink(legend_pad); + let legend_rect = rect.shrink(legend_pad); let mut legend_ui = ui.child_ui(legend_rect, layout); - self.entries - .iter_mut() - .map(|entry| legend_ui.add(entry)) - .reduce(|r1, r2| r1.union(r2)) - .unwrap() + legend_ui + .scope(|ui| { + ui.style_mut().body_text_style = config.text_style; + entries + .iter_mut() + .map(|entry| ui.add(entry)) + .reduce(|r1, r2| r1.union(r2)) + .unwrap() + }) + .inner } } From e62a8166efc4d17fa61ad1e8c7e09d41ae3197a2 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Fri, 21 May 2021 18:55:12 +0200 Subject: [PATCH 19/50] avoid allocations and use line_segment --- egui/src/widgets/plot/items.rs | 136 +++++++++++++-------------------- egui/src/widgets/plot/mod.rs | 8 +- 2 files changed, 57 insertions(+), 87 deletions(-) diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index 0fdd635f270..9994a9652ad 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -214,7 +214,7 @@ impl Marker { self } - pub(crate) fn get_shapes(&self, position: &Pos2) -> Vec { + pub(crate) fn get_shapes(&self, position: &Pos2, shapes: &mut Vec) { let sqrt_3 = 3f32.sqrt(); let frac_sqrt_3_2 = 3f32.sqrt() / 2.0; let frac_1_sqrt_2 = 1.0 / 2f32.sqrt(); @@ -227,17 +227,12 @@ impl Marker { } = *self; if color == Color32::TRANSPARENT { - return Vec::new(); + return; } let stroke_size = radius / 5.0; - let tf = |offset: Vec| -> Vec { - offset - .into_iter() - .map(|offset| *position + radius * offset) - .collect() - }; + let tf = |dx: f32, dy: f32| -> Pos2 { *position + radius * vec2(dx, dy) }; let default_stroke = Stroke::new(stroke_size, color); let stroke = (!filled).then(|| default_stroke).unwrap_or_default(); @@ -245,130 +240,105 @@ impl Marker { match shape { MarkerShape::Circle => { - vec![Shape::Circle { + shapes.push(Shape::Circle { center: *position, radius, fill, stroke, - }] + }); } MarkerShape::Diamond => { - let offsets = vec![ - vec2(1.0, 0.0), - vec2(0.0, -1.0), - vec2(-1.0, 0.0), - vec2(0.0, 1.0), - ]; - let points = tf(offsets); - vec![Shape::Path { + let points = vec![tf(1.0, 0.0), tf(0.0, -1.0), tf(-1.0, 0.0), tf(0.0, 1.0)]; + shapes.push(Shape::Path { points, closed: true, fill, stroke, - }] + }); } MarkerShape::Square => { - let offsets = vec![ - vec2(frac_1_sqrt_2, frac_1_sqrt_2), - vec2(frac_1_sqrt_2, -frac_1_sqrt_2), - vec2(-frac_1_sqrt_2, -frac_1_sqrt_2), - vec2(-frac_1_sqrt_2, frac_1_sqrt_2), + let points = vec![ + tf(frac_1_sqrt_2, frac_1_sqrt_2), + tf(frac_1_sqrt_2, -frac_1_sqrt_2), + tf(-frac_1_sqrt_2, -frac_1_sqrt_2), + tf(-frac_1_sqrt_2, frac_1_sqrt_2), ]; - let points = tf(offsets); - vec![Shape::Path { + shapes.push(Shape::Path { points, closed: true, fill, stroke, - }] + }); } MarkerShape::Cross => { - let diagonal1 = tf(vec![ - vec2(-frac_1_sqrt_2, -frac_1_sqrt_2), - vec2(frac_1_sqrt_2, frac_1_sqrt_2), - ]); - let diagonal2 = tf(vec![ - vec2(frac_1_sqrt_2, -frac_1_sqrt_2), - vec2(-frac_1_sqrt_2, frac_1_sqrt_2), - ]); - vec![ - Shape::line(diagonal1, default_stroke), - Shape::line(diagonal2, default_stroke), - ] + let diagonal1 = [ + tf(-frac_1_sqrt_2, -frac_1_sqrt_2), + tf(frac_1_sqrt_2, frac_1_sqrt_2), + ]; + let diagonal2 = [ + tf(frac_1_sqrt_2, -frac_1_sqrt_2), + tf(-frac_1_sqrt_2, frac_1_sqrt_2), + ]; + shapes.push(Shape::line_segment(diagonal1, default_stroke)); + shapes.push(Shape::line_segment(diagonal2, default_stroke)); } MarkerShape::Plus => { - let horizontal = tf(vec![vec2(-1.0, 0.0), vec2(1.0, 0.0)]); - let vertical = tf(vec![vec2(0.0, -1.0), vec2(0.0, 1.0)]); - vec![ - Shape::line(horizontal, default_stroke), - Shape::line(vertical, default_stroke), - ] + let horizontal = [tf(-1.0, 0.0), tf(1.0, 0.0)]; + let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)]; + shapes.push(Shape::line_segment(horizontal, default_stroke)); + shapes.push(Shape::line_segment(vertical, default_stroke)); } MarkerShape::Up => { - let offsets = vec![ - vec2(0.0, -1.0), - vec2(-0.5 * sqrt_3, 0.5), - vec2(0.5 * sqrt_3, 0.5), - ]; - let points = tf(offsets); - vec![Shape::Path { + let points = vec![tf(0.0, -1.0), tf(-0.5 * sqrt_3, 0.5), tf(0.5 * sqrt_3, 0.5)]; + shapes.push(Shape::Path { points, closed: true, fill, stroke, - }] + }); } MarkerShape::Down => { - let offsets = vec![ - vec2(0.0, 1.0), - vec2(-0.5 * sqrt_3, -0.5), - vec2(0.5 * sqrt_3, -0.5), + let points = vec![ + tf(0.0, 1.0), + tf(-0.5 * sqrt_3, -0.5), + tf(0.5 * sqrt_3, -0.5), ]; - let points = tf(offsets); - vec![Shape::Path { + shapes.push(Shape::Path { points, closed: true, fill, stroke, - }] + }); } MarkerShape::Left => { - let offsets = vec![ - vec2(-1.0, 0.0), - vec2(0.5, -0.5 * sqrt_3), - vec2(0.5, 0.5 * sqrt_3), - ]; - let points = tf(offsets); - vec![Shape::Path { + let points = vec![tf(-1.0, 0.0), tf(0.5, -0.5 * sqrt_3), tf(0.5, 0.5 * sqrt_3)]; + shapes.push(Shape::Path { points, closed: true, fill, stroke, - }] + }); } MarkerShape::Right => { - let offsets = vec![ - vec2(1.0, 0.0), - vec2(-0.5, -0.5 * sqrt_3), - vec2(-0.5, 0.5 * sqrt_3), + let points = vec![ + tf(1.0, 0.0), + tf(-0.5, -0.5 * sqrt_3), + tf(-0.5, 0.5 * sqrt_3), ]; - let points = tf(offsets); - vec![Shape::Path { + shapes.push(Shape::Path { points, closed: true, fill, stroke, - }] + }); } MarkerShape::Asterisk => { - let vertical = tf(vec![vec2(0.0, -1.0), vec2(0.0, 1.0)]); - let diagonal1 = tf(vec![vec2(-frac_sqrt_3_2, 0.5), vec2(frac_sqrt_3_2, -0.5)]); - let diagonal2 = tf(vec![vec2(-frac_sqrt_3_2, -0.5), vec2(frac_sqrt_3_2, 0.5)]); - vec![ - Shape::line(vertical, default_stroke), - Shape::line(diagonal1, default_stroke), - Shape::line(diagonal2, default_stroke), - ] + let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)]; + let diagonal1 = [tf(-frac_sqrt_3_2, 0.5), tf(frac_sqrt_3_2, -0.5)]; + let diagonal2 = [tf(-frac_sqrt_3_2, -0.5), tf(frac_sqrt_3_2, 0.5)]; + shapes.push(Shape::line_segment(vertical, default_stroke)); + shapes.push(Shape::line_segment(diagonal1, default_stroke)); + shapes.push(Shape::line_segment(diagonal2, default_stroke)); } } } diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 9aabacc98ce..b16fc65a0e1 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -553,10 +553,10 @@ impl Prepared { .collect(); let marker_shapes: Option> = marker.map(|marker| { - values_tf - .iter() - .flat_map(|pos| marker.get_shapes(pos)) - .collect() + values_tf.iter().fold(Vec::new(), |mut shapes, pos| { + marker.get_shapes(pos, &mut shapes); + shapes + }) }); if color.a() != 0 { From a4a37938e3997aeb2a2f41d331cc7592f441b7e8 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Fri, 21 May 2021 19:29:23 +0200 Subject: [PATCH 20/50] compare against transparent color --- egui/src/widgets/plot/items.rs | 12 +++++++----- egui/src/widgets/plot/mod.rs | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index 9994a9652ad..f84a0b6370e 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -517,10 +517,12 @@ impl Curve { /// Return the color by which the curve can be identified. pub(crate) fn get_color(&self) -> Option { - self.color.filter(|color| color.a() != 0).or_else(|| { - self.marker - .map(|marker| marker.color) - .filter(|color| *color != Color32::TRANSPARENT) - }) + self.color + .filter(|color| color != &Color32::TRANSPARENT) + .or_else(|| { + self.marker + .map(|marker| marker.color) + .filter(|color| *color != Color32::TRANSPARENT) + }) } } diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index b16fc65a0e1..e2439d6f883 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -140,7 +140,7 @@ impl Plot { /// Can be useful e.g. to show min/max bounds or similar. /// Always fills the full width of the plot. pub fn hline(mut self, mut hline: HLine) -> Self { - if hline.stroke.color.a() == 0 { + if hline.stroke.color != Color32::TRANSPARENT { hline.stroke.color = self.auto_color(); } self.hlines.push(hline); @@ -151,7 +151,7 @@ impl Plot { /// Can be useful e.g. to show min/max bounds or similar. /// Always fills the full height of the plot. pub fn vline(mut self, mut vline: VLine) -> Self { - if vline.stroke.color.a() == 0 { + if vline.stroke.color != Color32::TRANSPARENT { vline.stroke.color = self.auto_color(); } self.vlines.push(vline); From ae00424d3f85228e197d06cfdc16572cbb7f5ede Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Mon, 24 May 2021 18:27:05 +0200 Subject: [PATCH 21/50] create new Points primitive --- egui/src/widgets/plot/items.rs | 671 +++++++++--------- egui/src/widgets/plot/mod.rs | 151 ++-- egui_demo_lib/src/apps/demo/plot_demo.rs | 60 +- egui_demo_lib/src/apps/demo/widget_gallery.rs | 6 +- 4 files changed, 425 insertions(+), 463 deletions(-) diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index f84a0b6370e..19f85941860 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -2,7 +2,7 @@ use std::ops::RangeInclusive; -use super::transform::Bounds; +use super::transform::{Bounds, ScreenTransform}; use crate::*; /// A value in the value-space of the plot. @@ -71,314 +71,25 @@ struct ExplicitGenerator { points: usize, } -// ---------------------------------------------------------------------------- - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub(crate) enum MarkerShape { - Circle, - Diamond, - Square, - Cross, - Plus, - Up, - Down, - Left, - Right, - Asterisk, -} - -#[derive(Debug, Clone, Copy)] -pub struct Marker { - pub(crate) shape: MarkerShape, - /// Color of the marker. `Color32::TRANSPARENT` means that it will be picked automatically. - pub(crate) color: Color32, - /// Whether to fill the marker. Does not apply to all types. - pub(crate) filled: bool, - /// The maximum extent of the marker from its center. - pub(crate) radius: f32, -} - -impl Default for Marker { - fn default() -> Self { - Self { - shape: MarkerShape::Circle, - color: Color32::TRANSPARENT, - filled: true, - radius: 2.0, - } - } -} - -impl Marker { - /// Get a vector containing a marker of each shape. - pub fn all() -> Vec { - vec![ - Self::circle(), - Self::diamond(), - Self::square(), - Self::cross(), - Self::plus(), - Self::up(), - Self::down(), - Self::left(), - Self::right(), - Self::asterisk(), - ] - } - - pub fn circle() -> Self { - Self { - shape: MarkerShape::Circle, - ..Default::default() - } - } - - pub fn diamond() -> Self { - Self { - shape: MarkerShape::Diamond, - ..Default::default() - } - } - - pub fn square() -> Self { - Self { - shape: MarkerShape::Square, - ..Default::default() - } - } - - pub fn cross() -> Self { - Self { - shape: MarkerShape::Cross, - ..Default::default() - } - } - - pub fn plus() -> Self { - Self { - shape: MarkerShape::Plus, - ..Default::default() - } - } - - pub fn up() -> Self { - Self { - shape: MarkerShape::Up, - ..Default::default() - } - } - - pub fn down() -> Self { - Self { - shape: MarkerShape::Down, - ..Default::default() - } - } - - pub fn left() -> Self { - Self { - shape: MarkerShape::Left, - ..Default::default() - } - } - - pub fn right() -> Self { - Self { - shape: MarkerShape::Right, - ..Default::default() - } - } - - pub fn asterisk() -> Self { - Self { - shape: MarkerShape::Asterisk, - ..Default::default() - } - } - - /// Set the marker's color. Defaults to the curve's color. - pub fn color(mut self, color: Color32) -> Self { - self.color = color; - self - } - - /// Whether to fill the marker. - pub fn filled(mut self, filled: bool) -> Self { - self.filled = filled; - self - } - - /// Set the maximum extent of the marker around its position. - pub fn radius(mut self, radius: f32) -> Self { - self.radius = radius; - self - } - - pub(crate) fn get_shapes(&self, position: &Pos2, shapes: &mut Vec) { - let sqrt_3 = 3f32.sqrt(); - let frac_sqrt_3_2 = 3f32.sqrt() / 2.0; - let frac_1_sqrt_2 = 1.0 / 2f32.sqrt(); - - let Self { - color, - filled, - shape, - radius, - } = *self; - - if color == Color32::TRANSPARENT { - return; - } - - let stroke_size = radius / 5.0; - - let tf = |dx: f32, dy: f32| -> Pos2 { *position + radius * vec2(dx, dy) }; - - let default_stroke = Stroke::new(stroke_size, color); - let stroke = (!filled).then(|| default_stroke).unwrap_or_default(); - let fill = filled.then(|| color).unwrap_or_default(); - - match shape { - MarkerShape::Circle => { - shapes.push(Shape::Circle { - center: *position, - radius, - fill, - stroke, - }); - } - MarkerShape::Diamond => { - let points = vec![tf(1.0, 0.0), tf(0.0, -1.0), tf(-1.0, 0.0), tf(0.0, 1.0)]; - shapes.push(Shape::Path { - points, - closed: true, - fill, - stroke, - }); - } - MarkerShape::Square => { - let points = vec![ - tf(frac_1_sqrt_2, frac_1_sqrt_2), - tf(frac_1_sqrt_2, -frac_1_sqrt_2), - tf(-frac_1_sqrt_2, -frac_1_sqrt_2), - tf(-frac_1_sqrt_2, frac_1_sqrt_2), - ]; - shapes.push(Shape::Path { - points, - closed: true, - fill, - stroke, - }); - } - MarkerShape::Cross => { - let diagonal1 = [ - tf(-frac_1_sqrt_2, -frac_1_sqrt_2), - tf(frac_1_sqrt_2, frac_1_sqrt_2), - ]; - let diagonal2 = [ - tf(frac_1_sqrt_2, -frac_1_sqrt_2), - tf(-frac_1_sqrt_2, frac_1_sqrt_2), - ]; - shapes.push(Shape::line_segment(diagonal1, default_stroke)); - shapes.push(Shape::line_segment(diagonal2, default_stroke)); - } - MarkerShape::Plus => { - let horizontal = [tf(-1.0, 0.0), tf(1.0, 0.0)]; - let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)]; - shapes.push(Shape::line_segment(horizontal, default_stroke)); - shapes.push(Shape::line_segment(vertical, default_stroke)); - } - MarkerShape::Up => { - let points = vec![tf(0.0, -1.0), tf(-0.5 * sqrt_3, 0.5), tf(0.5 * sqrt_3, 0.5)]; - shapes.push(Shape::Path { - points, - closed: true, - fill, - stroke, - }); - } - MarkerShape::Down => { - let points = vec![ - tf(0.0, 1.0), - tf(-0.5 * sqrt_3, -0.5), - tf(0.5 * sqrt_3, -0.5), - ]; - shapes.push(Shape::Path { - points, - closed: true, - fill, - stroke, - }); - } - MarkerShape::Left => { - let points = vec![tf(-1.0, 0.0), tf(0.5, -0.5 * sqrt_3), tf(0.5, 0.5 * sqrt_3)]; - shapes.push(Shape::Path { - points, - closed: true, - fill, - stroke, - }); - } - MarkerShape::Right => { - let points = vec![ - tf(1.0, 0.0), - tf(-0.5, -0.5 * sqrt_3), - tf(-0.5, 0.5 * sqrt_3), - ]; - shapes.push(Shape::Path { - points, - closed: true, - fill, - stroke, - }); - } - MarkerShape::Asterisk => { - let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)]; - let diagonal1 = [tf(-frac_sqrt_3_2, 0.5), tf(frac_sqrt_3_2, -0.5)]; - let diagonal2 = [tf(-frac_sqrt_3_2, -0.5), tf(frac_sqrt_3_2, 0.5)]; - shapes.push(Shape::line_segment(vertical, default_stroke)); - shapes.push(Shape::line_segment(diagonal1, default_stroke)); - shapes.push(Shape::line_segment(diagonal2, default_stroke)); - } - } - } -} - -/// A series of values forming a path. -pub struct Curve { +pub struct ValueSeries { pub(crate) values: Vec, generator: Option, - pub(crate) bounds: Bounds, - pub(crate) marker: Option, - pub(crate) color: Option, - pub(crate) width: f32, - pub(crate) name: String, - pub(crate) highlight: bool, } -impl Curve { - fn empty() -> Self { +impl Default for ValueSeries { + fn default() -> Self { Self { values: Vec::new(), generator: None, - bounds: Bounds::NOTHING, - marker: None, - color: None, - width: 1.0, - name: Default::default(), - highlight: false, } } +} +impl ValueSeries { pub fn from_values(values: Vec) -> Self { - let mut bounds = Bounds::NOTHING; - for value in &values { - bounds.extend_with(value); - } Self { values, - bounds, - ..Self::empty() + generator: None, } } @@ -392,12 +103,6 @@ impl Curve { x_range: RangeInclusive, points: usize, ) -> Self { - let mut bounds = Bounds::NOTHING; - if x_range.start().is_finite() && x_range.end().is_finite() { - bounds.min[0] = *x_range.start(); - bounds.max[0] = *x_range.end(); - } - let generator = ExplicitGenerator { function: Box::new(function), x_range, @@ -405,9 +110,8 @@ impl Curve { }; Self { + values: Vec::new(), generator: Some(generator), - bounds, - ..Self::empty() } } @@ -426,19 +130,23 @@ impl Curve { Self::from_values_iter(values) } - /// Returns true if there are no data points available and there is no function to generate any. - pub(crate) fn no_data(&self) -> bool { - self.generator.is_none() && self.values.is_empty() + /// From a series of y-values. + /// The x-values will be the indices of these values + pub fn from_ys_f32(ys: &[f32]) -> Self { + let values: Vec = ys + .iter() + .enumerate() + .map(|(i, &y)| Value { + x: i as f64, + y: y as f64, + }) + .collect(); + Self::from_values(values) } - /// Returns the intersection of two ranges if they intersect. - fn range_intersection( - range1: &RangeInclusive, - range2: &RangeInclusive, - ) -> Option> { - let start = range1.start().max(*range2.start()); - let end = range1.end().min(*range2.end()); - (start < end).then(|| start..=end) + /// Returns true if there are no data points available and there is no function to generate any. + pub(crate) fn is_empty(&self) -> bool { + self.generator.is_none() && self.values.is_empty() } /// If initialized with a generator function, this will generate `n` evenly spaced points in the @@ -459,18 +167,75 @@ impl Curve { } } - /// From a series of y-values. - /// The x-values will be the indices of these values - pub fn from_ys_f32(ys: &[f32]) -> Self { - let values: Vec = ys + /// Returns the intersection of two ranges if they intersect. + fn range_intersection( + range1: &RangeInclusive, + range2: &RangeInclusive, + ) -> Option> { + let start = range1.start().max(*range2.start()); + let end = range1.end().min(*range2.end()); + (start < end).then(|| start..=end) + } + + pub(crate) fn get_bounds(&self) -> Bounds { + let mut bounds = Bounds::NOTHING; + self.values .iter() - .enumerate() - .map(|(i, &y)| Value { - x: i as f64, - y: y as f64, - }) - .collect(); - Self::from_values(values) + .for_each(|value| bounds.extend_with(value)); + bounds + } +} + +// ---------------------------------------------------------------------------- + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum MarkerShape { + Circle, + Diamond, + Square, + Cross, + Plus, + Up, + Down, + Left, + Right, + Asterisk, +} + +impl MarkerShape { + /// Get a vector containing all marker shapes. + pub fn all() -> Vec { + vec![ + Self::Circle, + Self::Diamond, + Self::Square, + Self::Cross, + Self::Plus, + Self::Up, + Self::Down, + Self::Left, + Self::Right, + Self::Asterisk, + ] + } +} + +/// A series of values forming a path. +pub struct Curve { + pub(crate) series: ValueSeries, + pub(crate) stroke: Stroke, + pub(crate) name: String, + pub(crate) highlight: bool, +} + +impl Curve { + pub fn new(series: ValueSeries) -> Self { + Self { + series, + stroke: Stroke::new(1.0, Color32::TRANSPARENT), + name: Default::default(), + highlight: false, + } } /// Highlight this curve in the plot by scaling up the line and marker size. @@ -481,27 +246,19 @@ impl Curve { /// Add a stroke. pub fn stroke(mut self, stroke: impl Into) -> Self { - let stroke: Stroke = stroke.into(); - self.color = Some(stroke.color); - self.width = stroke.width; - self - } - - /// Add a marker for all data points. - pub fn marker(mut self, marker: Marker) -> Self { - self.marker = Some(marker); + self.stroke = stroke.into(); self } /// Stroke width. A high value means the plot thickens. pub fn width(mut self, width: f32) -> Self { - self.width = width; + self.stroke.width = width; self } /// Stroke color. pub fn color(mut self, color: impl Into) -> Self { - self.color = Some(color.into()); + self.stroke.color = color.into(); self } @@ -515,14 +272,236 @@ impl Curve { self } - /// Return the color by which the curve can be identified. - pub(crate) fn get_color(&self) -> Option { - self.color - .filter(|color| color != &Color32::TRANSPARENT) - .or_else(|| { - self.marker - .map(|marker| marker.color) - .filter(|color| *color != Color32::TRANSPARENT) - }) + pub(crate) fn into_shapes(self, transform: &ScreenTransform, shapes: &mut Vec) { + let Self { + series: data, + mut stroke, + highlight, + .. + } = self; + + if highlight { + stroke.width *= 1.5; + } + + let values_tf: Vec<_> = data + .values + .iter() + .map(|v| transform.position_from_value(v)) + .collect(); + + let line_shape = if values_tf.len() > 1 { + Shape::line(values_tf, stroke) + } else { + Shape::circle_filled(values_tf[0], stroke.width / 2.0, stroke.color) + }; + shapes.push(line_shape); + } +} + +/// A series of values forming a path. +pub struct Points { + pub(crate) series: ValueSeries, + pub(crate) shape: MarkerShape, + /// Color of the marker. `Color32::TRANSPARENT` means that it will be picked automatically. + pub(crate) color: Color32, + /// Whether to fill the marker. Does not apply to all types. + pub(crate) filled: bool, + /// The maximum extent of the marker from its center. + pub(crate) radius: f32, + pub(crate) name: String, + pub(crate) highlight: bool, +} + +impl Points { + pub fn new(series: ValueSeries) -> Self { + Self { + series, + shape: MarkerShape::Circle, + color: Color32::TRANSPARENT, + filled: true, + radius: 1.0, + name: Default::default(), + highlight: false, + } + } + + pub fn shape(mut self, shape: MarkerShape) -> Self { + self.shape = shape; + self + } + + /// Highlight these points in the plot by scaling up the marker size. + pub fn highlight(mut self) -> Self { + self.highlight = true; + self + } + + /// Set the marker's color. Defaults to the curve's color. + pub fn color(mut self, color: Color32) -> Self { + self.color = color; + self + } + + /// Whether to fill the marker. + pub fn filled(mut self, filled: bool) -> Self { + self.filled = filled; + self + } + + /// Set the maximum extent of the marker around its position. + pub fn radius(mut self, radius: f32) -> Self { + self.radius = radius; + self + } + + /// Name of this curve. + /// + /// If a curve is given a name it will show up in the plot legend + /// (if legends are turned on). + #[allow(clippy::needless_pass_by_value)] + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self + } + + pub(crate) fn into_shapes(self, transform: &ScreenTransform, shapes: &mut Vec) { + let sqrt_3 = 3f32.sqrt(); + let frac_sqrt_3_2 = 3f32.sqrt() / 2.0; + let frac_1_sqrt_2 = 1.0 / 2f32.sqrt(); + + let Self { + series, + shape, + color, + filled, + mut radius, + highlight, + .. + } = self; + + if highlight { + radius *= 2f32.sqrt(); + } + + let stroke_size = radius / 5.0; + + let default_stroke = Stroke::new(stroke_size, color); + let stroke = (!filled).then(|| default_stroke).unwrap_or_default(); + let fill = filled.then(|| color).unwrap_or_default(); + + series + .values + .iter() + .map(|v| transform.position_from_value(v)) + .for_each(|center| { + let tf = |dx: f32, dy: f32| -> Pos2 { center + radius * vec2(dx, dy) }; + + match shape { + MarkerShape::Circle => { + shapes.push(Shape::Circle { + center, + radius, + fill, + stroke, + }); + } + MarkerShape::Diamond => { + let points = vec![tf(1.0, 0.0), tf(0.0, -1.0), tf(-1.0, 0.0), tf(0.0, 1.0)]; + shapes.push(Shape::Path { + points, + closed: true, + fill, + stroke, + }); + } + MarkerShape::Square => { + let points = vec![ + tf(frac_1_sqrt_2, frac_1_sqrt_2), + tf(frac_1_sqrt_2, -frac_1_sqrt_2), + tf(-frac_1_sqrt_2, -frac_1_sqrt_2), + tf(-frac_1_sqrt_2, frac_1_sqrt_2), + ]; + shapes.push(Shape::Path { + points, + closed: true, + fill, + stroke, + }); + } + MarkerShape::Cross => { + let diagonal1 = [ + tf(-frac_1_sqrt_2, -frac_1_sqrt_2), + tf(frac_1_sqrt_2, frac_1_sqrt_2), + ]; + let diagonal2 = [ + tf(frac_1_sqrt_2, -frac_1_sqrt_2), + tf(-frac_1_sqrt_2, frac_1_sqrt_2), + ]; + shapes.push(Shape::line_segment(diagonal1, default_stroke)); + shapes.push(Shape::line_segment(diagonal2, default_stroke)); + } + MarkerShape::Plus => { + let horizontal = [tf(-1.0, 0.0), tf(1.0, 0.0)]; + let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)]; + shapes.push(Shape::line_segment(horizontal, default_stroke)); + shapes.push(Shape::line_segment(vertical, default_stroke)); + } + MarkerShape::Up => { + let points = + vec![tf(0.0, -1.0), tf(-0.5 * sqrt_3, 0.5), tf(0.5 * sqrt_3, 0.5)]; + shapes.push(Shape::Path { + points, + closed: true, + fill, + stroke, + }); + } + MarkerShape::Down => { + let points = vec![ + tf(0.0, 1.0), + tf(-0.5 * sqrt_3, -0.5), + tf(0.5 * sqrt_3, -0.5), + ]; + shapes.push(Shape::Path { + points, + closed: true, + fill, + stroke, + }); + } + MarkerShape::Left => { + let points = + vec![tf(-1.0, 0.0), tf(0.5, -0.5 * sqrt_3), tf(0.5, 0.5 * sqrt_3)]; + shapes.push(Shape::Path { + points, + closed: true, + fill, + stroke, + }); + } + MarkerShape::Right => { + let points = vec![ + tf(1.0, 0.0), + tf(-0.5, -0.5 * sqrt_3), + tf(-0.5, 0.5 * sqrt_3), + ]; + shapes.push(Shape::Path { + points, + closed: true, + fill, + stroke, + }); + } + MarkerShape::Asterisk => { + let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)]; + let diagonal1 = [tf(-frac_sqrt_3_2, 0.5), tf(frac_sqrt_3_2, -0.5)]; + let diagonal2 = [tf(-frac_sqrt_3_2, -0.5), tf(frac_sqrt_3_2, 0.5)]; + shapes.push(Shape::line_segment(vertical, default_stroke)); + shapes.push(Shape::line_segment(diagonal1, default_stroke)); + shapes.push(Shape::line_segment(diagonal2, default_stroke)); + } + } + }); } } diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 9686d938129..4ba8e61571c 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -6,7 +6,7 @@ mod transform; use std::collections::{BTreeMap, HashSet}; -pub use items::{Curve, Marker, Value}; +pub use items::{Curve, MarkerShape, Points, Value, ValueSeries}; pub use items::{HLine, VLine}; use legend::LegendEntry; use transform::{Bounds, ScreenTransform}; @@ -22,7 +22,7 @@ use color::Hsva; struct PlotMemory { bounds: Bounds, auto_bounds: bool, - hidden_curves: HashSet, + hidden_items: HashSet, } // ---------------------------------------------------------------------------- @@ -48,6 +48,7 @@ pub struct Plot { next_auto_color_idx: usize, curves: Vec, + points: Vec, hlines: Vec, vlines: Vec, @@ -77,6 +78,7 @@ impl Plot { next_auto_color_idx: 0, curves: Default::default(), + points: Default::default(), hlines: Default::default(), vlines: Default::default(), @@ -110,30 +112,32 @@ impl Plot { /// Add a data curve. /// You can add multiple curves. pub fn curve(mut self, mut curve: Curve) -> Self { - if curve.no_data() { + if curve.series.is_empty() { return self; }; // Give the stroke an automatic color if no color has been assigned. - let stroke_color = curve.color.get_or_insert_with(|| self.auto_color()); - // Give the marker the same color as the stroke if no color has been assigned. - if let Some(marker) = &mut curve.marker { - if marker.color == Color32::TRANSPARENT { - marker.color = if *stroke_color != Color32::TRANSPARENT { - *stroke_color - } else { - self.auto_color() - }; - } + if curve.stroke.color == Color32::TRANSPARENT { + curve.stroke.color = self.auto_color(); } self.curves.push(curve); self } - /// Add multiple data curves. - pub fn curves(self, curves: Vec) -> Self { - curves.into_iter().fold(self, Self::curve) + /// Add data points. + pub fn points(mut self, mut points: Points) -> Self { + if points.series.is_empty() { + return self; + }; + + // Give the points an automatic color if no color has been assigned. + if points.color == Color32::TRANSPARENT { + points.color = self.auto_color(); + } + self.points.push(points); + + self } /// Add a horizontal line. @@ -271,6 +275,7 @@ impl Widget for Plot { name, next_auto_color_idx: _, mut curves, + mut points, hlines, vlines, center_x_axis, @@ -296,14 +301,14 @@ impl Widget for Plot { .get_mut_or_insert_with(plot_id, || PlotMemory { bounds: min_auto_bounds, auto_bounds: !min_auto_bounds.is_valid(), - hidden_curves: HashSet::new(), + hidden_items: HashSet::new(), }) .clone(); let PlotMemory { mut bounds, mut auto_bounds, - mut hidden_curves, + mut hidden_items, } = memory; // Determine the size of the plot in the UI @@ -352,19 +357,31 @@ impl Widget for Plot { .iter() .filter(|curve| !curve.name.is_empty()) .for_each(|curve| { - let checked = !hidden_curves.contains(&curve.name); + let checked = !hidden_items.contains(&curve.name); let text = curve.name.clone(); legend_entries .entry(curve.name.clone()) .and_modify(|entry| { - if Some(entry.color) != curve.get_color() { + if entry.color != curve.stroke.color { + entry.color = neutral_color + } + }) + .or_insert_with(|| LegendEntry::new(text, curve.stroke.color, checked)); + }); + points + .iter() + .filter(|points| !points.name.is_empty()) + .for_each(|points| { + let checked = !hidden_items.contains(&points.name); + let text = points.name.clone(); + legend_entries + .entry(points.name.clone()) + .and_modify(|entry| { + if entry.color != points.color { entry.color = neutral_color } }) - .or_insert_with(|| { - let color = curve.get_color().unwrap_or(neutral_color); - LegendEntry::new(text, color, checked) - }); + .or_insert_with(|| LegendEntry::new(text, points.color, checked)); }); // Show the legend. @@ -377,14 +394,14 @@ impl Widget for Plot { } }); - // Get the names of the hidden curves. - hidden_curves = legend_entries + // Get the names of the hidden items. + hidden_items = legend_entries .values() .filter(|entry| !entry.checked) .map(|entry| entry.text.clone()) .collect(); - // Highlight the hovered curves. + // Highlight the hovered items. legend_entries .values() .filter(|entry| entry.hovered) @@ -392,10 +409,14 @@ impl Widget for Plot { curves.iter_mut().for_each(|curve| { curve.highlight |= curve.name == entry.text; }); + points.iter_mut().for_each(|points| { + points.highlight |= points.name == entry.text; + }); }); - // Remove deselected curves. - curves.retain(|curve| !hidden_curves.contains(&curve.name)); + // Remove deselected items. + curves.retain(|curve| !hidden_items.contains(&curve.name)); + points.retain(|points| !hidden_items.contains(&points.name)); } // --- @@ -407,7 +428,12 @@ impl Widget for Plot { bounds = min_auto_bounds; hlines.iter().for_each(|line| bounds.extend_with_y(line.y)); vlines.iter().for_each(|line| bounds.extend_with_x(line.x)); - curves.iter().for_each(|curve| bounds.merge(&curve.bounds)); + curves + .iter() + .for_each(|curve| bounds.merge(&curve.series.get_bounds())); + points + .iter() + .for_each(|points| bounds.merge(&points.series.get_bounds())); bounds.add_relative_margin(margin_fraction); } // Make sure they are not empty. @@ -460,12 +486,16 @@ impl Widget for Plot { // Initialize values from functions. curves .iter_mut() - .for_each(|curve| curve.generate_points(transform.bounds().range_x())); + .for_each(|curve| curve.series.generate_points(transform.bounds().range_x())); + points + .iter_mut() + .for_each(|points| points.series.generate_points(transform.bounds().range_x())); let bounds = *transform.bounds(); let prepared = Prepared { curves, + points, hlines, vlines, show_x, @@ -479,7 +509,7 @@ impl Widget for Plot { PlotMemory { bounds, auto_bounds, - hidden_curves, + hidden_items, }, ); @@ -493,6 +523,7 @@ impl Widget for Plot { struct Prepared { curves: Vec, + points: Vec, hlines: Vec, vlines: Vec, show_x: bool, @@ -501,15 +532,15 @@ struct Prepared { } impl Prepared { - fn ui(&self, ui: &mut Ui, response: &Response) { - let Self { transform, .. } = self; - + fn ui(mut self, ui: &mut Ui, response: &Response) { let mut shapes = Vec::new(); for d in 0..2 { self.paint_axis(ui, d, &mut shapes); } + let transform = &self.transform; + for &hline in &self.hlines { let HLine { y, stroke } = hline; let points = [ @@ -528,49 +559,11 @@ impl Prepared { shapes.push(Shape::line_segment(points, stroke)); } - for curve in &self.curves { - let Curve { - values, - color, - mut width, - mut marker, - highlight, - .. - } = curve; - - let color = color.unwrap_or(Color32::TRANSPARENT); - - if *highlight { - width *= 1.5; - if let Some(marker) = &mut marker { - marker.radius *= 1.25; - } - } - - let values_tf: Vec<_> = values - .iter() - .map(|v| transform.position_from_value(v)) - .collect(); - - let marker_shapes: Option> = marker.map(|marker| { - values_tf.iter().fold(Vec::new(), |mut shapes, pos| { - marker.get_shapes(pos, &mut shapes); - shapes - }) - }); - - if color.a() != 0 { - let line_shape = if values_tf.len() > 1 { - Shape::line(values_tf, Stroke::new(width, color)) - } else { - Shape::circle_filled(values_tf[0], width / 2.0, color) - }; - shapes.push(line_shape); - } - - if let Some(marker_shapes) = marker_shapes { - shapes.extend(marker_shapes.into_iter()); - } + for curve in self.curves.drain(..) { + curve.into_shapes(transform, &mut shapes); + } + for points in self.points.drain(..) { + points.into_shapes(transform, &mut shapes); } if let Some(pointer) = response.hover_pos() { @@ -687,7 +680,7 @@ impl Prepared { let mut closest_curve = None; let mut closest_dist_sq = interact_radius.powi(2); for curve in curves { - for value in &curve.values { + for value in &curve.series.values { let pos = transform.position_from_value(value); let dist_sq = pointer.distance_sq(pos); if dist_sq < closest_dist_sq { diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index a6db9450706..51c038cb365 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -1,4 +1,4 @@ -use egui::plot::{Curve, Marker, Plot, Value}; +use egui::plot::{Curve, MarkerShape, Plot, Points, Value, ValueSeries}; use egui::*; use std::f64::consts::TAU; @@ -96,31 +96,30 @@ impl CurveDemo { r * t.sin() + self.circle_center.y as f64, ) }); - Curve::from_values_iter(circle) + Curve::new(ValueSeries::from_values_iter(circle)) .color(Color32::from_rgb(100, 200, 100)) .name("circle") } fn sin(&self) -> Curve { let time = self.time; - Curve::from_explicit_callback( + Curve::new(ValueSeries::from_explicit_callback( move |x| 0.5 * (2.0 * x).sin() * time.sin(), f64::NEG_INFINITY..=f64::INFINITY, 512, - ) + )) .color(Color32::from_rgb(200, 100, 100)) .name("wave") } fn thingy(&self) -> Curve { let time = self.time; - Curve::from_parametric_callback( + Curve::new(ValueSeries::from_parametric_callback( move |t| ((2.0 * t + time).sin(), (3.0 * t).sin()), 0.0..=TAU, - 100, - ) + 256, + )) .color(Color32::from_rgb(100, 150, 250)) - .marker(Marker::default()) .name("x = sin(2t), y = sin(3t)") } } @@ -150,8 +149,6 @@ impl Widget for &mut CurveDemo { #[derive(PartialEq)] struct MarkerDemo { - show_markers: bool, - show_lines: bool, fill_markers: bool, marker_radius: f32, custom_marker_color: bool, @@ -161,8 +158,6 @@ struct MarkerDemo { impl Default for MarkerDemo { fn default() -> Self { Self { - show_markers: true, - show_lines: true, fill_markers: true, marker_radius: 5.0, custom_marker_color: false, @@ -172,33 +167,30 @@ impl Default for MarkerDemo { } impl MarkerDemo { - fn markers(&self) -> Vec { - Marker::all() + fn markers(&self) -> Vec { + MarkerShape::all() .into_iter() .enumerate() .map(|(i, marker)| { let y_offset = i as f32 * 0.5 + 1.0; - let mut curve = Curve::from_values(vec![ + let mut points = Points::new(ValueSeries::from_values(vec![ Value::new(1.0, 0.0 + y_offset), Value::new(2.0, 0.5 + y_offset), Value::new(3.0, 0.0 + y_offset), Value::new(4.0, 0.5 + y_offset), Value::new(5.0, 0.0 + y_offset), Value::new(6.0, 0.5 + y_offset), - ]) - .name("Marker Lines"); - - if self.show_markers { - let mut marker = marker.filled(self.fill_markers).radius(self.marker_radius); - if self.custom_marker_color { - marker = marker.color(self.marker_color); - } - curve = curve.marker(marker); - } - if !self.show_lines { - curve = curve.color(Color32::TRANSPARENT); + ])) + .name("Marker Lines") + .filled(self.fill_markers) + .radius(self.marker_radius) + .shape(marker); + + if self.custom_marker_color { + points = points.color(self.marker_color); } - curve + + points }) .collect() } @@ -206,10 +198,6 @@ impl MarkerDemo { impl Widget for &mut MarkerDemo { fn ui(self, ui: &mut Ui) -> Response { - ui.horizontal(|ui| { - ui.checkbox(&mut self.show_lines, "show lines"); - ui.checkbox(&mut self.show_markers, "show markers"); - }); ui.horizontal(|ui| { ui.checkbox(&mut self.fill_markers, "fill markers"); ui.add( @@ -224,10 +212,10 @@ impl Widget for &mut MarkerDemo { } }); - let markers_plot = Plot::new("Markers Demo") - .curves(self.markers()) - .height(300.0) - .data_aspect(1.0); + let mut markers_plot = Plot::new("Markers Demo").height(300.0).data_aspect(1.0); + for marker in self.markers() { + markers_plot = markers_plot.points(marker); + } ui.add(markers_plot) } } diff --git a/egui_demo_lib/src/apps/demo/widget_gallery.rs b/egui_demo_lib/src/apps/demo/widget_gallery.rs index d8ff190a105..4e333ae3479 100644 --- a/egui_demo_lib/src/apps/demo/widget_gallery.rs +++ b/egui_demo_lib/src/apps/demo/widget_gallery.rs @@ -1,3 +1,5 @@ +use egui::plot::ValueSeries; + #[derive(Debug, PartialEq)] #[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] enum Enum { @@ -206,11 +208,11 @@ impl WidgetGallery { fn example_plot() -> egui::plot::Plot { let n = 128; - let curve = egui::plot::Curve::from_values_iter((0..=n).map(|i| { + let curve = egui::plot::Curve::new(ValueSeries::from_values_iter((0..=n).map(|i| { use std::f64::consts::TAU; let x = egui::remap(i as f64, 0.0..=(n as f64), -TAU..=TAU); egui::plot::Value::new(x, x.sin()) - })); + }))); egui::plot::Plot::new("Example Plot") .curve(curve) .height(32.0) From 773be0565af629a6cc072ffbb5981210452eff0e Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Mon, 24 May 2021 18:36:44 +0200 Subject: [PATCH 22/50] fix doctest --- egui/src/widgets/plot/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 4ba8e61571c..8c346c9daf8 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -33,12 +33,12 @@ struct PlotMemory { /// /// ``` /// # let ui = &mut egui::Ui::__test(); -/// use egui::plot::{Curve, Plot, Value}; +/// use egui::plot::{Curve, Plot, Value, ValueSeries}; /// let sin = (0..1000).map(|i| { /// let x = i as f64 * 0.01; /// Value::new(x, x.sin()) /// }); -/// let curve = Curve::from_values_iter(sin); +/// let curve = Curve::new(ValueSeries::from_values_iter(sin)); /// ui.add( /// Plot::new("Test Plot").curve(curve).view_aspect(2.0) /// ); From 68313027cdfd94fc87431454955576a0ce73e4c8 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Mon, 24 May 2021 19:01:42 +0200 Subject: [PATCH 23/50] some cleanup and fix hover --- egui/src/widgets/plot/items.rs | 29 ++++++++++++++-------------- egui/src/widgets/plot/mod.rs | 35 +++++++++++++++++++++++----------- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index 19f85941860..b48ad409cfd 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -264,15 +264,14 @@ impl Curve { /// Name of this curve. /// - /// If a curve is given a name it will show up in the plot legend - /// (if legends are turned on). + /// This name will show up in the plot legend, if legends are turned on. #[allow(clippy::needless_pass_by_value)] pub fn name(mut self, name: impl ToString) -> Self { self.name = name.to_string(); self } - pub(crate) fn into_shapes(self, transform: &ScreenTransform, shapes: &mut Vec) { + pub(crate) fn get_shapes(&self, transform: &ScreenTransform, shapes: &mut Vec) { let Self { series: data, mut stroke, @@ -280,7 +279,7 @@ impl Curve { .. } = self; - if highlight { + if *highlight { stroke.width *= 1.5; } @@ -299,7 +298,7 @@ impl Curve { } } -/// A series of values forming a path. +/// A series of points. pub struct Points { pub(crate) series: ValueSeries, pub(crate) shape: MarkerShape, @@ -326,18 +325,19 @@ impl Points { } } + /// Set the shape of the markers. pub fn shape(mut self, shape: MarkerShape) -> Self { self.shape = shape; self } - /// Highlight these points in the plot by scaling up the marker size. + /// Highlight these points in the plot by scaling up their markers. pub fn highlight(mut self) -> Self { self.highlight = true; self } - /// Set the marker's color. Defaults to the curve's color. + /// Set the marker's color. pub fn color(mut self, color: Color32) -> Self { self.color = color; self @@ -355,17 +355,16 @@ impl Points { self } - /// Name of this curve. + /// Name of this series of markers. /// - /// If a curve is given a name it will show up in the plot legend - /// (if legends are turned on). + /// This name will show up in the plot legend, if legends are turned on. #[allow(clippy::needless_pass_by_value)] pub fn name(mut self, name: impl ToString) -> Self { self.name = name.to_string(); self } - pub(crate) fn into_shapes(self, transform: &ScreenTransform, shapes: &mut Vec) { + pub(crate) fn get_shapes(&self, transform: &ScreenTransform, shapes: &mut Vec) { let sqrt_3 = 3f32.sqrt(); let frac_sqrt_3_2 = 3f32.sqrt() / 2.0; let frac_1_sqrt_2 = 1.0 / 2f32.sqrt(); @@ -380,20 +379,20 @@ impl Points { .. } = self; - if highlight { + if *highlight { radius *= 2f32.sqrt(); } let stroke_size = radius / 5.0; - let default_stroke = Stroke::new(stroke_size, color); + let default_stroke = Stroke::new(stroke_size, *color); let stroke = (!filled).then(|| default_stroke).unwrap_or_default(); - let fill = filled.then(|| color).unwrap_or_default(); + let fill = filled.then(|| *color).unwrap_or_default(); series .values .iter() - .map(|v| transform.position_from_value(v)) + .map(|value| transform.position_from_value(value)) .for_each(|center| { let tf = |dx: f32, dy: f32| -> Pos2 { center + radius * vec2(dx, dy) }; diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 8c346c9daf8..5f204e86d06 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -126,6 +126,7 @@ impl Plot { } /// Add data points. + /// You can add multiple sets of points. pub fn points(mut self, mut points: Points) -> Self { if points.series.is_empty() { return self; @@ -262,7 +263,7 @@ impl Plot { self } - /// Whether to show a legend including all named curves. Default: `true`. + /// Whether to show a legend including all named items. Default: `true`. pub fn show_legend(mut self, show: bool) -> Self { self.show_legend = show; self @@ -532,7 +533,7 @@ struct Prepared { } impl Prepared { - fn ui(mut self, ui: &mut Ui, response: &Response) { + fn ui(self, ui: &mut Ui, response: &Response) { let mut shapes = Vec::new(); for d in 0..2 { @@ -559,11 +560,11 @@ impl Prepared { shapes.push(Shape::line_segment(points, stroke)); } - for curve in self.curves.drain(..) { - curve.into_shapes(transform, &mut shapes); + for curve in &self.curves { + curve.get_shapes(transform, &mut shapes); } - for points in self.points.drain(..) { - points.into_shapes(transform, &mut shapes); + for points in &self.points { + points.get_shapes(transform, &mut shapes); } if let Some(pointer) = response.hover_pos() { @@ -668,6 +669,7 @@ impl Prepared { show_x, show_y, curves, + points, .. } = self; @@ -677,7 +679,7 @@ impl Prepared { let interact_radius: f32 = 16.0; let mut closest_value = None; - let mut closest_curve = None; + let mut closest_item = None; let mut closest_dist_sq = interact_radius.powi(2); for curve in curves { for value in &curve.series.values { @@ -686,15 +688,26 @@ impl Prepared { if dist_sq < closest_dist_sq { closest_dist_sq = dist_sq; closest_value = Some(value); - closest_curve = Some(curve); + closest_item = Some(curve.name.clone()); + } + } + } + for points in points { + for value in &points.series.values { + let pos = transform.position_from_value(value); + let dist_sq = pointer.distance_sq(pos); + if dist_sq < closest_dist_sq { + closest_dist_sq = dist_sq; + closest_value = Some(value); + closest_item = Some(points.name.clone()); } } } let mut prefix = String::new(); - if let Some(curve) = closest_curve { - if !curve.name.is_empty() { - prefix = format!("{}\n", curve.name); + if let Some(name) = closest_item { + if !name.is_empty() { + prefix = format!("{}\n", name); } } From 4ee57fd027a9c7180133c57516f4d5552a45db64 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Mon, 24 May 2021 22:11:48 +0200 Subject: [PATCH 24/50] common interface for lines and points --- egui/src/widgets/plot/items.rs | 73 ++++++++-- egui/src/widgets/plot/mod.rs | 133 +++++++----------- egui_demo_lib/src/apps/demo/plot_demo.rs | 34 ++--- egui_demo_lib/src/apps/demo/widget_gallery.rs | 4 +- 4 files changed, 129 insertions(+), 115 deletions(-) diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index b48ad409cfd..c66ecf1a16f 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -62,6 +62,15 @@ impl VLine { } } +pub(crate) trait PlotItem { + fn get_shapes(&self, transform: &ScreenTransform, shapes: &mut Vec); + fn series(&self) -> &ValueSeries; + fn series_mut(&mut self) -> &mut ValueSeries; + fn name(&self) -> &str; + fn color(&self) -> Color32; + fn highlight(&mut self); +} + // ---------------------------------------------------------------------------- /// Describes a function y = f(x) with an optional range for x and a number of points. @@ -97,7 +106,7 @@ impl ValueSeries { Self::from_values(iter.collect()) } - /// Draw a curve based on a function `y=f(x)`, a range (which can be infinite) for x and the number of points. + /// Draw a line based on a function `y=f(x)`, a range (which can be infinite) for x and the number of points. pub fn from_explicit_callback( function: impl Fn(f64) -> f64 + 'static, x_range: RangeInclusive, @@ -115,7 +124,7 @@ impl ValueSeries { } } - /// Draw a curve based on a function `(x,y)=f(t)`, a range for t and the number of points. + /// Draw a line based on a function `(x,y)=f(t)`, a range for t and the number of points. pub fn from_parametric_callback( function: impl Fn(f64) -> (f64, f64), t_range: RangeInclusive, @@ -221,14 +230,14 @@ impl MarkerShape { } /// A series of values forming a path. -pub struct Curve { +pub struct Line { pub(crate) series: ValueSeries, pub(crate) stroke: Stroke, pub(crate) name: String, pub(crate) highlight: bool, } -impl Curve { +impl Line { pub fn new(series: ValueSeries) -> Self { Self { series, @@ -238,7 +247,7 @@ impl Curve { } } - /// Highlight this curve in the plot by scaling up the line and marker size. + /// Highlight this line in the plot by scaling up the line and marker size. pub fn highlight(mut self) -> Self { self.highlight = true; self @@ -262,7 +271,7 @@ impl Curve { self } - /// Name of this curve. + /// Name of this line. /// /// This name will show up in the plot legend, if legends are turned on. #[allow(clippy::needless_pass_by_value)] @@ -270,10 +279,12 @@ impl Curve { self.name = name.to_string(); self } +} - pub(crate) fn get_shapes(&self, transform: &ScreenTransform, shapes: &mut Vec) { +impl PlotItem for Line { + fn get_shapes(&self, transform: &ScreenTransform, shapes: &mut Vec) { let Self { - series: data, + series, mut stroke, highlight, .. @@ -283,7 +294,7 @@ impl Curve { stroke.width *= 1.5; } - let values_tf: Vec<_> = data + let values_tf: Vec<_> = series .values .iter() .map(|v| transform.position_from_value(v)) @@ -296,6 +307,26 @@ impl Curve { }; shapes.push(line_shape); } + + fn series(&self) -> &ValueSeries { + &self.series + } + + fn series_mut(&mut self) -> &mut ValueSeries { + &mut self.series + } + + fn name(&self) -> &str { + self.name.as_str() + } + + fn color(&self) -> Color32 { + self.stroke.color + } + + fn highlight(&mut self) { + self.highlight = true; + } } /// A series of points. @@ -363,8 +394,10 @@ impl Points { self.name = name.to_string(); self } +} - pub(crate) fn get_shapes(&self, transform: &ScreenTransform, shapes: &mut Vec) { +impl PlotItem for Points { + fn get_shapes(&self, transform: &ScreenTransform, shapes: &mut Vec) { let sqrt_3 = 3f32.sqrt(); let frac_sqrt_3_2 = 3f32.sqrt() / 2.0; let frac_1_sqrt_2 = 1.0 / 2f32.sqrt(); @@ -503,4 +536,24 @@ impl Points { } }); } + + fn series(&self) -> &ValueSeries { + &self.series + } + + fn series_mut(&mut self) -> &mut ValueSeries { + &mut self.series + } + + fn name(&self) -> &str { + self.name.as_str() + } + + fn color(&self) -> Color32 { + self.color.clone() + } + + fn highlight(&mut self) { + self.highlight = true; + } } diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 5f204e86d06..12cf7869586 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -6,8 +6,9 @@ mod transform; use std::collections::{BTreeMap, HashSet}; -pub use items::{Curve, MarkerShape, Points, Value, ValueSeries}; +use items::PlotItem; pub use items::{HLine, VLine}; +pub use items::{Line, MarkerShape, Points, Value, ValueSeries}; use legend::LegendEntry; use transform::{Bounds, ScreenTransform}; @@ -29,26 +30,25 @@ struct PlotMemory { /// A 2D plot, e.g. a graph of a function. /// -/// `Plot` supports multiple curves. +/// `Plot` supports multiple lines and points. /// /// ``` /// # let ui = &mut egui::Ui::__test(); -/// use egui::plot::{Curve, Plot, Value, ValueSeries}; +/// use egui::plot::{Line, Plot, Value, ValueSeries}; /// let sin = (0..1000).map(|i| { /// let x = i as f64 * 0.01; /// Value::new(x, x.sin()) /// }); -/// let curve = Curve::new(ValueSeries::from_values_iter(sin)); +/// let line = Line::new(ValueSeries::from_values_iter(sin)); /// ui.add( -/// Plot::new("Test Plot").curve(curve).view_aspect(2.0) +/// Plot::new("Test Plot").line(line).view_aspect(2.0) /// ); /// ``` pub struct Plot { name: String, next_auto_color_idx: usize, - curves: Vec, - points: Vec, + items: Vec>, hlines: Vec, vlines: Vec, @@ -77,8 +77,7 @@ impl Plot { name: name.to_string(), next_auto_color_idx: 0, - curves: Default::default(), - points: Default::default(), + items: Default::default(), hlines: Default::default(), vlines: Default::default(), @@ -109,18 +108,18 @@ impl Plot { Hsva::new(h, 0.85, 0.5, 1.0).into() // TODO: OkLab or some other perspective color space } - /// Add a data curve. - /// You can add multiple curves. - pub fn curve(mut self, mut curve: Curve) -> Self { - if curve.series.is_empty() { + /// Add a data lines. + /// You can add multiple lines. + pub fn line(mut self, mut line: Line) -> Self { + if line.series.is_empty() { return self; }; // Give the stroke an automatic color if no color has been assigned. - if curve.stroke.color == Color32::TRANSPARENT { - curve.stroke.color = self.auto_color(); + if line.stroke.color == Color32::TRANSPARENT { + line.stroke.color = self.auto_color(); } - self.curves.push(curve); + self.items.push(Box::new(line)); self } @@ -136,7 +135,7 @@ impl Plot { if points.color == Color32::TRANSPARENT { points.color = self.auto_color(); } - self.points.push(points); + self.items.push(Box::new(points)); self } @@ -275,8 +274,7 @@ impl Widget for Plot { let Self { name, next_auto_color_idx: _, - mut curves, - mut points, + mut items, hlines, vlines, center_x_axis, @@ -350,39 +348,26 @@ impl Widget for Plot { // --- Legend --- if show_legend { - // Collect the legend entries. If multiple curves have the same name, they share a + // Collect the legend entries. If multiple items have the same name, they share a // checkbox. If their colors don't match, we pick a neutral color for the checkbox. let mut legend_entries: BTreeMap = BTreeMap::new(); let neutral_color = ui.visuals().noninteractive().fg_stroke.color; - curves + items .iter() - .filter(|curve| !curve.name.is_empty()) - .for_each(|curve| { - let checked = !hidden_items.contains(&curve.name); - let text = curve.name.clone(); + .filter(|item| !item.name().is_empty()) + .for_each(|item| { + let checked = !hidden_items.contains(item.name()); + let text = item.name().clone(); legend_entries - .entry(curve.name.clone()) + .entry(item.name().to_string()) .and_modify(|entry| { - if entry.color != curve.stroke.color { + if entry.color != item.color() { entry.color = neutral_color } }) - .or_insert_with(|| LegendEntry::new(text, curve.stroke.color, checked)); - }); - points - .iter() - .filter(|points| !points.name.is_empty()) - .for_each(|points| { - let checked = !hidden_items.contains(&points.name); - let text = points.name.clone(); - legend_entries - .entry(points.name.clone()) - .and_modify(|entry| { - if entry.color != points.color { - entry.color = neutral_color - } - }) - .or_insert_with(|| LegendEntry::new(text, points.color, checked)); + .or_insert_with(|| { + LegendEntry::new(text.to_string(), item.color(), checked) + }); }); // Show the legend. @@ -407,17 +392,15 @@ impl Widget for Plot { .values() .filter(|entry| entry.hovered) .for_each(|entry| { - curves.iter_mut().for_each(|curve| { - curve.highlight |= curve.name == entry.text; - }); - points.iter_mut().for_each(|points| { - points.highlight |= points.name == entry.text; + items.iter_mut().for_each(|item| { + if item.name() == entry.text { + item.highlight(); + } }); }); // Remove deselected items. - curves.retain(|curve| !hidden_items.contains(&curve.name)); - points.retain(|points| !hidden_items.contains(&points.name)); + items.retain(|item| !hidden_items.contains(item.name())); } // --- @@ -429,12 +412,9 @@ impl Widget for Plot { bounds = min_auto_bounds; hlines.iter().for_each(|line| bounds.extend_with_y(line.y)); vlines.iter().for_each(|line| bounds.extend_with_x(line.x)); - curves + items .iter() - .for_each(|curve| bounds.merge(&curve.series.get_bounds())); - points - .iter() - .for_each(|points| bounds.merge(&points.series.get_bounds())); + .for_each(|item| bounds.merge(&item.series().get_bounds())); bounds.add_relative_margin(margin_fraction); } // Make sure they are not empty. @@ -485,18 +465,15 @@ impl Widget for Plot { } // Initialize values from functions. - curves - .iter_mut() - .for_each(|curve| curve.series.generate_points(transform.bounds().range_x())); - points - .iter_mut() - .for_each(|points| points.series.generate_points(transform.bounds().range_x())); + items.iter_mut().for_each(|item| { + item.series_mut() + .generate_points(transform.bounds().range_x()) + }); let bounds = *transform.bounds(); let prepared = Prepared { - curves, - points, + items, hlines, vlines, show_x, @@ -523,8 +500,7 @@ impl Widget for Plot { } struct Prepared { - curves: Vec, - points: Vec, + items: Vec>, hlines: Vec, vlines: Vec, show_x: bool, @@ -560,11 +536,8 @@ impl Prepared { shapes.push(Shape::line_segment(points, stroke)); } - for curve in &self.curves { - curve.get_shapes(transform, &mut shapes); - } - for points in &self.points { - points.get_shapes(transform, &mut shapes); + for item in &self.items { + item.get_shapes(transform, &mut shapes); } if let Some(pointer) = response.hover_pos() { @@ -668,8 +641,7 @@ impl Prepared { transform, show_x, show_y, - curves, - points, + items, .. } = self; @@ -681,25 +653,14 @@ impl Prepared { let mut closest_value = None; let mut closest_item = None; let mut closest_dist_sq = interact_radius.powi(2); - for curve in curves { - for value in &curve.series.values { - let pos = transform.position_from_value(value); - let dist_sq = pointer.distance_sq(pos); - if dist_sq < closest_dist_sq { - closest_dist_sq = dist_sq; - closest_value = Some(value); - closest_item = Some(curve.name.clone()); - } - } - } - for points in points { - for value in &points.series.values { + for item in items { + for value in &item.series().values { let pos = transform.position_from_value(value); let dist_sq = pointer.distance_sq(pos); if dist_sq < closest_dist_sq { closest_dist_sq = dist_sq; closest_value = Some(value); - closest_item = Some(points.name.clone()); + closest_item = Some(item.name()); } } } diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 51c038cb365..a4faa1a191d 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -1,9 +1,9 @@ -use egui::plot::{Curve, MarkerShape, Plot, Points, Value, ValueSeries}; +use egui::plot::{Line, MarkerShape, Plot, Points, Value, ValueSeries}; use egui::*; use std::f64::consts::TAU; #[derive(PartialEq)] -struct CurveDemo { +struct LineDemo { animate: bool, time: f64, circle_radius: f64, @@ -13,7 +13,7 @@ struct CurveDemo { proportional: bool, } -impl Default for CurveDemo { +impl Default for LineDemo { fn default() -> Self { Self { animate: true, @@ -27,7 +27,7 @@ impl Default for CurveDemo { } } -impl CurveDemo { +impl LineDemo { fn options_ui(&mut self, ui: &mut Ui) { let Self { animate, @@ -86,7 +86,7 @@ impl CurveDemo { ui.label("Reset view with double-click."); } - fn circle(&self) -> Curve { + fn circle(&self) -> Line { let n = 512; let circle = (0..=n).map(|i| { let t = remap(i as f64, 0.0..=(n as f64), 0.0..=TAU); @@ -96,14 +96,14 @@ impl CurveDemo { r * t.sin() + self.circle_center.y as f64, ) }); - Curve::new(ValueSeries::from_values_iter(circle)) + Line::new(ValueSeries::from_values_iter(circle)) .color(Color32::from_rgb(100, 200, 100)) .name("circle") } - fn sin(&self) -> Curve { + fn sin(&self) -> Line { let time = self.time; - Curve::new(ValueSeries::from_explicit_callback( + Line::new(ValueSeries::from_explicit_callback( move |x| 0.5 * (2.0 * x).sin() * time.sin(), f64::NEG_INFINITY..=f64::INFINITY, 512, @@ -112,9 +112,9 @@ impl CurveDemo { .name("wave") } - fn thingy(&self) -> Curve { + fn thingy(&self) -> Line { let time = self.time; - Curve::new(ValueSeries::from_parametric_callback( + Line::new(ValueSeries::from_parametric_callback( move |t| ((2.0 * t + time).sin(), (3.0 * t).sin()), 0.0..=TAU, 256, @@ -124,17 +124,17 @@ impl CurveDemo { } } -impl Widget for &mut CurveDemo { +impl Widget for &mut LineDemo { fn ui(self, ui: &mut Ui) -> Response { self.options_ui(ui); if self.animate { ui.ctx().request_repaint(); self.time += ui.input().unstable_dt.at_most(1.0 / 30.0) as f64; }; - let mut plot = Plot::new("Curves Demo") - .curve(self.circle()) - .curve(self.sin()) - .curve(self.thingy()) + let mut plot = Plot::new("Lines Demo") + .line(self.circle()) + .line(self.sin()) + .line(self.thingy()) .height(300.0) .show_legend(self.legend); if self.square { @@ -222,7 +222,7 @@ impl Widget for &mut MarkerDemo { #[derive(PartialEq, Default)] pub struct PlotDemo { - curve_demo: CurveDemo, + line_demo: LineDemo, marker_demo: MarkerDemo, } @@ -247,7 +247,7 @@ impl super::View for PlotDemo { egui::reset_button(ui, self); ui.add(crate::__egui_github_link_file!()); }); - ui.collapsing("Curves", |ui| ui.add(&mut self.curve_demo)); + ui.collapsing("Lines", |ui| ui.add(&mut self.line_demo)); ui.collapsing("Markers", |ui| ui.add(&mut self.marker_demo)); } } diff --git a/egui_demo_lib/src/apps/demo/widget_gallery.rs b/egui_demo_lib/src/apps/demo/widget_gallery.rs index 4e333ae3479..48bf4ee831a 100644 --- a/egui_demo_lib/src/apps/demo/widget_gallery.rs +++ b/egui_demo_lib/src/apps/demo/widget_gallery.rs @@ -208,13 +208,13 @@ impl WidgetGallery { fn example_plot() -> egui::plot::Plot { let n = 128; - let curve = egui::plot::Curve::new(ValueSeries::from_values_iter((0..=n).map(|i| { + let line = egui::plot::Line::new(ValueSeries::from_values_iter((0..=n).map(|i| { use std::f64::consts::TAU; let x = egui::remap(i as f64, 0.0..=(n as f64), -TAU..=TAU); egui::plot::Value::new(x, x.sin()) }))); egui::plot::Plot::new("Example Plot") - .curve(curve) + .line(line) .height(32.0) .data_aspect(1.0) } From 4c375f0d3fe5c4851a1a4b443790714bf0d42860 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Mon, 24 May 2021 22:12:58 +0200 Subject: [PATCH 25/50] clippy fixes --- egui/src/widgets/plot/items.rs | 2 +- egui/src/widgets/plot/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index c66ecf1a16f..b107173594f 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -550,7 +550,7 @@ impl PlotItem for Points { } fn color(&self) -> Color32 { - self.color.clone() + self.color } fn highlight(&mut self) { diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 12cf7869586..5f88975c14c 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -357,7 +357,7 @@ impl Widget for Plot { .filter(|item| !item.name().is_empty()) .for_each(|item| { let checked = !hidden_items.contains(item.name()); - let text = item.name().clone(); + let text = item.name(); legend_entries .entry(item.name().to_string()) .and_modify(|entry| { From 95bfbbfc925f2043c44a409bc58e147e29f67af6 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Mon, 24 May 2021 23:54:34 +0200 Subject: [PATCH 26/50] reduce visibilities --- egui/src/widgets/plot/items.rs | 42 +++++++++++++++++----------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index b107173594f..61cb2c03e38 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -33,8 +33,8 @@ impl Value { /// A horizontal line in a plot, filling the full width #[derive(Clone, Copy, Debug, PartialEq)] pub struct HLine { - pub(crate) y: f64, - pub(crate) stroke: Stroke, + pub(super) y: f64, + pub(super) stroke: Stroke, } impl HLine { @@ -49,8 +49,8 @@ impl HLine { /// A vertical line in a plot, filling the full width #[derive(Clone, Copy, Debug, PartialEq)] pub struct VLine { - pub(crate) x: f64, - pub(crate) stroke: Stroke, + pub(super) x: f64, + pub(super) stroke: Stroke, } impl VLine { @@ -62,7 +62,7 @@ impl VLine { } } -pub(crate) trait PlotItem { +pub(super) trait PlotItem { fn get_shapes(&self, transform: &ScreenTransform, shapes: &mut Vec); fn series(&self) -> &ValueSeries; fn series_mut(&mut self) -> &mut ValueSeries; @@ -81,7 +81,7 @@ struct ExplicitGenerator { } pub struct ValueSeries { - pub(crate) values: Vec, + pub(super) values: Vec, generator: Option, } @@ -154,13 +154,13 @@ impl ValueSeries { } /// Returns true if there are no data points available and there is no function to generate any. - pub(crate) fn is_empty(&self) -> bool { + pub(super) fn is_empty(&self) -> bool { self.generator.is_none() && self.values.is_empty() } /// If initialized with a generator function, this will generate `n` evenly spaced points in the /// given range. - pub(crate) fn generate_points(&mut self, x_range: RangeInclusive) { + pub(super) fn generate_points(&mut self, x_range: RangeInclusive) { if let Some(generator) = self.generator.take() { if let Some(intersection) = Self::range_intersection(&x_range, &generator.x_range) { let increment = @@ -186,7 +186,7 @@ impl ValueSeries { (start < end).then(|| start..=end) } - pub(crate) fn get_bounds(&self) -> Bounds { + pub(super) fn get_bounds(&self) -> Bounds { let mut bounds = Bounds::NOTHING; self.values .iter() @@ -231,10 +231,10 @@ impl MarkerShape { /// A series of values forming a path. pub struct Line { - pub(crate) series: ValueSeries, - pub(crate) stroke: Stroke, - pub(crate) name: String, - pub(crate) highlight: bool, + pub(super) series: ValueSeries, + pub(super) stroke: Stroke, + pub(super) name: String, + pub(super) highlight: bool, } impl Line { @@ -291,7 +291,7 @@ impl PlotItem for Line { } = self; if *highlight { - stroke.width *= 1.5; + stroke.width *= 2.0; } let values_tf: Vec<_> = series @@ -331,16 +331,16 @@ impl PlotItem for Line { /// A series of points. pub struct Points { - pub(crate) series: ValueSeries, - pub(crate) shape: MarkerShape, + pub(super) series: ValueSeries, + pub(super) shape: MarkerShape, /// Color of the marker. `Color32::TRANSPARENT` means that it will be picked automatically. - pub(crate) color: Color32, + pub(super) color: Color32, /// Whether to fill the marker. Does not apply to all types. - pub(crate) filled: bool, + pub(super) filled: bool, /// The maximum extent of the marker from its center. - pub(crate) radius: f32, - pub(crate) name: String, - pub(crate) highlight: bool, + pub(super) radius: f32, + pub(super) name: String, + pub(super) highlight: bool, } impl Points { From 7867c47d715db15878d2c05fc058c5764b512f37 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Tue, 25 May 2021 23:18:38 +0200 Subject: [PATCH 27/50] update legend --- egui/src/widgets/plot/legend.rs | 37 +++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/egui/src/widgets/plot/legend.rs b/egui/src/widgets/plot/legend.rs index d0428112f18..e13b2f64e0b 100644 --- a/egui/src/widgets/plot/legend.rs +++ b/egui/src/widgets/plot/legend.rs @@ -3,9 +3,10 @@ use std::{ string::String, }; -use super::Curve; use crate::*; +use super::items::PlotItem; + /// Where to place the plot legend. #[derive(Debug, Clone, Copy, PartialEq)] pub enum LegendPosition { @@ -46,13 +47,13 @@ impl Default for Legend { #[derive(Clone)] struct LegendEntry { - color: Option, + color: Color32, checked: bool, hovered: bool, } impl LegendEntry { - fn new(color: Option, checked: bool) -> Self { + fn new(color: Color32, checked: bool) -> Self { Self { color, checked, @@ -106,11 +107,15 @@ impl Widget for (&String, &mut LegendEntry) { }); if *checked { - let neutral_color = ui.visuals().noninteractive().fg_stroke.color; + let fill = if *color == Color32::TRANSPARENT { + ui.visuals().noninteractive().fg_stroke.color + } else { + *color + }; painter.add(Shape::Circle { center: icon_rect.center(), radius: icon_size * 0.4, - fill: color.unwrap_or(neutral_color), + fill, stroke: Default::default(), }); } @@ -141,29 +146,29 @@ pub(crate) struct LegendWidget { impl LegendWidget { /// Create a new legend from curves, the names of curves that are hidden and the style of the /// text. Returns `None` if the legend has no entries. - pub fn try_new( + pub(super) fn try_new( rect: Rect, config: Legend, - curves: &[Curve], - hidden_curves: &HashSet, + items: &Vec>, + hidden_items: &HashSet, ) -> Option { // Collect the legend entries. If multiple curves have the same name, they share a // checkbox. If their colors don't match, we pick a neutral color for the checkbox. let mut entries: BTreeMap = BTreeMap::new(); - curves + items .iter() - .filter(|curve| !curve.name.is_empty()) - .for_each(|curve| { + .filter(|item| !item.name().is_empty()) + .for_each(|item| { entries - .entry(curve.name.clone()) + .entry(item.name().to_string()) .and_modify(|entry| { - if entry.color != curve.get_color() { - entry.color = None + if entry.color != item.color() { + entry.color = Color32::TRANSPARENT } }) .or_insert_with(|| { - let color = curve.get_color(); - let checked = !hidden_curves.contains(&curve.name); + let color = item.color(); + let checked = !hidden_items.contains(item.name()); LegendEntry::new(color, checked) }); }); From bffaa701970f57141e2fbcf9a9e224bb7bb3dd2b Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Tue, 25 May 2021 23:19:08 +0200 Subject: [PATCH 28/50] clippy fix --- egui/src/widgets/plot/legend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui/src/widgets/plot/legend.rs b/egui/src/widgets/plot/legend.rs index e13b2f64e0b..02b19713e39 100644 --- a/egui/src/widgets/plot/legend.rs +++ b/egui/src/widgets/plot/legend.rs @@ -149,7 +149,7 @@ impl LegendWidget { pub(super) fn try_new( rect: Rect, config: Legend, - items: &Vec>, + items: &[Box], hidden_items: &HashSet, ) -> Option { // Collect the legend entries. If multiple curves have the same name, they share a From 9f99826cd526c8baf8f4fd89ed476e4ab4989360 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Tue, 25 May 2021 23:21:11 +0200 Subject: [PATCH 29/50] change instances of "curve" to "item" --- egui/src/widgets/plot/legend.rs | 10 +++++----- egui/src/widgets/plot/mod.rs | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/egui/src/widgets/plot/legend.rs b/egui/src/widgets/plot/legend.rs index 02b19713e39..04c7380dda5 100644 --- a/egui/src/widgets/plot/legend.rs +++ b/egui/src/widgets/plot/legend.rs @@ -144,7 +144,7 @@ pub(crate) struct LegendWidget { } impl LegendWidget { - /// Create a new legend from curves, the names of curves that are hidden and the style of the + /// Create a new legend from items, the names of items that are hidden and the style of the /// text. Returns `None` if the legend has no entries. pub(super) fn try_new( rect: Rect, @@ -152,7 +152,7 @@ impl LegendWidget { items: &[Box], hidden_items: &HashSet, ) -> Option { - // Collect the legend entries. If multiple curves have the same name, they share a + // Collect the legend entries. If multiple items have the same name, they share a // checkbox. If their colors don't match, we pick a neutral color for the checkbox. let mut entries: BTreeMap = BTreeMap::new(); items @@ -179,8 +179,8 @@ impl LegendWidget { }) } - // Get the names of the hidden curves. - pub fn get_hidden_curves(&self) -> HashSet { + // Get the names of the hidden items. + pub fn get_hidden_items(&self) -> HashSet { self.entries .iter() .filter(|(_, entry)| !entry.checked) @@ -188,7 +188,7 @@ impl LegendWidget { .collect() } - // Get the name of the hovered curve. + // Get the name of the hovered items. pub fn get_hovered_entry_name(&self) -> Option { self.entries .iter() diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 85faa3ba7aa..238e09e6c67 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -271,7 +271,7 @@ impl Plot { self } - /// Show a legend including all named curves. + /// Show a legend including all named items. pub fn legend(mut self, legend: Legend) -> Self { self.legend_config = Some(legend); self @@ -364,9 +364,9 @@ impl Widget for Plot { show_x = false; show_y = false; } - // Remove the deselected curves. + // Remove the deselected items. items.retain(|item| !hidden_items.contains(item.name())); - // Highlight the hovered curves. + // Highlight the hovered items. if let Some(hovered_name) = &hovered_entry { items .iter_mut() @@ -453,7 +453,7 @@ impl Widget for Plot { if let Some(mut legend) = legend { ui.add(&mut legend); - hidden_items = legend.get_hidden_curves(); + hidden_items = legend.get_hidden_items(); hovered_entry = legend.get_hovered_entry_name(); } From 30b5073d25b3142fd8a443a4b9dc5f1ce8a76618 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Tue, 25 May 2021 23:22:21 +0200 Subject: [PATCH 30/50] change visibility --- egui/src/widgets/plot/legend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui/src/widgets/plot/legend.rs b/egui/src/widgets/plot/legend.rs index 04c7380dda5..23dd77ff533 100644 --- a/egui/src/widgets/plot/legend.rs +++ b/egui/src/widgets/plot/legend.rs @@ -137,7 +137,7 @@ impl Widget for (&String, &mut LegendEntry) { } #[derive(Clone)] -pub(crate) struct LegendWidget { +pub(super) struct LegendWidget { rect: Rect, entries: BTreeMap, config: Legend, From 88fae19dcb5fa08d0d3c9bc53f1f4323232acac2 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Wed, 26 May 2021 23:47:13 +0200 Subject: [PATCH 31/50] Update egui/src/widgets/plot/mod.rs Co-authored-by: Emil Ernerfeldt --- egui/src/widgets/plot/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 5f88975c14c..8791fd8bc94 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -144,7 +144,7 @@ impl Plot { /// Can be useful e.g. to show min/max bounds or similar. /// Always fills the full width of the plot. pub fn hline(mut self, mut hline: HLine) -> Self { - if hline.stroke.color != Color32::TRANSPARENT { + if hline.stroke.color == Color32::TRANSPARENT { hline.stroke.color = self.auto_color(); } self.hlines.push(hline); From 8bc576b7259f5e3f8bba5f5e7d8a2043e9624d77 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Wed, 26 May 2021 23:49:03 +0200 Subject: [PATCH 32/50] Update egui/src/widgets/plot/mod.rs Co-authored-by: Emil Ernerfeldt --- egui/src/widgets/plot/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 8791fd8bc94..1d2b4f6f3f9 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -155,7 +155,7 @@ impl Plot { /// Can be useful e.g. to show min/max bounds or similar. /// Always fills the full height of the plot. pub fn vline(mut self, mut vline: VLine) -> Self { - if vline.stroke.color != Color32::TRANSPARENT { + if vline.stroke.color == Color32::TRANSPARENT { vline.stroke.color = self.auto_color(); } self.vlines.push(vline); From c6423beaf18505929ff3a7ec31ca069a1ff62ee9 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Wed, 26 May 2021 23:50:35 +0200 Subject: [PATCH 33/50] Update egui_demo_lib/src/apps/demo/plot_demo.rs Co-authored-by: Emil Ernerfeldt --- egui_demo_lib/src/apps/demo/plot_demo.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index a4faa1a191d..46ac633f98d 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -181,7 +181,7 @@ impl MarkerDemo { Value::new(5.0, 0.0 + y_offset), Value::new(6.0, 0.5 + y_offset), ])) - .name("Marker Lines") + .name(format!("{:?}", marker)) .filled(self.fill_markers) .radius(self.marker_radius) .shape(marker); From 3aeb3547a8be0dcc848fd7cdf7333d54636c3534 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Wed, 26 May 2021 23:56:46 +0200 Subject: [PATCH 34/50] Update egui_demo_lib/src/apps/demo/plot_demo.rs Co-authored-by: Emil Ernerfeldt --- egui_demo_lib/src/apps/demo/plot_demo.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 46ac633f98d..a707836c39c 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -247,7 +247,17 @@ impl super::View for PlotDemo { egui::reset_button(ui, self); ui.add(crate::__egui_github_link_file!()); }); - ui.collapsing("Lines", |ui| ui.add(&mut self.line_demo)); - ui.collapsing("Markers", |ui| ui.add(&mut self.marker_demo)); + ui.separator(); + ui.horizontal(|ui| { + ui.selectable_value(&mut self.showing_markers, false, "Lines"); + ui.selectable_value(&mut self.showing_markers, true, "Markers"); + }); + ui.separator(); + + if self.showing_markers { + ui.add(&mut self.marker_demo); + } else { + ui.add(&mut self.line_demo); + } } } From e76a67918a4d4866c30ad5ac67d0ce7784ec5e83 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Thu, 27 May 2021 00:05:56 +0200 Subject: [PATCH 35/50] changes based on review --- CHANGELOG.md | 3 + egui/src/widgets/plot/items.rs | 30 +++++----- egui/src/widgets/plot/mod.rs | 2 +- egui_demo_lib/src/apps/demo/plot_demo.rs | 60 ++++++++++++------- egui_demo_lib/src/apps/demo/widget_gallery.rs | 9 ++- 5 files changed, 60 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6cbaa148c6..98e61441520 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [ * Add an option to overwrite frame of `SidePanel` and `TopPanel`. * `TextEdit` now supports edits on a generic buffer using `TextBuffer`. +### Changed 🔧 +* Plot: Changed `Curve` to `Line`. + ## 0.12.0 - 2021-05-10 - Multitouch, user memory, window pivots, and improved plots ### Added ⭐ diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index 61cb2c03e38..3f881ab73e3 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -64,8 +64,8 @@ impl VLine { pub(super) trait PlotItem { fn get_shapes(&self, transform: &ScreenTransform, shapes: &mut Vec); - fn series(&self) -> &ValueSeries; - fn series_mut(&mut self) -> &mut ValueSeries; + fn series(&self) -> &Values; + fn series_mut(&mut self) -> &mut Values; fn name(&self) -> &str; fn color(&self) -> Color32; fn highlight(&mut self); @@ -80,12 +80,12 @@ struct ExplicitGenerator { points: usize, } -pub struct ValueSeries { +pub struct Values { pub(super) values: Vec, generator: Option, } -impl Default for ValueSeries { +impl Default for Values { fn default() -> Self { Self { values: Vec::new(), @@ -94,7 +94,7 @@ impl Default for ValueSeries { } } -impl ValueSeries { +impl Values { pub fn from_values(values: Vec) -> Self { Self { values, @@ -231,14 +231,14 @@ impl MarkerShape { /// A series of values forming a path. pub struct Line { - pub(super) series: ValueSeries, + pub(super) series: Values, pub(super) stroke: Stroke, pub(super) name: String, pub(super) highlight: bool, } impl Line { - pub fn new(series: ValueSeries) -> Self { + pub fn new(series: Values) -> Self { Self { series, stroke: Stroke::new(1.0, Color32::TRANSPARENT), @@ -265,7 +265,7 @@ impl Line { self } - /// Stroke color. + /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. pub fn color(mut self, color: impl Into) -> Self { self.stroke.color = color.into(); self @@ -308,11 +308,11 @@ impl PlotItem for Line { shapes.push(line_shape); } - fn series(&self) -> &ValueSeries { + fn series(&self) -> &Values { &self.series } - fn series_mut(&mut self) -> &mut ValueSeries { + fn series_mut(&mut self) -> &mut Values { &mut self.series } @@ -329,9 +329,9 @@ impl PlotItem for Line { } } -/// A series of points. +/// A set of points. pub struct Points { - pub(super) series: ValueSeries, + pub(super) series: Values, pub(super) shape: MarkerShape, /// Color of the marker. `Color32::TRANSPARENT` means that it will be picked automatically. pub(super) color: Color32, @@ -344,7 +344,7 @@ pub struct Points { } impl Points { - pub fn new(series: ValueSeries) -> Self { + pub fn new(series: Values) -> Self { Self { series, shape: MarkerShape::Circle, @@ -537,11 +537,11 @@ impl PlotItem for Points { }); } - fn series(&self) -> &ValueSeries { + fn series(&self) -> &Values { &self.series } - fn series_mut(&mut self) -> &mut ValueSeries { + fn series_mut(&mut self) -> &mut Values { &mut self.series } diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 1d2b4f6f3f9..40c39e23419 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -8,7 +8,7 @@ use std::collections::{BTreeMap, HashSet}; use items::PlotItem; pub use items::{HLine, VLine}; -pub use items::{Line, MarkerShape, Points, Value, ValueSeries}; +pub use items::{Line, MarkerShape, Points, Value, Values}; use legend::LegendEntry; 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 a707836c39c..d562752661e 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -1,4 +1,4 @@ -use egui::plot::{Line, MarkerShape, Plot, Points, Value, ValueSeries}; +use egui::plot::{Line, MarkerShape, Plot, Points, Value, Values}; use egui::*; use std::f64::consts::TAU; @@ -65,25 +65,14 @@ impl LineDemo { }); }); - ui.horizontal(|ui| { + ui.vertical(|ui| { ui.style_mut().wrap = Some(false); ui.checkbox(animate, "animate"); - ui.add_space(8.0); ui.checkbox(square, "square view"); ui.checkbox(legend, "legend"); ui.checkbox(proportional, "proportional data axes"); }); }); - - ui.label("Pan by dragging, or scroll (+ shift = horizontal)."); - if cfg!(target_arch = "wasm32") { - ui.label("Zoom with ctrl / ⌘ + mouse wheel, or with pinch gesture."); - } else if cfg!(target_os = "macos") { - ui.label("Zoom with ctrl / ⌘ + scroll."); - } else { - ui.label("Zoom with ctrl + scroll."); - } - ui.label("Reset view with double-click."); } fn circle(&self) -> Line { @@ -96,14 +85,14 @@ impl LineDemo { r * t.sin() + self.circle_center.y as f64, ) }); - Line::new(ValueSeries::from_values_iter(circle)) + Line::new(Values::from_values_iter(circle)) .color(Color32::from_rgb(100, 200, 100)) .name("circle") } fn sin(&self) -> Line { let time = self.time; - Line::new(ValueSeries::from_explicit_callback( + Line::new(Values::from_explicit_callback( move |x| 0.5 * (2.0 * x).sin() * time.sin(), f64::NEG_INFINITY..=f64::INFINITY, 512, @@ -114,7 +103,7 @@ impl LineDemo { fn thingy(&self) -> Line { let time = self.time; - Line::new(ValueSeries::from_parametric_callback( + Line::new(Values::from_parametric_callback( move |t| ((2.0 * t + time).sin(), (3.0 * t).sin()), 0.0..=TAU, 256, @@ -173,7 +162,7 @@ impl MarkerDemo { .enumerate() .map(|(i, marker)| { let y_offset = i as f32 * 0.5 + 1.0; - let mut points = Points::new(ValueSeries::from_values(vec![ + let mut points = Points::new(Values::from_values(vec![ Value::new(1.0, 0.0 + y_offset), Value::new(2.0, 0.5 + y_offset), Value::new(3.0, 0.0 + y_offset), @@ -220,10 +209,23 @@ impl Widget for &mut MarkerDemo { } } +#[derive(PartialEq, Eq)] +enum Panel { + Lines, + Markers, +} + +impl Default for Panel { + fn default() -> Self { + Self::Lines + } +} + #[derive(PartialEq, Default)] pub struct PlotDemo { line_demo: LineDemo, marker_demo: MarkerDemo, + open_panel: Panel, } impl super::Demo for PlotDemo { @@ -246,18 +248,30 @@ impl super::View for PlotDemo { ui.vertical_centered(|ui| { egui::reset_button(ui, self); ui.add(crate::__egui_github_link_file!()); + ui.label("Pan by dragging, or scroll (+ shift = horizontal)."); + if cfg!(target_arch = "wasm32") { + ui.label("Zoom with ctrl / ⌘ + mouse wheel, or with pinch gesture."); + } else if cfg!(target_os = "macos") { + ui.label("Zoom with ctrl / ⌘ + scroll."); + } else { + ui.label("Zoom with ctrl + scroll."); + } + ui.label("Reset view with double-click."); }); ui.separator(); ui.horizontal(|ui| { - ui.selectable_value(&mut self.showing_markers, false, "Lines"); - ui.selectable_value(&mut self.showing_markers, true, "Markers"); + ui.selectable_value(&mut self.open_panel, Panel::Lines, "Lines"); + ui.selectable_value(&mut self.open_panel, Panel::Markers, "Markers"); }); ui.separator(); - if self.showing_markers { - ui.add(&mut self.marker_demo); - } else { - ui.add(&mut self.line_demo); + match self.open_panel { + Panel::Lines => { + ui.add(&mut self.line_demo); + } + Panel::Markers => { + ui.add(&mut self.marker_demo); + } } } } diff --git a/egui_demo_lib/src/apps/demo/widget_gallery.rs b/egui_demo_lib/src/apps/demo/widget_gallery.rs index 48bf4ee831a..7570c9e2176 100644 --- a/egui_demo_lib/src/apps/demo/widget_gallery.rs +++ b/egui_demo_lib/src/apps/demo/widget_gallery.rs @@ -1,5 +1,3 @@ -use egui::plot::ValueSeries; - #[derive(Debug, PartialEq)] #[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] enum Enum { @@ -207,13 +205,14 @@ impl WidgetGallery { } fn example_plot() -> egui::plot::Plot { + use egui::plot::{Line, Plot, Value, Values}; let n = 128; - let line = egui::plot::Line::new(ValueSeries::from_values_iter((0..=n).map(|i| { + let line = Line::new(Values::from_values_iter((0..=n).map(|i| { use std::f64::consts::TAU; let x = egui::remap(i as f64, 0.0..=(n as f64), -TAU..=TAU); - egui::plot::Value::new(x, x.sin()) + Value::new(x, x.sin()) }))); - egui::plot::Plot::new("Example Plot") + Plot::new("Example Plot") .line(line) .height(32.0) .data_aspect(1.0) From af472506e8ddc3dce694a9590c59535396e49e35 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Thu, 27 May 2021 00:15:49 +0200 Subject: [PATCH 36/50] add legend to demo --- egui_demo_lib/src/apps/demo/plot_demo.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index bbd7d41473e..e9875400319 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -325,6 +325,7 @@ impl super::View for PlotDemo { ui.horizontal(|ui| { ui.selectable_value(&mut self.open_panel, Panel::Lines, "Lines"); ui.selectable_value(&mut self.open_panel, Panel::Markers, "Markers"); + ui.selectable_value(&mut self.open_panel, Panel::Legend, "Legend"); }); ui.separator(); From 43b45390ad9b81e7cfd9d69a2fe85e0c4bf7dd9b Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Thu, 27 May 2021 00:17:25 +0200 Subject: [PATCH 37/50] fix test --- egui/src/widgets/plot/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 40c39e23419..33e51f8a156 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -34,12 +34,12 @@ struct PlotMemory { /// /// ``` /// # let ui = &mut egui::Ui::__test(); -/// use egui::plot::{Line, Plot, Value, ValueSeries}; +/// use egui::plot::{Line, Plot, Value, Values}; /// let sin = (0..1000).map(|i| { /// let x = i as f64 * 0.01; /// Value::new(x, x.sin()) /// }); -/// let line = Line::new(ValueSeries::from_values_iter(sin)); +/// let line = Line::new(Values::from_values_iter(sin)); /// ui.add( /// Plot::new("Test Plot").line(line).view_aspect(2.0) /// ); From a40efbfc632d271ab78663b087cd28acbc70cf09 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Thu, 27 May 2021 00:24:04 +0200 Subject: [PATCH 38/50] move highlighted items to front --- egui/src/widgets/plot/items.rs | 9 +++++++++ egui/src/widgets/plot/mod.rs | 2 ++ 2 files changed, 11 insertions(+) diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index a8cc6659ddf..43184f2e82f 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -69,6 +69,7 @@ pub(super) trait PlotItem { fn name(&self) -> &str; fn color(&self) -> Color32; fn highlight(&mut self); + fn highlighted(&self) -> bool; } // ---------------------------------------------------------------------------- @@ -328,6 +329,10 @@ impl PlotItem for Line { fn highlight(&mut self) { self.highlight = true; } + + fn highlighted(&self) -> bool { + self.highlight + } } /// A set of points. @@ -558,4 +563,8 @@ impl PlotItem for Points { fn highlight(&mut self) { self.highlight = true; } + + fn highlighted(&self) -> bool { + self.highlight + } } diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 2b16667630d..bb20a62acd9 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -373,6 +373,8 @@ impl Widget for Plot { .filter(|entry| entry.name() == hovered_name) .for_each(|entry| entry.highlight()); } + // Move highlighted items to front. + items.sort_by(|a, b| a.highlighted().cmp(&b.highlighted())); auto_bounds |= response.double_clicked_by(PointerButton::Primary); From c541952c228c7f05df628341cf21e8083e62c9f8 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Thu, 27 May 2021 00:27:20 +0200 Subject: [PATCH 39/50] dynamic plot size --- egui_demo_lib/src/apps/demo/plot_demo.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index d562752661e..6a7faf3ec38 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -124,7 +124,6 @@ impl Widget for &mut LineDemo { .line(self.circle()) .line(self.sin()) .line(self.thingy()) - .height(300.0) .show_legend(self.legend); if self.square { plot = plot.view_aspect(1.0); From 5f294049cd9fb325fd03fc969833df44309ebd83 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Thu, 27 May 2021 17:08:59 +0200 Subject: [PATCH 40/50] add legend again --- egui_demo_lib/src/apps/demo/plot_demo.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 25b7692ed63..e7f03fdd7ca 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -196,7 +196,9 @@ impl Widget for &mut MarkerDemo { } }); - let mut markers_plot = Plot::new("Markers Demo").height(300.0).data_aspect(1.0); + let mut markers_plot = Plot::new("Markers Demo") + .data_aspect(1.0) + .legend(Legend::default()); for marker in self.markers() { markers_plot = markers_plot.points(marker); } @@ -263,7 +265,6 @@ impl Widget for &mut LegendDemo { .line(LegendDemo::line_with_slope(2.0).name("lines")) .line(LegendDemo::sin().name("sin(x)")) .line(LegendDemo::cos().name("cos(x)")) - .height(300.0) .legend(*config) .data_aspect(1.0); ui.add(legend_plot) From 4197bd137cd6ec15b0d7c34db170565ac6dc07ae Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Thu, 27 May 2021 17:09:49 +0200 Subject: [PATCH 41/50] remove height --- egui_demo_lib/src/apps/demo/plot_demo.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 6a7faf3ec38..0b59fd08551 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -200,7 +200,7 @@ impl Widget for &mut MarkerDemo { } }); - let mut markers_plot = Plot::new("Markers Demo").height(300.0).data_aspect(1.0); + let mut markers_plot = Plot::new("Markers Demo").data_aspect(1.0); for marker in self.markers() { markers_plot = markers_plot.points(marker); } From 8c9d1a0634442c05122d2e0eacfaad5dfe802878 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Thu, 27 May 2021 17:20:13 +0200 Subject: [PATCH 42/50] clippy fix --- egui/src/widgets/plot/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index bb20a62acd9..de1efe7628f 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -374,7 +374,7 @@ impl Widget for Plot { .for_each(|entry| entry.highlight()); } // Move highlighted items to front. - items.sort_by(|a, b| a.highlighted().cmp(&b.highlighted())); + items.sort_by_key(|item| item.highlighted()); auto_bounds |= response.double_clicked_by(PointerButton::Primary); From 7653addc6102be138bef344c3e5212de44e74aa3 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Thu, 27 May 2021 18:47:18 +0200 Subject: [PATCH 43/50] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4517efe9129..383541d8034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [ ## Unreleased ### Added ⭐ +* [Plot legend improvements](https://github.com/emilk/egui/pull/410). * [Line markers for plots](https://github.com/emilk/egui/pull/363). * Add right and bottom panels (`SidePanel::right` and `Panel::bottom`). * Add resizable panels. From 96f175a7922156b270c238a20c62265420772b7c Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Fri, 28 May 2021 19:38:19 +0200 Subject: [PATCH 44/50] minor changes --- egui/src/widgets/plot/items.rs | 4 ++-- egui/src/widgets/plot/mod.rs | 16 +++++----------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index 58d90b56da1..837908ab4f4 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -394,8 +394,8 @@ impl Points { /// Name of this set of points. /// - /// This name will show up in the plot legend, if legends are turned on. Multiple lines may - /// share the same name. + /// This name will show up in the plot legend, if legends are turned on. Multiple sets of points + /// may share the same name. #[allow(clippy::needless_pass_by_value)] pub fn name(mut self, name: impl ToString) -> Self { self.name = name.to_string(); diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index de1efe7628f..5cc2d08fccd 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -518,13 +518,11 @@ impl Prepared { item.get_shapes(transform, &mut shapes); } - let painter_rect = ui.painter().sub_region(*transform.frame()); - - painter_rect.extend(shapes); - if let Some(pointer) = response.hover_pos() { - painter_rect.extend(self.hover(ui, pointer)); + self.hover(ui, pointer, &mut shapes); } + + ui.painter().sub_region(*transform.frame()).extend(shapes); } fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec) { @@ -616,7 +614,7 @@ impl Prepared { } } - fn hover(&self, ui: &Ui, pointer: Pos2) -> Vec { + fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec) { let Self { transform, show_x, @@ -625,10 +623,8 @@ impl Prepared { .. } = self; - let mut shapes = Vec::new(); - if !show_x && !show_y { - return Vec::new(); + return; } let interact_radius: f32 = 16.0; @@ -712,7 +708,5 @@ impl Prepared { TextStyle::Body, ui.visuals().text_color(), )); - - shapes } } From eac5bf56b3d345dc00c0558364d3e740d6701273 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Thu, 3 Jun 2021 19:07:47 +0200 Subject: [PATCH 45/50] Update egui/src/widgets/plot/legend.rs Co-authored-by: Emil Ernerfeldt --- egui/src/widgets/plot/legend.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/egui/src/widgets/plot/legend.rs b/egui/src/widgets/plot/legend.rs index 23dd77ff533..173ae888c9c 100644 --- a/egui/src/widgets/plot/legend.rs +++ b/egui/src/widgets/plot/legend.rs @@ -10,10 +10,10 @@ use super::items::PlotItem; /// Where to place the plot legend. #[derive(Debug, Clone, Copy, PartialEq)] pub enum LegendPosition { - TopLeft, - TopRight, - BottomLeft, - BottomRight, + LeftTop, + RightTop, + LeftBottom, + RightBottom, } impl LegendPosition { From 109a9b20874b52da63ad3e3a7100dc15568b2e05 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Thu, 3 Jun 2021 19:07:54 +0200 Subject: [PATCH 46/50] Update egui/src/widgets/plot/legend.rs Co-authored-by: Emil Ernerfeldt --- egui/src/widgets/plot/legend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui/src/widgets/plot/legend.rs b/egui/src/widgets/plot/legend.rs index 173ae888c9c..878a0cd6b9e 100644 --- a/egui/src/widgets/plot/legend.rs +++ b/egui/src/widgets/plot/legend.rs @@ -87,7 +87,7 @@ impl Widget for (&String, &mut LegendEntry) { response.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, &galley.text)); let visuals = ui.style().interact(&response); - let flipped = ui.layout().cross_align() == Align::RIGHT; + let flipped = ui.layout().horizontal_align() == Align::RIGHT; let icon_position_x = if flipped { rect.right() - icon_size / 2.0 From 834f7845594cb1c0c6c2701fa09657029d5d1858 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Thu, 3 Jun 2021 19:08:03 +0200 Subject: [PATCH 47/50] Update egui/src/widgets/plot/legend.rs Co-authored-by: Emil Ernerfeldt --- egui/src/widgets/plot/legend.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/egui/src/widgets/plot/legend.rs b/egui/src/widgets/plot/legend.rs index 878a0cd6b9e..36ac055d464 100644 --- a/egui/src/widgets/plot/legend.rs +++ b/egui/src/widgets/plot/legend.rs @@ -163,7 +163,8 @@ impl LegendWidget { .entry(item.name().to_string()) .and_modify(|entry| { if entry.color != item.color() { - entry.color = Color32::TRANSPARENT + // Multiple items with different colors + entry.color = Color32::TRANSPARENT; } }) .or_insert_with(|| { From 384502087dd13d7cd284ed1f4cabd65645f8697f Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Thu, 3 Jun 2021 19:25:08 +0200 Subject: [PATCH 48/50] changes based on review --- egui/src/widgets/plot/items.rs | 4 +- egui/src/widgets/plot/legend.rs | 57 +++++++++++------------- egui/src/widgets/plot/mod.rs | 2 +- egui_demo_lib/src/apps/demo/plot_demo.rs | 4 +- 4 files changed, 30 insertions(+), 37 deletions(-) diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index 837908ab4f4..9e49c476b8c 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -275,7 +275,7 @@ impl Line { /// Name of this line. /// /// This name will show up in the plot legend, if legends are turned on. Multiple lines may - /// share the same name. + /// share the same name, in which case they will also share an entry in the legend. #[allow(clippy::needless_pass_by_value)] pub fn name(mut self, name: impl ToString) -> Self { self.name = name.to_string(); @@ -395,7 +395,7 @@ impl Points { /// Name of this set of points. /// /// This name will show up in the plot legend, if legends are turned on. Multiple sets of points - /// may share the same name. + /// may share the same name, in which case they will also share an entry in the legend. #[allow(clippy::needless_pass_by_value)] pub fn name(mut self, name: impl ToString) -> Self { self.name = name.to_string(); diff --git a/egui/src/widgets/plot/legend.rs b/egui/src/widgets/plot/legend.rs index 36ac055d464..8c6f53b389b 100644 --- a/egui/src/widgets/plot/legend.rs +++ b/egui/src/widgets/plot/legend.rs @@ -9,20 +9,20 @@ use super::items::PlotItem; /// Where to place the plot legend. #[derive(Debug, Clone, Copy, PartialEq)] -pub enum LegendPosition { +pub enum Corner { LeftTop, RightTop, LeftBottom, RightBottom, } -impl LegendPosition { - pub fn all() -> impl Iterator { +impl Corner { + pub fn all() -> impl Iterator { [ - LegendPosition::TopLeft, - LegendPosition::TopRight, - LegendPosition::BottomLeft, - LegendPosition::BottomRight, + Corner::LeftTop, + Corner::RightTop, + Corner::LeftBottom, + Corner::RightBottom, ] .iter() .copied() @@ -33,14 +33,14 @@ impl LegendPosition { #[derive(Clone, Copy, PartialEq)] pub struct Legend { pub text_style: TextStyle, - pub position: LegendPosition, + pub position: Corner, } impl Default for Legend { fn default() -> Self { Self { text_style: TextStyle::Body, - position: LegendPosition::TopRight, + position: Corner::RightTop, } } } @@ -60,22 +60,15 @@ impl LegendEntry { hovered: false, } } -} -impl Widget for (&String, &mut LegendEntry) { - fn ui(self, ui: &mut Ui) -> Response { - let ( - text, - LegendEntry { - color, - checked, - hovered, - }, - ) = self; - - let galley = ui - .fonts() - .layout_no_wrap(ui.style().body_text_style, text.clone()); + fn ui(&mut self, ui: &mut Ui, text: String) -> Response { + let Self { + color, + checked, + hovered, + } = self; + + let galley = ui.fonts().layout_no_wrap(ui.style().body_text_style, text); let icon_size = galley.size.y; let icon_spacing = icon_size / 5.0; @@ -87,9 +80,9 @@ impl Widget for (&String, &mut LegendEntry) { response.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, &galley.text)); let visuals = ui.style().interact(&response); - let flipped = ui.layout().horizontal_align() == Align::RIGHT; + let label_on_the_left = ui.layout().cross_align() == Align::RIGHT; - let icon_position_x = if flipped { + let icon_position_x = if label_on_the_left { rect.right() - icon_size / 2.0 } else { rect.left() + icon_size / 2.0 @@ -120,7 +113,7 @@ impl Widget for (&String, &mut LegendEntry) { }); } - let text_position_x = if flipped { + let text_position_x = if label_on_the_left { rect.right() - icon_size - icon_spacing - galley.size.x } else { rect.left() + icon_size + icon_spacing @@ -207,12 +200,12 @@ impl Widget for &mut LegendWidget { } = self; let main_dir = match config.position { - LegendPosition::TopLeft | LegendPosition::TopRight => Direction::TopDown, - LegendPosition::BottomLeft | LegendPosition::BottomRight => Direction::BottomUp, + Corner::LeftTop | Corner::RightTop => Direction::TopDown, + Corner::LeftBottom | Corner::RightBottom => Direction::BottomUp, }; let cross_align = match config.position { - LegendPosition::TopLeft | LegendPosition::BottomLeft => Align::LEFT, - LegendPosition::TopRight | LegendPosition::BottomRight => Align::RIGHT, + Corner::LeftTop | Corner::LeftBottom => Align::LEFT, + Corner::RightTop | Corner::RightBottom => Align::RIGHT, }; let layout = Layout::from_main_dir_and_cross_align(main_dir, cross_align); let legend_pad = 2.0; @@ -223,7 +216,7 @@ impl Widget for &mut LegendWidget { ui.style_mut().body_text_style = config.text_style; entries .iter_mut() - .map(|entry| ui.add(entry)) + .map(|(name, entry)| entry.ui(ui, name.clone())) .reduce(|r1, r2| r1.union(r2)) .unwrap() }) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 5cc2d08fccd..902355eed83 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -10,7 +10,7 @@ use items::PlotItem; pub use items::{HLine, VLine}; pub use items::{Line, MarkerShape, Points, Value, Values}; use legend::LegendWidget; -pub use legend::{Legend, LegendPosition}; +pub use legend::{Corner, Legend}; use transform::{Bounds, ScreenTransform}; use crate::*; diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index e7f03fdd7ca..6cf73a53431 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -1,5 +1,5 @@ use egui::*; -use plot::{Legend, LegendPosition, Line, MarkerShape, Plot, Points, Value, Values}; +use plot::{Corner, Legend, Line, MarkerShape, Plot, Points, Value, Values}; use std::f64::consts::TAU; #[derive(PartialEq)] @@ -255,7 +255,7 @@ impl Widget for &mut LegendDemo { }); ui.label("Position:"); ui.horizontal(|ui| { - LegendPosition::all().for_each(|position| { + Corner::all().for_each(|position| { ui.selectable_value(&mut config.position, position, format!("{:?}", position)); }); }); From 25bc76f14b40d8bc4da2ce24b9f611f21b9a47f8 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sat, 5 Jun 2021 20:54:28 +0200 Subject: [PATCH 49/50] add functions to mutate legend config --- egui/src/widgets/plot/legend.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/egui/src/widgets/plot/legend.rs b/egui/src/widgets/plot/legend.rs index 8c6f53b389b..165e74afba1 100644 --- a/egui/src/widgets/plot/legend.rs +++ b/egui/src/widgets/plot/legend.rs @@ -45,6 +45,18 @@ impl Default for Legend { } } +impl Legend { + pub fn text_style(mut self, style: TextStyle) -> Self { + self.text_style = style; + self + } + + pub fn position(mut self, corner: Corner) -> Self { + self.position = corner; + self + } +} + #[derive(Clone)] struct LegendEntry { color: Color32, From 1fcf5c04711c575121c6a324610c1135ab4a6ae0 Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Mon, 7 Jun 2021 21:39:34 +0200 Subject: [PATCH 50/50] use horizontal_align --- egui/src/widgets/plot/legend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui/src/widgets/plot/legend.rs b/egui/src/widgets/plot/legend.rs index 165e74afba1..3f29d025600 100644 --- a/egui/src/widgets/plot/legend.rs +++ b/egui/src/widgets/plot/legend.rs @@ -92,7 +92,7 @@ impl LegendEntry { response.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, &galley.text)); let visuals = ui.style().interact(&response); - let label_on_the_left = ui.layout().cross_align() == Align::RIGHT; + let label_on_the_left = ui.layout().horizontal_align() == Align::RIGHT; let icon_position_x = if label_on_the_left { rect.right() - icon_size / 2.0