From 0bf9fc9428423b375ae079782c095504e85ca42c Mon Sep 17 00:00:00 2001 From: Sven Niederberger Date: Sun, 24 Jul 2022 17:13:12 +0200 Subject: [PATCH] Improve plot item UX (#1816) * initial work * changelog entry * fix CI * Update egui/src/widgets/plot/items/values.rs Co-authored-by: Emil Ernerfeldt * Update egui/src/widgets/plot/items/values.rs Co-authored-by: Emil Ernerfeldt * derive 'FromIterator' * remove `bytemuck` dependency again and remove borrowing plot points for now * update doctest * update documentation * remove unnecessary numeric cast * cargo fmt * Update egui/src/widgets/plot/items/values.rs Co-authored-by: Emil Ernerfeldt Co-authored-by: Emil Ernerfeldt --- CHANGELOG.md | 1 + Cargo.lock | 2 +- eframe/src/epi.rs | 2 +- egui/src/widgets/plot/items/bar.rs | 8 +- egui/src/widgets/plot/items/box_elem.rs | 14 +- egui/src/widgets/plot/items/mod.rs | 108 ++++++++-------- egui/src/widgets/plot/items/rect_elem.rs | 20 +-- egui/src/widgets/plot/items/values.rs | 155 ++++++++++++++--------- egui/src/widgets/plot/mod.rs | 46 +++---- egui/src/widgets/plot/transform.rs | 27 ++-- egui_demo_app/Cargo.toml | 2 +- egui_demo_lib/src/demo/context_menu.rs | 24 ++-- egui_demo_lib/src/demo/plot_demo.rs | 112 +++++++++------- egui_demo_lib/src/demo/widget_gallery.rs | 15 ++- 14 files changed, 302 insertions(+), 234 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fec0e8e3f..2c8aabf2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w * `PaintCallback` shapes now require the whole callback to be put in an `Arc` with the value being a backend-specific callback type ([#1684](https://github.com/emilk/egui/pull/1684)). * Replaced `needs_repaint` in `FullOutput` with `repaint_after`. Used to force repaint after the set duration in reactive mode ([#1694](https://github.com/emilk/egui/pull/1694)). * `Layout::left_to_right` and `Layout::right_to_left` now takes the vertical align as an argument. Previous default was `Align::Center`. +* Improved ergonomics of adding plot items. All plot items that take a series of 2D coordinates can now be created directly from `Vec<[f64; 2]>`. The `Value` and `Values` types were removed in favor of `PlotPoint` and `PlotPoints` respectively. ### Fixed 🐛 * Fixed `Response::changed` for `ui.toggle_value` ([#1573](https://github.com/emilk/egui/pull/1573)). diff --git a/Cargo.lock b/Cargo.lock index 9bc2224b3..2230f3014 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3871,7 +3871,7 @@ version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "static_assertions", ] diff --git a/eframe/src/epi.rs b/eframe/src/epi.rs index 805efd4a1..88f48d8f7 100644 --- a/eframe/src/epi.rs +++ b/eframe/src/epi.rs @@ -374,7 +374,7 @@ impl Theme { // ---------------------------------------------------------------------------- -/// `WebGl` Context options +/// WebGL Context options #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum WebGlContextOption { diff --git a/egui/src/widgets/plot/items/bar.rs b/egui/src/widgets/plot/items/bar.rs index 39f3a91fb..6d27d1285 100644 --- a/egui/src/widgets/plot/items/bar.rs +++ b/egui/src/widgets/plot/items/bar.rs @@ -2,7 +2,7 @@ use crate::emath::NumExt; use crate::epaint::{Color32, RectShape, Rounding, Shape, Stroke}; use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement}; -use crate::plot::{BarChart, ScreenTransform, Value}; +use crate::plot::{BarChart, PlotPoint, ScreenTransform}; /// One bar in a [`BarChart`]. Potentially floating, allowing stacked bar charts. /// Width can be changed to allow variable-width histograms. @@ -157,15 +157,15 @@ impl RectElement for Bar { self.name.as_str() } - fn bounds_min(&self) -> Value { + fn bounds_min(&self) -> PlotPoint { self.point_at(self.argument - self.bar_width / 2.0, self.lower()) } - fn bounds_max(&self) -> Value { + fn bounds_max(&self) -> PlotPoint { self.point_at(self.argument + self.bar_width / 2.0, self.upper()) } - fn values_with_ruler(&self) -> Vec { + fn values_with_ruler(&self) -> Vec { let base = self.base_offset.unwrap_or(0.0); let value_center = self.point_at(self.argument, base + self.value); diff --git a/egui/src/widgets/plot/items/box_elem.rs b/egui/src/widgets/plot/items/box_elem.rs index 7cda17c85..985df8eeb 100644 --- a/egui/src/widgets/plot/items/box_elem.rs +++ b/egui/src/widgets/plot/items/box_elem.rs @@ -2,7 +2,7 @@ use crate::emath::NumExt; use crate::epaint::{Color32, RectShape, Rounding, Shape, Stroke}; use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement}; -use crate::plot::{BoxPlot, ScreenTransform, Value}; +use crate::plot::{BoxPlot, PlotPoint, ScreenTransform}; /// Contains the values of a single box in a box plot. #[derive(Clone, Debug, PartialEq)] @@ -161,8 +161,8 @@ impl BoxElem { let line_between = |v1, v2| { Shape::line_segment( [ - transform.position_from_value(&v1), - transform.position_from_value(&v2), + transform.position_from_point(&v1), + transform.position_from_point(&v2), ], stroke, ) @@ -236,19 +236,19 @@ impl RectElement for BoxElem { self.name.as_str() } - fn bounds_min(&self) -> Value { + fn bounds_min(&self) -> PlotPoint { let argument = self.argument - self.box_width.max(self.whisker_width) / 2.0; let value = self.spread.lower_whisker; self.point_at(argument, value) } - fn bounds_max(&self) -> Value { + fn bounds_max(&self) -> PlotPoint { let argument = self.argument + self.box_width.max(self.whisker_width) / 2.0; let value = self.spread.upper_whisker; self.point_at(argument, value) } - fn values_with_ruler(&self) -> Vec { + fn values_with_ruler(&self) -> Vec { let median = self.point_at(self.argument, self.spread.median); let q1 = self.point_at(self.argument, self.spread.quartile1); let q3 = self.point_at(self.argument, self.spread.quartile3); @@ -262,7 +262,7 @@ impl RectElement for BoxElem { self.orientation } - fn corner_value(&self) -> Value { + fn corner_value(&self) -> PlotPoint { self.point_at(self.argument, self.spread.upper_whisker) } diff --git a/egui/src/widgets/plot/items/mod.rs b/egui/src/widgets/plot/items/mod.rs index 59fdaa1e2..ffb1bd5a7 100644 --- a/egui/src/widgets/plot/items/mod.rs +++ b/egui/src/widgets/plot/items/mod.rs @@ -13,7 +13,7 @@ use values::{ClosestElem, PlotGeometry}; pub use bar::Bar; pub use box_elem::{BoxElem, BoxSpread}; -pub use values::{LineStyle, MarkerShape, Orientation, Value, Values}; +pub use values::{LineStyle, MarkerShape, Orientation, PlotPoint, PlotPoints}; mod bar; mod box_elem; @@ -49,7 +49,7 @@ pub(super) trait PlotItem { .iter() .enumerate() .map(|(index, value)| { - let pos = transform.position_from_value(value); + let pos = transform.position_from_point(value); let dist_sq = point.distance_sq(pos); ClosestElem { index, dist_sq } }) @@ -86,7 +86,7 @@ pub(super) trait PlotItem { // this method is only called, if the value is in the result set of find_closest() let value = points[elem.index]; - let pointer = plot.transform.position_from_value(&value); + let pointer = plot.transform.position_from_point(&value); shapes.push(Shape::circle_filled(pointer, 3.0, line_color)); rulers_at_value(pointer, value, self.name(), plot, shapes, label_formatter); @@ -169,8 +169,8 @@ impl PlotItem for HLine { .. } = self; let points = vec![ - transform.position_from_value(&Value::new(transform.bounds().min[0], *y)), - transform.position_from_value(&Value::new(transform.bounds().max[0], *y)), + transform.position_from_point(&PlotPoint::new(transform.bounds().min[0], *y)), + transform.position_from_point(&PlotPoint::new(transform.bounds().max[0], *y)), ]; style.style_line(points, *stroke, *highlight, shapes); } @@ -279,8 +279,8 @@ impl PlotItem for VLine { .. } = self; let points = vec![ - transform.position_from_value(&Value::new(*x, transform.bounds().min[1])), - transform.position_from_value(&Value::new(*x, transform.bounds().max[1])), + transform.position_from_point(&PlotPoint::new(*x, transform.bounds().min[1])), + transform.position_from_point(&PlotPoint::new(*x, transform.bounds().max[1])), ]; style.style_line(points, *stroke, *highlight, shapes); } @@ -317,7 +317,7 @@ impl PlotItem for VLine { /// A series of values forming a path. pub struct Line { - pub(super) series: Values, + pub(super) series: PlotPoints, pub(super) stroke: Stroke, pub(super) name: String, pub(super) highlight: bool, @@ -326,9 +326,9 @@ pub struct Line { } impl Line { - pub fn new(series: Values) -> Self { + pub fn new(series: impl Into) -> Self { Self { - series, + series: series.into(), stroke: Stroke::new(1.0, Color32::TRANSPARENT), name: Default::default(), highlight: false, @@ -405,9 +405,9 @@ impl PlotItem for Line { } = self; let values_tf: Vec<_> = series - .values + .points() .iter() - .map(|v| transform.position_from_value(v)) + .map(|v| transform.position_from_point(v)) .collect(); let n_values = values_tf.len(); @@ -421,7 +421,7 @@ impl PlotItem for Line { fill_alpha = (2.0 * fill_alpha).at_most(1.0); } let y = transform - .position_from_value(&Value::new(0.0, y_reference)) + .position_from_point(&PlotPoint::new(0.0, y_reference)) .y; let fill_color = Rgba::from(stroke.color) .to_opaque() @@ -474,7 +474,7 @@ impl PlotItem for Line { } fn geometry(&self) -> PlotGeometry<'_> { - PlotGeometry::Points(&self.series.values) + PlotGeometry::Points(self.series.points()) } fn get_bounds(&self) -> PlotBounds { @@ -484,7 +484,7 @@ impl PlotItem for Line { /// A convex polygon. pub struct Polygon { - pub(super) series: Values, + pub(super) series: PlotPoints, pub(super) stroke: Stroke, pub(super) name: String, pub(super) highlight: bool, @@ -493,9 +493,9 @@ pub struct Polygon { } impl Polygon { - pub fn new(series: Values) -> Self { + pub fn new(series: impl Into) -> Self { Self { - series, + series: series.into(), stroke: Stroke::new(1.0, Color32::TRANSPARENT), name: Default::default(), highlight: false, @@ -570,9 +570,9 @@ impl PlotItem for Polygon { } let mut values_tf: Vec<_> = series - .values + .points() .iter() - .map(|v| transform.position_from_value(v)) + .map(|v| transform.position_from_point(v)) .collect(); let fill = Rgba::from(stroke.color).to_opaque().multiply(fill_alpha); @@ -604,7 +604,7 @@ impl PlotItem for Polygon { } fn geometry(&self) -> PlotGeometry<'_> { - PlotGeometry::Points(&self.series.values) + PlotGeometry::Points(self.series.points()) } fn get_bounds(&self) -> PlotBounds { @@ -616,7 +616,7 @@ impl PlotItem for Polygon { #[derive(Clone)] pub struct Text { pub(super) text: WidgetText, - pub(super) position: Value, + pub(super) position: PlotPoint, pub(super) name: String, pub(super) highlight: bool, pub(super) color: Color32, @@ -624,7 +624,7 @@ pub struct Text { } impl Text { - pub fn new(position: Value, text: impl Into) -> Self { + pub fn new(position: PlotPoint, text: impl Into) -> Self { Self { text: text.into(), position, @@ -679,7 +679,7 @@ impl PlotItem for Text { .clone() .into_galley(ui, Some(false), f32::INFINITY, TextStyle::Small); - let pos = transform.position_from_value(&self.position); + let pos = transform.position_from_point(&self.position); let rect = self .anchor .anchor_rect(Rect::from_min_size(pos, galley.size())); @@ -730,7 +730,7 @@ impl PlotItem for Text { /// A set of points. pub struct Points { - pub(super) series: Values, + pub(super) series: PlotPoints, pub(super) shape: MarkerShape, /// Color of the marker. `Color32::TRANSPARENT` means that it will be picked automatically. pub(super) color: Color32, @@ -744,9 +744,9 @@ pub struct Points { } impl Points { - pub fn new(series: Values) -> Self { + pub fn new(series: impl Into) -> Self { Self { - series, + series: series.into(), shape: MarkerShape::Circle, color: Color32::TRANSPARENT, filled: true, @@ -837,12 +837,12 @@ impl PlotItem for Points { stem_stroke.width *= 2.0; } - let y_reference = stems.map(|y| transform.position_from_value(&Value::new(0.0, y)).y); + let y_reference = stems.map(|y| transform.position_from_point(&PlotPoint::new(0.0, y)).y); series - .values + .points() .iter() - .map(|value| transform.position_from_value(value)) + .map(|value| transform.position_from_point(value)) .for_each(|center| { let tf = |dx: f32, dy: f32| -> Pos2 { center + radius * vec2(dx, dy) }; @@ -955,7 +955,7 @@ impl PlotItem for Points { } fn geometry(&self) -> PlotGeometry<'_> { - PlotGeometry::Points(&self.series.values) + PlotGeometry::Points(self.series.points()) } fn get_bounds(&self) -> PlotBounds { @@ -965,18 +965,18 @@ impl PlotItem for Points { /// A set of arrows. pub struct Arrows { - pub(super) origins: Values, - pub(super) tips: Values, + pub(super) origins: PlotPoints, + pub(super) tips: PlotPoints, pub(super) color: Color32, pub(super) name: String, pub(super) highlight: bool, } impl Arrows { - pub fn new(origins: Values, tips: Values) -> Self { + pub fn new(origins: impl Into, tips: impl Into) -> Self { Self { - origins, - tips, + origins: origins.into(), + tips: tips.into(), color: Color32::TRANSPARENT, name: Default::default(), highlight: false, @@ -1020,13 +1020,13 @@ impl PlotItem for Arrows { } = self; let stroke = Stroke::new(if *highlight { 2.0 } else { 1.0 }, *color); origins - .values + .points() .iter() - .zip(tips.values.iter()) + .zip(tips.points().iter()) .map(|(origin, tip)| { ( - transform.position_from_value(origin), - transform.position_from_value(tip), + transform.position_from_point(origin), + transform.position_from_point(tip), ) }) .for_each(|(origin, tip)| { @@ -1070,7 +1070,7 @@ impl PlotItem for Arrows { } fn geometry(&self) -> PlotGeometry<'_> { - PlotGeometry::Points(&self.origins.values) + PlotGeometry::Points(self.origins.points()) } fn get_bounds(&self) -> PlotBounds { @@ -1081,7 +1081,7 @@ impl PlotItem for Arrows { /// An image in the plot. #[derive(Clone)] pub struct PlotImage { - pub(super) position: Value, + pub(super) position: PlotPoint, pub(super) texture_id: TextureId, pub(super) uv: Rect, pub(super) size: Vec2, @@ -1095,7 +1095,7 @@ impl PlotImage { /// Create a new image with position and size in plot coordinates. pub fn new( texture_id: impl Into, - center_position: Value, + center_position: PlotPoint, size: impl Into, ) -> Self { Self { @@ -1160,16 +1160,16 @@ impl PlotItem for PlotImage { .. } = self; let rect = { - let left_top = Value::new( + let left_top = PlotPoint::new( position.x as f32 - size.x / 2.0, position.y as f32 - size.y / 2.0, ); - let right_bottom = Value::new( + let right_bottom = PlotPoint::new( position.x as f32 + size.x / 2.0, position.y as f32 + size.y / 2.0, ); - let left_top_tf = transform.position_from_value(&left_top); - let right_bottom_tf = transform.position_from_value(&right_bottom); + let left_top_tf = transform.position_from_point(&left_top); + let right_bottom_tf = transform.position_from_point(&right_bottom); Rect::from_two_pos(left_top_tf, right_bottom_tf) }; Image::new(*texture_id, *size) @@ -1210,11 +1210,11 @@ impl PlotItem for PlotImage { fn get_bounds(&self) -> PlotBounds { let mut bounds = PlotBounds::NOTHING; - let left_top = Value::new( + let left_top = PlotPoint::new( self.position.x as f32 - self.size.x / 2.0, self.position.y as f32 - self.size.y / 2.0, ); - let right_bottom = Value::new( + let right_bottom = PlotPoint::new( self.position.x as f32 + self.size.x / 2.0, self.position.y as f32 + self.size.y / 2.0, ); @@ -1586,8 +1586,8 @@ fn add_rulers_and_text( // Rulers for argument (usually vertical) if show_argument { - let push_argument_ruler = |argument: Value, shapes: &mut Vec| { - let position = plot.transform.position_from_value(&argument); + let push_argument_ruler = |argument: PlotPoint, shapes: &mut Vec| { + let position = plot.transform.position_from_point(&argument); let line = match orientation { Orientation::Horizontal => horizontal_line(position, plot.transform, line_color), Orientation::Vertical => vertical_line(position, plot.transform, line_color), @@ -1602,8 +1602,8 @@ fn add_rulers_and_text( // Rulers for values (usually horizontal) if show_values { - let push_value_ruler = |value: Value, shapes: &mut Vec| { - let position = plot.transform.position_from_value(&value); + let push_value_ruler = |value: PlotPoint, shapes: &mut Vec| { + let position = plot.transform.position_from_point(&value); let line = match orientation { Orientation::Horizontal => vertical_line(position, plot.transform, line_color), Orientation::Vertical => horizontal_line(position, plot.transform, line_color), @@ -1632,7 +1632,7 @@ fn add_rulers_and_text( let corner_value = elem.corner_value(); shapes.push(Shape::text( &*plot.ui.fonts(), - plot.transform.position_from_value(&corner_value) + vec2(3.0, -2.0), + plot.transform.position_from_point(&corner_value) + vec2(3.0, -2.0), Align2::LEFT_BOTTOM, text, font_id, @@ -1645,7 +1645,7 @@ fn add_rulers_and_text( #[allow(clippy::too_many_arguments)] pub(super) fn rulers_at_value( pointer: Pos2, - value: Value, + value: PlotPoint, name: &str, plot: &PlotConfig<'_>, shapes: &mut Vec, diff --git a/egui/src/widgets/plot/items/rect_elem.rs b/egui/src/widgets/plot/items/rect_elem.rs index 395b2ca48..fa9ce441a 100644 --- a/egui/src/widgets/plot/items/rect_elem.rs +++ b/egui/src/widgets/plot/items/rect_elem.rs @@ -1,4 +1,4 @@ -use super::{Orientation, Value}; +use super::{Orientation, PlotPoint}; use crate::plot::transform::{PlotBounds, ScreenTransform}; use epaint::emath::NumExt; use epaint::{Color32, Rgba, Stroke}; @@ -6,8 +6,8 @@ use epaint::{Color32, Rgba, Stroke}; /// Trait that abstracts from rectangular 'Value'-like elements, such as bars or boxes pub(super) trait RectElement { fn name(&self) -> &str; - fn bounds_min(&self) -> Value; - fn bounds_max(&self) -> Value; + fn bounds_min(&self) -> PlotPoint; + fn bounds_max(&self) -> PlotPoint; fn bounds(&self) -> PlotBounds { let mut bounds = PlotBounds::NOTHING; @@ -17,29 +17,29 @@ pub(super) trait RectElement { } /// At which argument (input; usually X) there is a ruler (usually vertical) - fn arguments_with_ruler(&self) -> Vec { + fn arguments_with_ruler(&self) -> Vec { // Default: one at center vec![self.bounds().center()] } /// At which value (output; usually Y) there is a ruler (usually horizontal) - fn values_with_ruler(&self) -> Vec; + fn values_with_ruler(&self) -> Vec; /// The diagram's orientation (vertical/horizontal) fn orientation(&self) -> Orientation; /// Get X/Y-value for (argument, value) pair, taking into account orientation - fn point_at(&self, argument: f64, value: f64) -> Value { + fn point_at(&self, argument: f64, value: f64) -> PlotPoint { match self.orientation() { - Orientation::Horizontal => Value::new(value, argument), - Orientation::Vertical => Value::new(argument, value), + Orientation::Horizontal => PlotPoint::new(value, argument), + Orientation::Vertical => PlotPoint::new(argument, value), } } /// Right top of the rectangle (position of text) - fn corner_value(&self) -> Value { + fn corner_value(&self) -> PlotPoint { //self.point_at(self.position + self.width / 2.0, value) - Value { + PlotPoint { x: self.bounds_max().x, y: self.bounds_max().y, } diff --git a/egui/src/widgets/plot/items/values.rs b/egui/src/widgets/plot/items/values.rs index 0a0f2d238..3cfb845d1 100644 --- a/egui/src/widgets/plot/items/values.rs +++ b/egui/src/widgets/plot/items/values.rs @@ -3,12 +3,12 @@ use std::ops::{Bound, RangeBounds, RangeInclusive}; use crate::plot::transform::PlotBounds; -/// A value in the value-space of the plot. +/// A point coordinate in the plot. /// /// Uses f64 for improved accuracy to enable plotting /// large values (e.g. unix time on x axis). #[derive(Clone, Copy, Debug, PartialEq)] -pub struct Value { +pub struct PlotPoint { /// This is often something monotonically increasing, such as time, but doesn't have to be. /// Goes from left to right. pub x: f64, @@ -16,7 +16,14 @@ pub struct Value { pub y: f64, } -impl Value { +impl From<[f64; 2]> for PlotPoint { + #[inline] + fn from([x, y]: [f64; 2]) -> Self { + Self { x, y } + } +} + +impl PlotPoint { #[inline(always)] pub fn new(x: impl Into, y: impl Into) -> Self { Self { @@ -140,22 +147,49 @@ impl Default for Orientation { // ---------------------------------------------------------------------------- -#[derive(Default)] -pub struct Values { - pub(super) values: Vec, - generator: Option, +/// Represents many [`PlotPoint`]s. +/// +/// These can be an owned `Vec` or generated with a function. +pub enum PlotPoints { + Owned(Vec), + Generator(ExplicitGenerator), + // Borrowed(&[PlotPoint]), // TODO: Lifetimes are tricky in this case. } -impl Values { - pub fn from_values(values: Vec) -> Self { - Self { - values, - generator: None, - } +impl Default for PlotPoints { + fn default() -> Self { + Self::Owned(Vec::new()) + } +} + +impl From<[f64; 2]> for PlotPoints { + fn from(coordinate: [f64; 2]) -> Self { + Self::new(vec![coordinate]) + } +} + +impl From> for PlotPoints { + fn from(coordinates: Vec<[f64; 2]>) -> Self { + Self::new(coordinates) + } +} + +impl FromIterator<[f64; 2]> for PlotPoints { + fn from_iter>(iter: T) -> Self { + Self::Owned(iter.into_iter().map(|point| point.into()).collect()) + } +} + +impl PlotPoints { + pub fn new(points: Vec<[f64; 2]>) -> Self { + Self::from_iter(points) } - pub fn from_values_iter(iter: impl Iterator) -> Self { - Self::from_values(iter.collect()) + pub fn points(&self) -> &[PlotPoint] { + match self { + PlotPoints::Owned(points) => points.as_slice(), + PlotPoints::Generator(_) => &[], + } } /// Draw a line based on a function `y=f(x)`, a range (which can be infinite) for x and the number of points. @@ -180,10 +214,7 @@ impl Values { points, }; - Self { - values: Vec::new(), - generator: Some(generator), - } + Self::Generator(generator) } /// Draw a line based on a function `(x,y)=f(t)`, a range for t and the number of points. @@ -208,48 +239,55 @@ impl Values { } else { (end - start) / points as f64 }; - let values = (0..points).map(|i| { - let t = start + i as f64 * increment; - let (x, y) = function(t); - Value { x, y } - }); - Self::from_values_iter(values) + (0..points) + .map(|i| { + let t = start + i as f64 * increment; + let (x, y) = function(t); + [x, y] + }) + .collect() } /// 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() + ys.iter() .enumerate() - .map(|(i, &y)| Value { - x: i as f64, - y: y as f64, - }) - .collect(); - Self::from_values(values) + .map(|(i, &y)| [i as f64, y as f64]) + .collect() + } + + /// From a series of y-values. + /// The x-values will be the indices of these values + pub fn from_ys_f64(ys: &[f64]) -> Self { + ys.iter().enumerate().map(|(i, &y)| [i as f64, y]).collect() } /// 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() + match self { + PlotPoints::Owned(points) => points.is_empty(), + PlotPoints::Generator(_) => false, + } } /// If initialized with a generator function, this will generate `n` evenly spaced points in the /// given range. 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 = - (intersection.end() - intersection.start()) / (generator.points - 1) as f64; - self.values = (0..generator.points) - .map(|i| { - let x = intersection.start() + i as f64 * increment; - let y = (generator.function)(x); - Value { x, y } - }) - .collect(); - } + if let Self::Generator(generator) = self { + *self = Self::range_intersection(&x_range, &generator.x_range) + .map(|intersection| { + let increment = + (intersection.end() - intersection.start()) / (generator.points - 1) as f64; + (0..generator.points) + .map(|i| { + let x = intersection.start() + i as f64 * increment; + let y = (generator.function)(x); + [x, y] + }) + .collect() + }) + .unwrap_or_default(); } } @@ -264,18 +302,15 @@ impl Values { } pub(super) fn get_bounds(&self) -> PlotBounds { - if self.values.is_empty() { - if let Some(generator) = &self.generator { - generator.estimate_bounds() - } else { - PlotBounds::NOTHING - } - } else { - let mut bounds = PlotBounds::NOTHING; - for value in &self.values { - bounds.extend_with(value); + match self { + PlotPoints::Owned(points) => { + let mut bounds = PlotBounds::NOTHING; + for point in points { + bounds.extend_with(point); + } + bounds } - bounds + PlotPoints::Generator(generator) => generator.estimate_bounds(), } } } @@ -318,13 +353,13 @@ impl MarkerShape { // ---------------------------------------------------------------------------- -/// Query the values of the plot, for geometric relations like closest checks +/// Query the points of the plot, for geometric relations like closest checks pub(crate) enum PlotGeometry<'a> { /// No geometry based on single elements (examples: text, image, horizontal/vertical line) None, /// Point values (X-Y graphs) - Points(&'a [Value]), + Points(&'a [PlotPoint]), /// Rectangles (examples: boxes or bars) // Has currently no data, as it would require copying rects or iterating a list of pointers. @@ -335,7 +370,7 @@ pub(crate) enum PlotGeometry<'a> { // ---------------------------------------------------------------------------- /// Describes a function y = f(x) with an optional range for x and a number of points. -struct ExplicitGenerator { +pub struct ExplicitGenerator { function: Box f64>, x_range: RangeInclusive, points: usize, diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 29da309ac..911d846da 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -13,7 +13,7 @@ use transform::ScreenTransform; pub use items::{ Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, HLine, Line, LineStyle, MarkerShape, - Orientation, PlotImage, Points, Polygon, Text, VLine, Value, Values, + Orientation, PlotImage, PlotPoint, PlotPoints, Points, Polygon, Text, VLine, }; pub use legend::{Corner, Legend}; pub use transform::PlotBounds; @@ -22,7 +22,7 @@ mod items; mod legend; mod transform; -type LabelFormatterFn = dyn Fn(&str, &Value) -> String; +type LabelFormatterFn = dyn Fn(&str, &PlotPoint) -> String; type LabelFormatter = Option>; type AxisFormatterFn = dyn Fn(f64, &RangeInclusive) -> String; type AxisFormatter = Option>; @@ -32,12 +32,12 @@ type GridSpacer = Box; /// Specifies the coordinates formatting when passed to [`Plot::coordinates_formatter`]. pub struct CoordinatesFormatter { - function: Box String>, + function: Box String>, } impl CoordinatesFormatter { /// Create a new formatter based on the pointer coordinate and the plot bounds. - pub fn new(function: impl Fn(&Value, &PlotBounds) -> String + 'static) -> Self { + pub fn new(function: impl Fn(&PlotPoint, &PlotBounds) -> String + 'static) -> Self { Self { function: Box::new(function), } @@ -52,7 +52,7 @@ impl CoordinatesFormatter { } } - fn format(&self, value: &Value, bounds: &PlotBounds) -> String { + fn format(&self, value: &PlotPoint, bounds: &PlotBounds) -> String { (self.function)(value, bounds) } } @@ -178,12 +178,12 @@ impl LinkedAxisGroup { /// /// ``` /// # egui::__run_test_ui(|ui| { -/// use egui::plot::{Line, Plot, Value, Values}; -/// let sin = (0..1000).map(|i| { +/// use egui::plot::{Line, Plot, PlotPoints}; +/// let sin: PlotPoints = (0..1000).map(|i| { /// let x = i as f64 * 0.01; -/// Value::new(x, x.sin()) -/// }); -/// let line = Line::new(Values::from_values_iter(sin)); +/// [x, x.sin()] +/// }).collect(); +/// let line = Line::new(sin); /// Plot::new("my_plot").view_aspect(2.0).show(ui, |plot_ui| plot_ui.line(line)); /// # }); /// ``` @@ -359,12 +359,12 @@ impl Plot { /// /// ``` /// # egui::__run_test_ui(|ui| { - /// use egui::plot::{Line, Plot, Value, Values}; - /// let sin = (0..1000).map(|i| { + /// use egui::plot::{Line, Plot, PlotPoints}; + /// let sin: PlotPoints = (0..1000).map(|i| { /// let x = i as f64 * 0.01; - /// Value::new(x, x.sin()) - /// }); - /// let line = Line::new(Values::from_values_iter(sin)); + /// [x, x.sin()] + /// }).collect(); + /// let line = Line::new(sin); /// Plot::new("my_plot").view_aspect(2.0) /// .label_formatter(|name, value| { /// if !name.is_empty() { @@ -378,7 +378,7 @@ impl Plot { /// ``` pub fn label_formatter( mut self, - label_formatter: impl Fn(&str, &Value) -> String + 'static, + label_formatter: impl Fn(&str, &PlotPoint) -> String + 'static, ) -> Self { self.label_formatter = Some(Box::new(label_formatter)); self @@ -892,7 +892,7 @@ impl PlotUi { } /// The pointer position in plot coordinates. Independent of whether the pointer is in the plot area. - pub fn pointer_coordinate(&self) -> Option { + pub fn pointer_coordinate(&self) -> Option { // We need to subtract the drag delta to keep in sync with the frame-delayed screen transform: let last_pos = self.ctx().input().pointer.latest_pos()? - self.response.drag_delta(); let value = self.plot_from_screen(last_pos); @@ -907,12 +907,12 @@ impl PlotUi { } /// Transform the plot coordinates to screen coordinates. - pub fn screen_from_plot(&self, position: Value) -> Pos2 { - self.last_screen_transform.position_from_value(&position) + pub fn screen_from_plot(&self, position: PlotPoint) -> Pos2 { + self.last_screen_transform.position_from_point(&position) } /// Transform the screen coordinates to plot coordinates. - pub fn plot_from_screen(&self, position: Pos2) -> Value { + pub fn plot_from_screen(&self, position: Pos2) -> PlotPoint { self.last_screen_transform.value_from_position(position) } @@ -1188,12 +1188,12 @@ impl PreparedPlot { let value_main = step.value; let value = if axis == 0 { - Value::new(value_main, value_cross) + PlotPoint::new(value_main, value_cross) } else { - Value::new(value_cross, value_main) + PlotPoint::new(value_cross, value_main) }; - let pos_in_gui = transform.position_from_value(&value); + let pos_in_gui = transform.position_from_point(&value); let spacing_in_points = (transform.dpos_dvalue()[axis] * step.step_size).abs() as f32; let line_alpha = remap_clamp( diff --git a/egui/src/widgets/plot/transform.rs b/egui/src/widgets/plot/transform.rs index 65544b768..b5eb0cb2a 100644 --- a/egui/src/widgets/plot/transform.rs +++ b/egui/src/widgets/plot/transform.rs @@ -1,6 +1,6 @@ use std::ops::RangeInclusive; -use super::items::Value; +use super::PlotPoint; use crate::*; /// 2D bounding box of f64 precision. @@ -52,15 +52,16 @@ impl PlotBounds { self.max[1] - self.min[1] } - pub fn center(&self) -> Value { - Value { - x: (self.min[0] + self.max[0]) / 2.0, - y: (self.min[1] + self.max[1]) / 2.0, - } + pub fn center(&self) -> PlotPoint { + [ + (self.min[0] + self.max[0]) / 2.0, + (self.min[1] + self.max[1]) / 2.0, + ] + .into() } /// Expand to include the given (x,y) value - pub(crate) fn extend_with(&mut self, value: &Value) { + pub(crate) fn extend_with(&mut self, value: &PlotPoint) { self.extend_with_x(value.x); self.extend_with_y(value.y); } @@ -236,7 +237,7 @@ impl ScreenTransform { } } - pub fn position_from_value(&self, value: &Value) -> Pos2 { + pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 { let x = remap( value.x, self.bounds.min[0]..=self.bounds.max[0], @@ -250,7 +251,7 @@ impl ScreenTransform { pos2(x as f32, y as f32) } - pub fn value_from_position(&self, pos: Pos2) -> Value { + pub fn value_from_position(&self, pos: Pos2) -> PlotPoint { let x = remap( pos.x as f64, (self.frame.left() as f64)..=(self.frame.right() as f64), @@ -261,16 +262,16 @@ impl ScreenTransform { (self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis! self.bounds.min[1]..=self.bounds.max[1], ); - Value::new(x, y) + PlotPoint::new(x, y) } /// Transform a rectangle of plot values to a screen-coordinate rectangle. /// /// This typically means that the rect is mirrored vertically (top becomes bottom and vice versa), /// since the plot's coordinate system has +Y up, while egui has +Y down. - pub fn rect_from_values(&self, value1: &Value, value2: &Value) -> Rect { - let pos1 = self.position_from_value(value1); - let pos2 = self.position_from_value(value2); + pub fn rect_from_values(&self, value1: &PlotPoint, value2: &PlotPoint) -> Rect { + let pos1 = self.position_from_point(value1); + let pos2 = self.position_from_point(value2); let mut rect = Rect::NOTHING; rect.extend_with(pos1); diff --git a/egui_demo_app/Cargo.toml b/egui_demo_app/Cargo.toml index a89550468..48f2ba17d 100644 --- a/egui_demo_app/Cargo.toml +++ b/egui_demo_app/Cargo.toml @@ -45,7 +45,7 @@ egui_demo_lib = { version = "0.18.0", path = "../egui_demo_lib", features = ["ch # Optional dependencies: -bytemuck = { version = "1.9.1", optional = true } +bytemuck = { version = "1.7.1", optional = true } egui_extras = { version = "0.18.0", optional = true, path = "../egui_extras" } wgpu = { version = "0.13", optional = true, features = ["webgl"] } diff --git a/egui_demo_lib/src/demo/context_menu.rs b/egui_demo_lib/src/demo/context_menu.rs index 65b532a21..bd951234b 100644 --- a/egui_demo_lib/src/demo/context_menu.rs +++ b/egui_demo_lib/src/demo/context_menu.rs @@ -116,17 +116,21 @@ impl super::View for ContextMenus { impl ContextMenus { fn example_plot(&self, ui: &mut egui::Ui) -> egui::Response { - use egui::plot::{Line, Value, Values}; + use egui::plot::{Line, PlotPoints}; let n = 128; - 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); - match self.plot { - Plot::Sin => Value::new(x, x.sin()), - Plot::Bell => Value::new(x, 10.0 * gaussian(x)), - Plot::Sigmoid => Value::new(x, sigmoid(x)), - } - }))); + let line = Line::new( + (0..=n) + .map(|i| { + use std::f64::consts::TAU; + let x = egui::remap(i as f64, 0.0..=n as f64, -TAU..=TAU); + match self.plot { + Plot::Sin => [x, x.sin()], + Plot::Bell => [x, 10.0 * gaussian(x)], + Plot::Sigmoid => [x, sigmoid(x)], + } + }) + .collect::(), + ); egui::plot::Plot::new("example_plot") .show_axes(self.show_axes) .allow_drag(self.allow_drag) diff --git a/egui_demo_lib/src/demo/plot_demo.rs b/egui_demo_lib/src/demo/plot_demo.rs index 9684c47dc..a751c2a86 100644 --- a/egui_demo_lib/src/demo/plot_demo.rs +++ b/egui_demo_lib/src/demo/plot_demo.rs @@ -5,8 +5,8 @@ use egui::plot::{GridInput, GridMark}; use egui::*; use plot::{ Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, CoordinatesFormatter, Corner, HLine, - Legend, Line, LineStyle, MarkerShape, Plot, PlotImage, Points, Polygon, Text, VLine, Value, - Values, + Legend, Line, LineStyle, MarkerShape, Plot, PlotImage, PlotPoint, PlotPoints, Points, Polygon, + Text, VLine, }; // ---------------------------------------------------------------------------- @@ -108,15 +108,17 @@ impl LineDemo { 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); - let r = self.circle_radius; - Value::new( - r * t.cos() + self.circle_center.x as f64, - r * t.sin() + self.circle_center.y as f64, - ) - }); - Line::new(Values::from_values_iter(circle)) + let circle_points: PlotPoints = (0..=n) + .map(|i| { + let t = remap(i as f64, 0.0..=(n as f64), 0.0..=TAU); + let r = self.circle_radius; + [ + r * t.cos() + self.circle_center.x as f64, + r * t.sin() + self.circle_center.y as f64, + ] + }) + .collect(); + Line::new(circle_points) .color(Color32::from_rgb(100, 200, 100)) .style(self.line_style) .name("circle") @@ -124,7 +126,7 @@ impl LineDemo { fn sin(&self) -> Line { let time = self.time; - Line::new(Values::from_explicit_callback( + Line::new(PlotPoints::from_explicit_callback( move |x| 0.5 * (2.0 * x).sin() * time.sin(), .., 512, @@ -136,7 +138,7 @@ impl LineDemo { fn thingy(&self) -> Line { let time = self.time; - Line::new(Values::from_parametric_callback( + Line::new(PlotPoints::from_parametric_callback( move |t| ((2.0 * t + time).sin(), (3.0 * t).sin()), 0.0..=TAU, 256, @@ -199,15 +201,15 @@ impl MarkerDemo { MarkerShape::all() .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), - ])) + let y_offset = i as f64 * 0.5 + 1.0; + let mut points = Points::new(vec![ + [1.0, 0.0 + y_offset], + [2.0, 0.5 + y_offset], + [3.0, 0.0 + y_offset], + [4.0, 0.5 + y_offset], + [5.0, 0.0 + y_offset], + [6.0, 0.5 + y_offset], + ]) .name(format!("{:?}", marker)) .filled(self.fill_markers) .radius(self.marker_radius) @@ -259,13 +261,25 @@ struct LegendDemo { impl LegendDemo { fn line_with_slope(slope: f64) -> Line { - Line::new(Values::from_explicit_callback(move |x| slope * x, .., 100)) + Line::new(PlotPoints::from_explicit_callback( + move |x| slope * x, + .., + 100, + )) } fn sin() -> Line { - Line::new(Values::from_explicit_callback(move |x| x.sin(), .., 100)) + Line::new(PlotPoints::from_explicit_callback( + move |x| x.sin(), + .., + 100, + )) } fn cos() -> Line { - Line::new(Values::from_explicit_callback(move |x| x.cos(), .., 100)) + Line::new(PlotPoints::from_explicit_callback( + move |x| x.cos(), + .., + 100, + )) } fn ui(&mut self, ui: &mut Ui) -> Response { @@ -327,7 +341,7 @@ impl CustomAxisDemo { CustomAxisDemo::MINS_PER_DAY * min } - let values = Values::from_explicit_callback( + let values = PlotPoints::from_explicit_callback( move |x| 1.0 / (1.0 + (-2.5 * (x / CustomAxisDemo::MINS_PER_DAY - 2.0)).exp()), days(0.0)..days(5.0), 100, @@ -410,7 +424,7 @@ impl CustomAxisDemo { } }; - let label_fmt = |_s: &str, val: &Value| { + let label_fmt = |_s: &str, val: &PlotPoint| { format!( "Day {d}, {h}:{m:02}\n{p:.2}%", d = get_day(val.x), @@ -458,13 +472,25 @@ impl Default for LinkedAxisDemo { impl LinkedAxisDemo { fn line_with_slope(slope: f64) -> Line { - Line::new(Values::from_explicit_callback(move |x| slope * x, .., 100)) + Line::new(PlotPoints::from_explicit_callback( + move |x| slope * x, + .., + 100, + )) } fn sin() -> Line { - Line::new(Values::from_explicit_callback(move |x| x.sin(), .., 100)) + Line::new(PlotPoints::from_explicit_callback( + move |x| x.sin(), + .., + 100, + )) } fn cos() -> Line { - Line::new(Values::from_explicit_callback(move |x| x.cos(), .., 100)) + Line::new(PlotPoints::from_explicit_callback( + move |x| x.cos(), + .., + 100, + )) } fn configure_plot(plot_ui: &mut plot::PlotUi) { @@ -519,28 +545,26 @@ impl ItemsDemo { let n = 100; let mut sin_values: Vec<_> = (0..=n) .map(|i| remap(i as f64, 0.0..=n as f64, -TAU..=TAU)) - .map(|i| Value::new(i, i.sin())) + .map(|i| [i, i.sin()]) .collect(); - let line = Line::new(Values::from_values(sin_values.split_off(n / 2))).fill(-1.5); - let polygon = Polygon::new(Values::from_parametric_callback( + let line = Line::new(sin_values.split_off(n / 2)).fill(-1.5); + let polygon = Polygon::new(PlotPoints::from_parametric_callback( |t| (4.0 * t.sin() + 2.0 * t.cos(), 4.0 * t.cos() + 2.0 * t.sin()), 0.0..TAU, 100, )); - let points = Points::new(Values::from_values(sin_values)) - .stems(-1.5) - .radius(1.0); + let points = Points::new(sin_values).stems(-1.5).radius(1.0); let arrows = { let pos_radius = 8.0; let tip_radius = 7.0; - let arrow_origins = Values::from_parametric_callback( + let arrow_origins = PlotPoints::from_parametric_callback( |t| (pos_radius * t.sin(), pos_radius * t.cos()), 0.0..TAU, 36, ); - let arrow_tips = Values::from_parametric_callback( + let arrow_tips = PlotPoints::from_parametric_callback( |t| (tip_radius * t.sin(), tip_radius * t.cos()), 0.0..TAU, 36, @@ -557,7 +581,7 @@ impl ItemsDemo { }); let image = PlotImage::new( texture, - Value::new(0.0, 10.0), + PlotPoint::new(0.0, 10.0), 5.0 * vec2(texture.aspect_ratio(), 1.0), ); @@ -574,10 +598,10 @@ impl ItemsDemo { plot_ui.line(line.name("Line with fill")); plot_ui.polygon(polygon.name("Convex polygon")); plot_ui.points(points.name("Points with stems")); - plot_ui.text(Text::new(Value::new(-3.0, -3.0), "wow").name("Text")); - plot_ui.text(Text::new(Value::new(-2.0, 2.5), "so graph").name("Text")); - plot_ui.text(Text::new(Value::new(3.0, 3.0), "much color").name("Text")); - plot_ui.text(Text::new(Value::new(2.5, -2.0), "such plot").name("Text")); + plot_ui.text(Text::new(PlotPoint::new(-3.0, -3.0), "wow").name("Text")); + plot_ui.text(Text::new(PlotPoint::new(-2.0, 2.5), "so graph").name("Text")); + plot_ui.text(Text::new(PlotPoint::new(3.0, 3.0), "much color").name("Text")); + plot_ui.text(Text::new(PlotPoint::new(2.5, -2.0), "such plot").name("Text")); plot_ui.image(image.name("Image")); plot_ui.arrows(arrows.name("Arrows")); }) @@ -600,7 +624,7 @@ impl InteractionDemo { inner: (screen_pos, pointer_coordinate, pointer_coordinate_drag_delta, bounds, hovered), } = plot.show(ui, |plot_ui| { ( - plot_ui.screen_from_plot(Value::new(0.0, 0.0)), + plot_ui.screen_from_plot(PlotPoint::new(0.0, 0.0)), plot_ui.pointer_coordinate(), plot_ui.pointer_coordinate_drag_delta(), plot_ui.plot_bounds(), diff --git a/egui_demo_lib/src/demo/widget_gallery.rs b/egui_demo_lib/src/demo/widget_gallery.rs index f1e9d5fb8..419b95c58 100644 --- a/egui_demo_lib/src/demo/widget_gallery.rs +++ b/egui_demo_lib/src/demo/widget_gallery.rs @@ -260,13 +260,16 @@ impl WidgetGallery { } fn example_plot(ui: &mut egui::Ui) -> egui::Response { - use egui::plot::{Line, Value, Values}; + use egui::plot::{Line, PlotPoints}; let n = 128; - 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); - Value::new(x, x.sin()) - }))); + let line_points: PlotPoints = (0..=n) + .map(|i| { + use std::f64::consts::TAU; + let x = egui::remap(i as f64, 0.0..=n as f64, -TAU..=TAU); + [x, x.sin()] + }) + .collect(); + let line = Line::new(line_points); egui::plot::Plot::new("example_plot") .height(32.0) .data_aspect(1.0)