diff --git a/CHANGELOG.md b/CHANGELOG.md index 7880f2fe695..4517efe9129 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 ⭐ +* [Line markers for plots](https://github.com/emilk/egui/pull/363). * Add right and bottom panels (`SidePanel::right` and `Panel::bottom`). * Add resizable panels. * Add an option to overwrite frame of a `Panel`. @@ -18,6 +19,7 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [ * `TextEdit` now supports edits on a generic buffer using `TextBuffer`. ### Changed 🔧 +* Plot: Changed `Curve` to `Line`. * `TopPanel::top` is now `TopBottomPanel::top`. * `SidePanel::left` no longet takes the default width by argument, but by a builder call. diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index d0b4c72c8d7..3f881ab73e3 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. @@ -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,6 +62,15 @@ impl VLine { } } +pub(super) trait PlotItem { + fn get_shapes(&self, transform: &ScreenTransform, shapes: &mut Vec); + fn series(&self) -> &Values; + fn series_mut(&mut self) -> &mut Values; + 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. @@ -71,37 +80,25 @@ struct ExplicitGenerator { points: usize, } -// ---------------------------------------------------------------------------- - -/// A series of values forming a path. -pub struct Curve { - pub(crate) values: Vec, +pub struct Values { + pub(super) values: Vec, generator: Option, - pub(crate) bounds: Bounds, - pub(crate) stroke: Stroke, - pub(crate) name: String, } -impl Curve { - fn empty() -> Self { +impl Default for Values { + fn default() -> Self { Self { values: Vec::new(), generator: None, - bounds: Bounds::NOTHING, - stroke: Stroke::new(2.0, Color32::TRANSPARENT), - name: Default::default(), } } +} +impl Values { 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, } } @@ -109,18 +106,12 @@ impl Curve { 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, 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, @@ -128,13 +119,12 @@ impl Curve { }; Self { + values: Vec::new(), generator: Some(generator), - bounds, - ..Self::empty() } } - /// 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, @@ -149,24 +139,28 @@ 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(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 = @@ -182,18 +176,81 @@ 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(super) 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 Line { + pub(super) series: Values, + pub(super) stroke: Stroke, + pub(super) name: String, + pub(super) highlight: bool, +} + +impl Line { + pub fn new(series: Values) -> Self { + Self { + series, + stroke: Stroke::new(1.0, Color32::TRANSPARENT), + name: Default::default(), + highlight: false, + } + } + + /// Highlight this line in the plot by scaling up the line and marker size. + pub fn highlight(mut self) -> Self { + self.highlight = true; + self } /// Add a stroke. @@ -214,13 +271,289 @@ impl Curve { self } - /// Name of this curve. + /// Name of this line. /// - /// 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 } } + +impl PlotItem for Line { + fn get_shapes(&self, transform: &ScreenTransform, shapes: &mut Vec) { + let Self { + series, + mut stroke, + highlight, + .. + } = self; + + if *highlight { + stroke.width *= 2.0; + } + + let values_tf: Vec<_> = series + .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); + } + + fn series(&self) -> &Values { + &self.series + } + + fn series_mut(&mut self) -> &mut Values { + &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 set of points. +pub struct Points { + 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, + /// Whether to fill the marker. Does not apply to all types. + pub(super) filled: bool, + /// The maximum extent of the marker from its center. + pub(super) radius: f32, + pub(super) name: String, + pub(super) highlight: bool, +} + +impl Points { + pub fn new(series: Values) -> Self { + Self { + series, + shape: MarkerShape::Circle, + color: Color32::TRANSPARENT, + filled: true, + radius: 1.0, + name: Default::default(), + highlight: false, + } + } + + /// 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 their markers. + pub fn highlight(mut self) -> Self { + self.highlight = true; + self + } + + /// Set the marker'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 series of markers. + /// + /// 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 + } +} + +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(); + + 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(|value| transform.position_from_value(value)) + .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)); + } + } + }); + } + + fn series(&self) -> &Values { + &self.series + } + + fn series_mut(&mut self) -> &mut Values { + &mut self.series + } + + fn name(&self) -> &str { + self.name.as_str() + } + + fn color(&self) -> Color32 { + self.color + } + + fn highlight(&mut self) { + self.highlight = true; + } +} diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 9787bdb09b8..33e51f8a156 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -6,15 +6,15 @@ mod transform; use std::collections::{BTreeMap, HashSet}; -pub use items::{Curve, Value}; +use items::PlotItem; pub use items::{HLine, VLine}; +pub use items::{Line, MarkerShape, Points, Value, Values}; +use legend::LegendEntry; use transform::{Bounds, ScreenTransform}; use crate::*; use color::Hsva; -use self::legend::LegendEntry; - // ---------------------------------------------------------------------------- /// Information about the plot that has to persist between frames. @@ -23,32 +23,32 @@ use self::legend::LegendEntry; struct PlotMemory { bounds: Bounds, auto_bounds: bool, - hidden_curves: HashSet, + hidden_items: HashSet, } // ---------------------------------------------------------------------------- /// 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}; +/// 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 curve = Curve::from_values_iter(sin); +/// let line = Line::new(Values::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, + items: Vec>, hlines: Vec, vlines: Vec, @@ -77,7 +77,7 @@ impl Plot { name: name.to_string(), next_auto_color_idx: 0, - curves: Default::default(), + items: Default::default(), hlines: Default::default(), vlines: Default::default(), @@ -100,23 +100,43 @@ 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 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 line.stroke.color == Color32::TRANSPARENT { + line.stroke.color = self.auto_color(); } + self.items.push(Box::new(line)); + + self } - /// 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); - self.curves.push(curve); + /// 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; + }; + + // Give the points an automatic color if no color has been assigned. + if points.color == Color32::TRANSPARENT { + points.color = self.auto_color(); } + self.items.push(Box::new(points)); + self } @@ -124,7 +144,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 } @@ -133,7 +155,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 } @@ -238,7 +262,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 @@ -250,7 +274,7 @@ impl Widget for Plot { let Self { name, next_auto_color_idx: _, - mut curves, + mut items, hlines, vlines, center_x_axis, @@ -276,14 +300,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 @@ -324,23 +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(); - curves + let neutral_color = ui.visuals().noninteractive().fg_stroke.color; + items .iter() - .filter(|curve| !curve.name.is_empty()) - .for_each(|curve| { - let checked = !hidden_curves.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(); legend_entries - .entry(curve.name.clone()) + .entry(item.name().to_string()) .and_modify(|entry| { - if entry.color != curve.stroke.color { - entry.color = ui.visuals().noninteractive().fg_stroke.color + if entry.color != item.color() { + entry.color = neutral_color } }) - .or_insert_with(|| LegendEntry::new(text, curve.stroke.color, checked)); + .or_insert_with(|| { + LegendEntry::new(text.to_string(), item.color(), checked) + }); }); // Show the legend. @@ -353,28 +380,27 @@ 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) .for_each(|entry| { - curves - .iter_mut() - .filter(|curve| curve.name == entry.text) - .for_each(|curve| { - curve.stroke.width *= 2.0; - }); + items.iter_mut().for_each(|item| { + if item.name() == entry.text { + item.highlight(); + } + }); }); - // Remove deselected curves. - curves.retain(|curve| !hidden_curves.contains(&curve.name)); + // Remove deselected items. + items.retain(|item| !hidden_items.contains(item.name())); } // --- @@ -386,7 +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.iter().for_each(|curve| bounds.merge(&curve.bounds)); + items + .iter() + .for_each(|item| bounds.merge(&item.series().get_bounds())); bounds.add_relative_margin(margin_fraction); } // Make sure they are not empty. @@ -437,14 +465,15 @@ impl Widget for Plot { } // Initialize values from functions. - curves - .iter_mut() - .for_each(|curve| curve.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, + items, hlines, vlines, show_x, @@ -458,7 +487,7 @@ impl Widget for Plot { PlotMemory { bounds, auto_bounds, - hidden_curves, + hidden_items, }, ); @@ -471,7 +500,7 @@ impl Widget for Plot { } struct Prepared { - curves: Vec, + items: Vec>, hlines: Vec, vlines: Vec, show_x: bool, @@ -480,15 +509,15 @@ struct Prepared { } impl Prepared { - fn ui(&self, ui: &mut Ui, response: &Response) { - let Self { transform, .. } = self; - + fn ui(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 = [ @@ -507,22 +536,8 @@ impl Prepared { shapes.push(Shape::line_segment(points, stroke)); } - 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) - } else { - Shape::line( - values - .iter() - .map(|v| transform.position_from_value(v)) - .collect(), - stroke, - ) - }; - shapes.push(shape); + for item in &self.items { + item.get_shapes(transform, &mut shapes); } if let Some(pointer) = response.hover_pos() { @@ -626,7 +641,7 @@ impl Prepared { transform, show_x, show_y, - curves, + items, .. } = self; @@ -636,24 +651,24 @@ 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.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_curve = Some(curve); + closest_item = Some(item.name()); } } } 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); } } diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index df457cf3d2e..0b59fd08551 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, Plot, Value}; +use egui::plot::{Line, MarkerShape, Plot, Points, Value, Values}; use egui::*; use std::f64::consts::TAU; #[derive(PartialEq)] -pub struct PlotDemo { +struct LineDemo { animate: bool, time: f64, circle_radius: f64, @@ -13,7 +13,7 @@ pub struct PlotDemo { proportional: bool, } -impl Default for PlotDemo { +impl Default for LineDemo { fn default() -> Self { Self { animate: true, @@ -27,29 +27,8 @@ impl Default for PlotDemo { } } -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 PlotDemo { +impl LineDemo { 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: _, @@ -58,6 +37,7 @@ impl PlotDemo { square, legend, proportional, + .. } = self; ui.horizontal(|ui| { @@ -88,25 +68,14 @@ impl PlotDemo { 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) -> 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); @@ -116,55 +85,192 @@ impl PlotDemo { r * t.sin() + self.circle_center.y as f64, ) }); - Curve::from_values_iter(circle) + Line::new(Values::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::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, - ) + )) .color(Color32::from_rgb(200, 100, 100)) .name("wave") } - fn thingy(&self) -> Curve { + fn thingy(&self) -> Line { let time = self.time; - Curve::from_parametric_callback( + Line::new(Values::from_parametric_callback( move |t| ((2.0 * t + time).sin(), (3.0 * t).sin()), 0.0..=TAU, - 512, - ) + 256, + )) .color(Color32::from_rgb(100, 150, 250)) .name("x = sin(2t), y = sin(3t)") } } -impl super::View for PlotDemo { - fn ui(&mut self, ui: &mut Ui) { +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("Demo Plot") - .curve(self.circle()) - .curve(self.sin()) - .curve(self.thingy()) - .show_legend(self.legend) - .min_size(Vec2::new(200.0, 200.0)); + let mut plot = Plot::new("Lines Demo") + .line(self.circle()) + .line(self.sin()) + .line(self.thingy()) + .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); + ui.add(plot) + } +} + +#[derive(PartialEq)] +struct MarkerDemo { + fill_markers: bool, + marker_radius: f32, + custom_marker_color: bool, + marker_color: Color32, +} + +impl Default for MarkerDemo { + fn default() -> Self { + Self { + fill_markers: true, + marker_radius: 5.0, + custom_marker_color: false, + marker_color: Color32::GRAY, + } + } +} + +impl MarkerDemo { + fn markers(&self) -> Vec { + MarkerShape::all() + .into_iter() + .enumerate() + .map(|(i, marker)| { + let y_offset = i as f32 * 0.5 + 1.0; + 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), + 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(format!("{:?}", marker)) + .filled(self.fill_markers) + .radius(self.marker_radius) + .shape(marker); + + if self.custom_marker_color { + points = points.color(self.marker_color); + } + + points + }) + .collect() + } +} + +impl Widget for &mut MarkerDemo { + fn ui(self, ui: &mut Ui) -> Response { + 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 mut markers_plot = Plot::new("Markers Demo").data_aspect(1.0); + for marker in self.markers() { + markers_plot = markers_plot.points(marker); + } + ui.add(markers_plot) + } +} + +#[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 { + 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| { + 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.open_panel, Panel::Lines, "Lines"); + ui.selectable_value(&mut self.open_panel, Panel::Markers, "Markers"); + }); + ui.separator(); + + 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 d8ff190a105..7570c9e2176 100644 --- a/egui_demo_lib/src/apps/demo/widget_gallery.rs +++ b/egui_demo_lib/src/apps/demo/widget_gallery.rs @@ -205,14 +205,15 @@ impl WidgetGallery { } fn example_plot() -> egui::plot::Plot { + use egui::plot::{Line, Plot, Value, Values}; let n = 128; - let curve = egui::plot::Curve::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()) - })); - egui::plot::Plot::new("Example Plot") - .curve(curve) + Value::new(x, x.sin()) + }))); + Plot::new("Example Plot") + .line(line) .height(32.0) .data_aspect(1.0) }