Browse Source

Improve plot item UX (#1816)

* initial work

* changelog entry

* fix CI

* Update egui/src/widgets/plot/items/values.rs

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>

* Update egui/src/widgets/plot/items/values.rs

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>

* 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 <emil.ernerfeldt@gmail.com>

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
pull/1849/head
Sven Niederberger 2 years ago
committed by GitHub
parent
commit
0bf9fc9428
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 2
      Cargo.lock
  3. 2
      eframe/src/epi.rs
  4. 8
      egui/src/widgets/plot/items/bar.rs
  5. 14
      egui/src/widgets/plot/items/box_elem.rs
  6. 108
      egui/src/widgets/plot/items/mod.rs
  7. 20
      egui/src/widgets/plot/items/rect_elem.rs
  8. 155
      egui/src/widgets/plot/items/values.rs
  9. 46
      egui/src/widgets/plot/mod.rs
  10. 27
      egui/src/widgets/plot/transform.rs
  11. 2
      egui_demo_app/Cargo.toml
  12. 24
      egui_demo_lib/src/demo/context_menu.rs
  13. 112
      egui_demo_lib/src/demo/plot_demo.rs
  14. 15
      egui_demo_lib/src/demo/widget_gallery.rs

1
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<dyn Any>` 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)).

2
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",
]

2
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 {

8
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<Value> {
fn values_with_ruler(&self) -> Vec<PlotPoint> {
let base = self.base_offset.unwrap_or(0.0);
let value_center = self.point_at(self.argument, base + self.value);

14
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<Value> {
fn values_with_ruler(&self) -> Vec<PlotPoint> {
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)
}

108
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<PlotPoints>) -> 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<PlotPoints>) -> 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<WidgetText>) -> Self {
pub fn new(position: PlotPoint, text: impl Into<WidgetText>) -> 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<PlotPoints>) -> 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<PlotPoints>, tips: impl Into<PlotPoints>) -> 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<TextureId>,
center_position: Value,
center_position: PlotPoint,
size: impl Into<Vec2>,
) -> 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<Shape>| {
let position = plot.transform.position_from_value(&argument);
let push_argument_ruler = |argument: PlotPoint, shapes: &mut Vec<Shape>| {
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<Shape>| {
let position = plot.transform.position_from_value(&value);
let push_value_ruler = |value: PlotPoint, shapes: &mut Vec<Shape>| {
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<Shape>,

20
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<Value> {
fn arguments_with_ruler(&self) -> Vec<PlotPoint> {
// 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<Value>;
fn values_with_ruler(&self) -> Vec<PlotPoint>;
/// 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,
}

155
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<f64>, y: impl Into<f64>) -> Self {
Self {
@ -140,22 +147,49 @@ impl Default for Orientation {
// ----------------------------------------------------------------------------
#[derive(Default)]
pub struct Values {
pub(super) values: Vec<Value>,
generator: Option<ExplicitGenerator>,
/// Represents many [`PlotPoint`]s.
///
/// These can be an owned `Vec` or generated with a function.
pub enum PlotPoints {
Owned(Vec<PlotPoint>),
Generator(ExplicitGenerator),
// Borrowed(&[PlotPoint]), // TODO: Lifetimes are tricky in this case.
}
impl Values {
pub fn from_values(values: Vec<Value>) -> 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<Vec<[f64; 2]>> for PlotPoints {
fn from(coordinates: Vec<[f64; 2]>) -> Self {
Self::new(coordinates)
}
}
impl FromIterator<[f64; 2]> for PlotPoints {
fn from_iter<T: IntoIterator<Item = [f64; 2]>>(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<Item = Value>) -> 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<Value> = 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<f64>) {
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<dyn Fn(f64) -> f64>,
x_range: RangeInclusive<f64>,
points: usize,

46
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<Box<LabelFormatterFn>>;
type AxisFormatterFn = dyn Fn(f64, &RangeInclusive<f64>) -> String;
type AxisFormatter = Option<Box<AxisFormatterFn>>;
@ -32,12 +32,12 @@ type GridSpacer = Box<GridSpacerFn>;
/// Specifies the coordinates formatting when passed to [`Plot::coordinates_formatter`].
pub struct CoordinatesFormatter {
function: Box<dyn Fn(&Value, &PlotBounds) -> String>,
function: Box<dyn Fn(&PlotPoint, &PlotBounds) -> 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<Value> {
pub fn pointer_coordinate(&self) -> Option<PlotPoint> {
// 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(

27
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);

2
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"] }

24
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::<PlotPoints>(),
);
egui::plot::Plot::new("example_plot")
.show_axes(self.show_axes)
.allow_drag(self.allow_drag)

112
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(),

15
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)

Loading…
Cancel
Save