Plotting: Add line markers (#363)

* initial work on markers

* clippy fix

* simplify marker

* use option for color

* prepare for more demo plots

* more improvements for markers

* some small adjustments

* better highlighting

* don't draw transparent lines

* use transparent color instead of option

* don't brighten curves when highlighting

* update changelog

* avoid allocations and use line_segment

* compare against transparent color

* create new Points primitive

* fix doctest

* some cleanup and fix hover

* common interface for lines and points

* clippy fixes

* reduce visibilities

* Update egui/src/widgets/plot/mod.rs

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

* Update egui/src/widgets/plot/mod.rs

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

* Update egui_demo_lib/src/apps/demo/plot_demo.rs

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

* Update egui_demo_lib/src/apps/demo/plot_demo.rs

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

* changes based on review

* fix test

* dynamic plot size

* remove height

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
Sven Niederberger 2021-05-27 18:40:20 +02:00 committed by GitHub
parent e320ef6c64
commit 8623909d82
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 663 additions and 206 deletions

View file

@ -8,6 +8,7 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [
## Unreleased ## Unreleased
### Added ⭐ ### Added ⭐
* [Line markers for plots](https://github.com/emilk/egui/pull/363).
* Add right and bottom panels (`SidePanel::right` and `Panel::bottom`). * Add right and bottom panels (`SidePanel::right` and `Panel::bottom`).
* Add resizable panels. * Add resizable panels.
* Add an option to overwrite frame of a `Panel`. * 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`. * `TextEdit` now supports edits on a generic buffer using `TextBuffer`.
### Changed 🔧 ### Changed 🔧
* Plot: Changed `Curve` to `Line`.
* `TopPanel::top` is now `TopBottomPanel::top`. * `TopPanel::top` is now `TopBottomPanel::top`.
* `SidePanel::left` no longet takes the default width by argument, but by a builder call. * `SidePanel::left` no longet takes the default width by argument, but by a builder call.

View file

@ -2,7 +2,7 @@
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use super::transform::Bounds; use super::transform::{Bounds, ScreenTransform};
use crate::*; use crate::*;
/// A value in the value-space of the plot. /// 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 /// A horizontal line in a plot, filling the full width
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct HLine { pub struct HLine {
pub(crate) y: f64, pub(super) y: f64,
pub(crate) stroke: Stroke, pub(super) stroke: Stroke,
} }
impl HLine { impl HLine {
@ -49,8 +49,8 @@ impl HLine {
/// A vertical line in a plot, filling the full width /// A vertical line in a plot, filling the full width
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct VLine { pub struct VLine {
pub(crate) x: f64, pub(super) x: f64,
pub(crate) stroke: Stroke, pub(super) stroke: Stroke,
} }
impl VLine { impl VLine {
@ -62,6 +62,15 @@ impl VLine {
} }
} }
pub(super) trait PlotItem {
fn get_shapes(&self, transform: &ScreenTransform, shapes: &mut Vec<Shape>);
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. /// 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, points: usize,
} }
// ---------------------------------------------------------------------------- pub struct Values {
pub(super) values: Vec<Value>,
/// A series of values forming a path.
pub struct Curve {
pub(crate) values: Vec<Value>,
generator: Option<ExplicitGenerator>, generator: Option<ExplicitGenerator>,
pub(crate) bounds: Bounds,
pub(crate) stroke: Stroke,
pub(crate) name: String,
} }
impl Curve { impl Default for Values {
fn empty() -> Self { fn default() -> Self {
Self { Self {
values: Vec::new(), values: Vec::new(),
generator: None, generator: None,
bounds: Bounds::NOTHING, }
stroke: Stroke::new(2.0, Color32::TRANSPARENT),
name: Default::default(),
} }
} }
impl Values {
pub fn from_values(values: Vec<Value>) -> Self { pub fn from_values(values: Vec<Value>) -> Self {
let mut bounds = Bounds::NOTHING;
for value in &values {
bounds.extend_with(value);
}
Self { Self {
values, values,
bounds, generator: None,
..Self::empty()
} }
} }
@ -109,18 +106,12 @@ impl Curve {
Self::from_values(iter.collect()) 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( pub fn from_explicit_callback(
function: impl Fn(f64) -> f64 + 'static, function: impl Fn(f64) -> f64 + 'static,
x_range: RangeInclusive<f64>, x_range: RangeInclusive<f64>,
points: usize, points: usize,
) -> Self { ) -> 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 { let generator = ExplicitGenerator {
function: Box::new(function), function: Box::new(function),
x_range, x_range,
@ -128,13 +119,12 @@ impl Curve {
}; };
Self { Self {
values: Vec::new(),
generator: Some(generator), 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( pub fn from_parametric_callback(
function: impl Fn(f64) -> (f64, f64), function: impl Fn(f64) -> (f64, f64),
t_range: RangeInclusive<f64>, t_range: RangeInclusive<f64>,
@ -149,24 +139,28 @@ impl Curve {
Self::from_values_iter(values) Self::from_values_iter(values)
} }
/// Returns true if there are no data points available and there is no function to generate any. /// From a series of y-values.
pub(crate) fn no_data(&self) -> bool { /// The x-values will be the indices of these values
self.generator.is_none() && self.values.is_empty() pub fn from_ys_f32(ys: &[f32]) -> Self {
let values: Vec<Value> = 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. /// Returns true if there are no data points available and there is no function to generate any.
fn range_intersection( pub(super) fn is_empty(&self) -> bool {
range1: &RangeInclusive<f64>, self.generator.is_none() && self.values.is_empty()
range2: &RangeInclusive<f64>,
) -> Option<RangeInclusive<f64>> {
let start = range1.start().max(*range2.start());
let end = range1.end().min(*range2.end());
(start < end).then(|| start..=end)
} }
/// If initialized with a generator function, this will generate `n` evenly spaced points in the /// If initialized with a generator function, this will generate `n` evenly spaced points in the
/// given range. /// given range.
pub(crate) fn generate_points(&mut self, x_range: RangeInclusive<f64>) { pub(super) fn generate_points(&mut self, x_range: RangeInclusive<f64>) {
if let Some(generator) = self.generator.take() { if let Some(generator) = self.generator.take() {
if let Some(intersection) = Self::range_intersection(&x_range, &generator.x_range) { if let Some(intersection) = Self::range_intersection(&x_range, &generator.x_range) {
let increment = let increment =
@ -182,18 +176,81 @@ impl Curve {
} }
} }
/// From a series of y-values. /// Returns the intersection of two ranges if they intersect.
/// The x-values will be the indices of these values fn range_intersection(
pub fn from_ys_f32(ys: &[f32]) -> Self { range1: &RangeInclusive<f64>,
let values: Vec<Value> = ys range2: &RangeInclusive<f64>,
) -> Option<RangeInclusive<f64>> {
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() .iter()
.enumerate() .for_each(|value| bounds.extend_with(value));
.map(|(i, &y)| Value { bounds
x: i as f64, }
y: y as f64, }
})
.collect(); // ----------------------------------------------------------------------------
Self::from_values(values)
#[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<Self> {
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. /// Add a stroke.
@ -214,13 +271,289 @@ impl Curve {
self self
} }
/// Name of this curve. /// Name of this line.
/// ///
/// If a curve is given a name it will show up in the plot legend /// This name will show up in the plot legend, if legends are turned on.
/// (if legends are turned on).
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub fn name(mut self, name: impl ToString) -> Self { pub fn name(mut self, name: impl ToString) -> Self {
self.name = name.to_string(); self.name = name.to_string();
self self
} }
} }
impl PlotItem for Line {
fn get_shapes(&self, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
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<Shape>) {
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;
}
}

View file

@ -6,15 +6,15 @@ mod transform;
use std::collections::{BTreeMap, HashSet}; use std::collections::{BTreeMap, HashSet};
pub use items::{Curve, Value}; use items::PlotItem;
pub use items::{HLine, VLine}; pub use items::{HLine, VLine};
pub use items::{Line, MarkerShape, Points, Value, Values};
use legend::LegendEntry;
use transform::{Bounds, ScreenTransform}; use transform::{Bounds, ScreenTransform};
use crate::*; use crate::*;
use color::Hsva; use color::Hsva;
use self::legend::LegendEntry;
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// Information about the plot that has to persist between frames. /// Information about the plot that has to persist between frames.
@ -23,32 +23,32 @@ use self::legend::LegendEntry;
struct PlotMemory { struct PlotMemory {
bounds: Bounds, bounds: Bounds,
auto_bounds: bool, auto_bounds: bool,
hidden_curves: HashSet<String>, hidden_items: HashSet<String>,
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// A 2D plot, e.g. a graph of a function. /// 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(); /// # 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 sin = (0..1000).map(|i| {
/// let x = i as f64 * 0.01; /// let x = i as f64 * 0.01;
/// Value::new(x, x.sin()) /// Value::new(x, x.sin())
/// }); /// });
/// let curve = Curve::from_values_iter(sin); /// let line = Line::new(Values::from_values_iter(sin));
/// ui.add( /// 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 { pub struct Plot {
name: String, name: String,
next_auto_color_idx: usize, next_auto_color_idx: usize,
curves: Vec<Curve>, items: Vec<Box<dyn PlotItem>>,
hlines: Vec<HLine>, hlines: Vec<HLine>,
vlines: Vec<VLine>, vlines: Vec<VLine>,
@ -77,7 +77,7 @@ impl Plot {
name: name.to_string(), name: name.to_string(),
next_auto_color_idx: 0, next_auto_color_idx: 0,
curves: Default::default(), items: Default::default(),
hlines: Default::default(), hlines: Default::default(),
vlines: Default::default(), vlines: Default::default(),
@ -100,23 +100,43 @@ impl Plot {
} }
} }
fn auto_color(&mut self, color: &mut Color32) { fn auto_color(&mut self) -> Color32 {
if *color == Color32::TRANSPARENT {
let i = self.next_auto_color_idx; let i = self.next_auto_color_idx;
self.next_auto_color_idx += 1; self.next_auto_color_idx += 1;
let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875 let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875
let h = i as f32 * golden_ratio; 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 Hsva::new(h, 0.85, 0.5, 1.0).into() // TODO: OkLab or some other perspective color space
}
} }
/// Add a data curve. /// Add a data lines.
/// You can add multiple curves. /// You can add multiple lines.
pub fn curve(mut self, mut curve: Curve) -> Self { pub fn line(mut self, mut line: Line) -> Self {
if !curve.no_data() { if line.series.is_empty() {
self.auto_color(&mut curve.stroke.color); return self;
self.curves.push(curve); };
// 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 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 self
} }
@ -124,7 +144,9 @@ impl Plot {
/// Can be useful e.g. to show min/max bounds or similar. /// Can be useful e.g. to show min/max bounds or similar.
/// Always fills the full width of the plot. /// Always fills the full width of the plot.
pub fn hline(mut self, mut hline: HLine) -> Self { 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.hlines.push(hline);
self self
} }
@ -133,7 +155,9 @@ impl Plot {
/// Can be useful e.g. to show min/max bounds or similar. /// Can be useful e.g. to show min/max bounds or similar.
/// Always fills the full height of the plot. /// Always fills the full height of the plot.
pub fn vline(mut self, mut vline: VLine) -> Self { 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.vlines.push(vline);
self self
} }
@ -238,7 +262,7 @@ impl Plot {
self 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 { pub fn show_legend(mut self, show: bool) -> Self {
self.show_legend = show; self.show_legend = show;
self self
@ -250,7 +274,7 @@ impl Widget for Plot {
let Self { let Self {
name, name,
next_auto_color_idx: _, next_auto_color_idx: _,
mut curves, mut items,
hlines, hlines,
vlines, vlines,
center_x_axis, center_x_axis,
@ -276,14 +300,14 @@ impl Widget for Plot {
.get_mut_or_insert_with(plot_id, || PlotMemory { .get_mut_or_insert_with(plot_id, || PlotMemory {
bounds: min_auto_bounds, bounds: min_auto_bounds,
auto_bounds: !min_auto_bounds.is_valid(), auto_bounds: !min_auto_bounds.is_valid(),
hidden_curves: HashSet::new(), hidden_items: HashSet::new(),
}) })
.clone(); .clone();
let PlotMemory { let PlotMemory {
mut bounds, mut bounds,
mut auto_bounds, mut auto_bounds,
mut hidden_curves, mut hidden_items,
} = memory; } = memory;
// Determine the size of the plot in the UI // Determine the size of the plot in the UI
@ -324,23 +348,26 @@ impl Widget for Plot {
// --- Legend --- // --- Legend ---
if show_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. // checkbox. If their colors don't match, we pick a neutral color for the checkbox.
let mut legend_entries: BTreeMap<String, LegendEntry> = BTreeMap::new(); let mut legend_entries: BTreeMap<String, LegendEntry> = BTreeMap::new();
curves let neutral_color = ui.visuals().noninteractive().fg_stroke.color;
items
.iter() .iter()
.filter(|curve| !curve.name.is_empty()) .filter(|item| !item.name().is_empty())
.for_each(|curve| { .for_each(|item| {
let checked = !hidden_curves.contains(&curve.name); let checked = !hidden_items.contains(item.name());
let text = curve.name.clone(); let text = item.name();
legend_entries legend_entries
.entry(curve.name.clone()) .entry(item.name().to_string())
.and_modify(|entry| { .and_modify(|entry| {
if entry.color != curve.stroke.color { if entry.color != item.color() {
entry.color = ui.visuals().noninteractive().fg_stroke.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. // Show the legend.
@ -353,28 +380,27 @@ impl Widget for Plot {
} }
}); });
// Get the names of the hidden curves. // Get the names of the hidden items.
hidden_curves = legend_entries hidden_items = legend_entries
.values() .values()
.filter(|entry| !entry.checked) .filter(|entry| !entry.checked)
.map(|entry| entry.text.clone()) .map(|entry| entry.text.clone())
.collect(); .collect();
// Highlight the hovered curves. // Highlight the hovered items.
legend_entries legend_entries
.values() .values()
.filter(|entry| entry.hovered) .filter(|entry| entry.hovered)
.for_each(|entry| { .for_each(|entry| {
curves items.iter_mut().for_each(|item| {
.iter_mut() if item.name() == entry.text {
.filter(|curve| curve.name == entry.text) item.highlight();
.for_each(|curve| { }
curve.stroke.width *= 2.0;
}); });
}); });
// Remove deselected curves. // Remove deselected items.
curves.retain(|curve| !hidden_curves.contains(&curve.name)); items.retain(|item| !hidden_items.contains(item.name()));
} }
// --- // ---
@ -386,7 +412,9 @@ impl Widget for Plot {
bounds = min_auto_bounds; bounds = min_auto_bounds;
hlines.iter().for_each(|line| bounds.extend_with_y(line.y)); hlines.iter().for_each(|line| bounds.extend_with_y(line.y));
vlines.iter().for_each(|line| bounds.extend_with_x(line.x)); 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); bounds.add_relative_margin(margin_fraction);
} }
// Make sure they are not empty. // Make sure they are not empty.
@ -437,14 +465,15 @@ impl Widget for Plot {
} }
// Initialize values from functions. // Initialize values from functions.
curves items.iter_mut().for_each(|item| {
.iter_mut() item.series_mut()
.for_each(|curve| curve.generate_points(transform.bounds().range_x())); .generate_points(transform.bounds().range_x())
});
let bounds = *transform.bounds(); let bounds = *transform.bounds();
let prepared = Prepared { let prepared = Prepared {
curves, items,
hlines, hlines,
vlines, vlines,
show_x, show_x,
@ -458,7 +487,7 @@ impl Widget for Plot {
PlotMemory { PlotMemory {
bounds, bounds,
auto_bounds, auto_bounds,
hidden_curves, hidden_items,
}, },
); );
@ -471,7 +500,7 @@ impl Widget for Plot {
} }
struct Prepared { struct Prepared {
curves: Vec<Curve>, items: Vec<Box<dyn PlotItem>>,
hlines: Vec<HLine>, hlines: Vec<HLine>,
vlines: Vec<VLine>, vlines: Vec<VLine>,
show_x: bool, show_x: bool,
@ -480,15 +509,15 @@ struct Prepared {
} }
impl Prepared { impl Prepared {
fn ui(&self, ui: &mut Ui, response: &Response) { fn ui(self, ui: &mut Ui, response: &Response) {
let Self { transform, .. } = self;
let mut shapes = Vec::new(); let mut shapes = Vec::new();
for d in 0..2 { for d in 0..2 {
self.paint_axis(ui, d, &mut shapes); self.paint_axis(ui, d, &mut shapes);
} }
let transform = &self.transform;
for &hline in &self.hlines { for &hline in &self.hlines {
let HLine { y, stroke } = hline; let HLine { y, stroke } = hline;
let points = [ let points = [
@ -507,22 +536,8 @@ impl Prepared {
shapes.push(Shape::line_segment(points, stroke)); shapes.push(Shape::line_segment(points, stroke));
} }
for curve in &self.curves { for item in &self.items {
let stroke = curve.stroke; item.get_shapes(transform, &mut shapes);
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);
} }
if let Some(pointer) = response.hover_pos() { if let Some(pointer) = response.hover_pos() {
@ -626,7 +641,7 @@ impl Prepared {
transform, transform,
show_x, show_x,
show_y, show_y,
curves, items,
.. ..
} = self; } = self;
@ -636,24 +651,24 @@ impl Prepared {
let interact_radius: f32 = 16.0; let interact_radius: f32 = 16.0;
let mut closest_value = None; let mut closest_value = None;
let mut closest_curve = None; let mut closest_item = None;
let mut closest_dist_sq = interact_radius.powi(2); let mut closest_dist_sq = interact_radius.powi(2);
for curve in curves { for item in items {
for value in &curve.values { for value in &item.series().values {
let pos = transform.position_from_value(value); let pos = transform.position_from_value(value);
let dist_sq = pointer.distance_sq(pos); let dist_sq = pointer.distance_sq(pos);
if dist_sq < closest_dist_sq { if dist_sq < closest_dist_sq {
closest_dist_sq = dist_sq; closest_dist_sq = dist_sq;
closest_value = Some(value); closest_value = Some(value);
closest_curve = Some(curve); closest_item = Some(item.name());
} }
} }
} }
let mut prefix = String::new(); let mut prefix = String::new();
if let Some(curve) = closest_curve { if let Some(name) = closest_item {
if !curve.name.is_empty() { if !name.is_empty() {
prefix = format!("{}\n", curve.name); prefix = format!("{}\n", name);
} }
} }

View file

@ -1,9 +1,9 @@
use egui::plot::{Curve, Plot, Value}; use egui::plot::{Line, MarkerShape, Plot, Points, Value, Values};
use egui::*; use egui::*;
use std::f64::consts::TAU; use std::f64::consts::TAU;
#[derive(PartialEq)] #[derive(PartialEq)]
pub struct PlotDemo { struct LineDemo {
animate: bool, animate: bool,
time: f64, time: f64,
circle_radius: f64, circle_radius: f64,
@ -13,7 +13,7 @@ pub struct PlotDemo {
proportional: bool, proportional: bool,
} }
impl Default for PlotDemo { impl Default for LineDemo {
fn default() -> Self { fn default() -> Self {
Self { Self {
animate: true, animate: true,
@ -27,29 +27,8 @@ impl Default for PlotDemo {
} }
} }
impl super::Demo for PlotDemo { impl LineDemo {
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 {
fn options_ui(&mut self, ui: &mut Ui) { 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 { let Self {
animate, animate,
time: _, time: _,
@ -58,6 +37,7 @@ impl PlotDemo {
square, square,
legend, legend,
proportional, proportional,
..
} = self; } = self;
ui.horizontal(|ui| { ui.horizontal(|ui| {
@ -88,25 +68,14 @@ impl PlotDemo {
ui.vertical(|ui| { ui.vertical(|ui| {
ui.style_mut().wrap = Some(false); ui.style_mut().wrap = Some(false);
ui.checkbox(animate, "animate"); ui.checkbox(animate, "animate");
ui.add_space(8.0);
ui.checkbox(square, "square view"); ui.checkbox(square, "square view");
ui.checkbox(legend, "legend"); ui.checkbox(legend, "legend");
ui.checkbox(proportional, "proportional data axes"); 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 n = 512;
let circle = (0..=n).map(|i| { let circle = (0..=n).map(|i| {
let t = remap(i as f64, 0.0..=(n as f64), 0.0..=TAU); 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, 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)) .color(Color32::from_rgb(100, 200, 100))
.name("circle") .name("circle")
} }
fn sin(&self) -> Curve { fn sin(&self) -> Line {
let time = self.time; let time = self.time;
Curve::from_explicit_callback( Line::new(Values::from_explicit_callback(
move |x| 0.5 * (2.0 * x).sin() * time.sin(), move |x| 0.5 * (2.0 * x).sin() * time.sin(),
f64::NEG_INFINITY..=f64::INFINITY, f64::NEG_INFINITY..=f64::INFINITY,
512, 512,
) ))
.color(Color32::from_rgb(200, 100, 100)) .color(Color32::from_rgb(200, 100, 100))
.name("wave") .name("wave")
} }
fn thingy(&self) -> Curve { fn thingy(&self) -> Line {
let time = self.time; 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()), move |t| ((2.0 * t + time).sin(), (3.0 * t).sin()),
0.0..=TAU, 0.0..=TAU,
512, 256,
) ))
.color(Color32::from_rgb(100, 150, 250)) .color(Color32::from_rgb(100, 150, 250))
.name("x = sin(2t), y = sin(3t)") .name("x = sin(2t), y = sin(3t)")
} }
} }
impl super::View for PlotDemo { impl Widget for &mut LineDemo {
fn ui(&mut self, ui: &mut Ui) { fn ui(self, ui: &mut Ui) -> Response {
self.options_ui(ui); self.options_ui(ui);
if self.animate { if self.animate {
ui.ctx().request_repaint(); ui.ctx().request_repaint();
self.time += ui.input().unstable_dt.at_most(1.0 / 30.0) as f64; self.time += ui.input().unstable_dt.at_most(1.0 / 30.0) as f64;
}; };
let mut plot = Plot::new("Lines Demo")
let mut plot = Plot::new("Demo Plot") .line(self.circle())
.curve(self.circle()) .line(self.sin())
.curve(self.sin()) .line(self.thingy())
.curve(self.thingy()) .show_legend(self.legend);
.show_legend(self.legend)
.min_size(Vec2::new(200.0, 200.0));
if self.square { if self.square {
plot = plot.view_aspect(1.0); plot = plot.view_aspect(1.0);
} }
if self.proportional { if self.proportional {
plot = plot.data_aspect(1.0); 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<Points> {
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);
}
}
} }
} }

View file

@ -205,14 +205,15 @@ impl WidgetGallery {
} }
fn example_plot() -> egui::plot::Plot { fn example_plot() -> egui::plot::Plot {
use egui::plot::{Line, Plot, Value, Values};
let n = 128; 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; use std::f64::consts::TAU;
let x = egui::remap(i as f64, 0.0..=(n as f64), -TAU..=TAU); let x = egui::remap(i as f64, 0.0..=(n as f64), -TAU..=TAU);
egui::plot::Value::new(x, x.sin()) Value::new(x, x.sin())
})); })));
egui::plot::Plot::new("Example Plot") Plot::new("Example Plot")
.curve(curve) .line(line)
.height(32.0) .height(32.0)
.data_aspect(1.0) .data_aspect(1.0)
} }