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

View file

@ -2,7 +2,7 @@
use std::ops::RangeInclusive;
use super::transform::Bounds;
use super::transform::{Bounds, ScreenTransform};
use crate::*;
/// A value in the value-space of the plot.
@ -33,8 +33,8 @@ impl Value {
/// A horizontal line in a plot, filling the full width
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct HLine {
pub(crate) y: f64,
pub(crate) stroke: Stroke,
pub(super) y: f64,
pub(super) stroke: Stroke,
}
impl HLine {
@ -49,8 +49,8 @@ impl HLine {
/// A vertical line in a plot, filling the full width
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct VLine {
pub(crate) x: f64,
pub(crate) stroke: Stroke,
pub(super) x: f64,
pub(super) stroke: Stroke,
}
impl VLine {
@ -62,6 +62,15 @@ impl VLine {
}
}
pub(super) trait PlotItem {
fn get_shapes(&self, transform: &ScreenTransform, shapes: &mut Vec<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.
@ -71,37 +80,25 @@ struct ExplicitGenerator {
points: usize,
}
// ----------------------------------------------------------------------------
/// A series of values forming a path.
pub struct Curve {
pub(crate) values: Vec<Value>,
pub struct Values {
pub(super) values: Vec<Value>,
generator: Option<ExplicitGenerator>,
pub(crate) bounds: Bounds,
pub(crate) stroke: Stroke,
pub(crate) name: String,
}
impl Curve {
fn empty() -> Self {
impl Default for Values {
fn default() -> Self {
Self {
values: Vec::new(),
generator: None,
bounds: Bounds::NOTHING,
stroke: Stroke::new(2.0, Color32::TRANSPARENT),
name: Default::default(),
}
}
}
impl Values {
pub fn from_values(values: Vec<Value>) -> Self {
let mut bounds = Bounds::NOTHING;
for value in &values {
bounds.extend_with(value);
}
Self {
values,
bounds,
..Self::empty()
generator: None,
}
}
@ -109,18 +106,12 @@ impl Curve {
Self::from_values(iter.collect())
}
/// Draw a curve based on a function `y=f(x)`, a range (which can be infinite) for x and the number of points.
/// Draw a line based on a function `y=f(x)`, a range (which can be infinite) for x and the number of points.
pub fn from_explicit_callback(
function: impl Fn(f64) -> f64 + 'static,
x_range: RangeInclusive<f64>,
points: usize,
) -> Self {
let mut bounds = Bounds::NOTHING;
if x_range.start().is_finite() && x_range.end().is_finite() {
bounds.min[0] = *x_range.start();
bounds.max[0] = *x_range.end();
}
let generator = ExplicitGenerator {
function: Box::new(function),
x_range,
@ -128,13 +119,12 @@ impl Curve {
};
Self {
values: Vec::new(),
generator: Some(generator),
bounds,
..Self::empty()
}
}
/// Draw a curve based on a function `(x,y)=f(t)`, a range for t and the number of points.
/// Draw a line based on a function `(x,y)=f(t)`, a range for t and the number of points.
pub fn from_parametric_callback(
function: impl Fn(f64) -> (f64, f64),
t_range: RangeInclusive<f64>,
@ -149,24 +139,28 @@ impl Curve {
Self::from_values_iter(values)
}
/// Returns true if there are no data points available and there is no function to generate any.
pub(crate) fn no_data(&self) -> bool {
self.generator.is_none() && self.values.is_empty()
/// From a series of y-values.
/// The x-values will be the indices of these values
pub fn from_ys_f32(ys: &[f32]) -> Self {
let values: Vec<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.
fn range_intersection(
range1: &RangeInclusive<f64>,
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)
/// Returns true if there are no data points available and there is no function to generate any.
pub(super) fn is_empty(&self) -> bool {
self.generator.is_none() && self.values.is_empty()
}
/// If initialized with a generator function, this will generate `n` evenly spaced points in the
/// given range.
pub(crate) fn generate_points(&mut self, x_range: RangeInclusive<f64>) {
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 =
@ -182,18 +176,81 @@ impl Curve {
}
}
/// From a series of y-values.
/// The x-values will be the indices of these values
pub fn from_ys_f32(ys: &[f32]) -> Self {
let values: Vec<Value> = ys
/// Returns the intersection of two ranges if they intersect.
fn range_intersection(
range1: &RangeInclusive<f64>,
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()
.enumerate()
.map(|(i, &y)| Value {
x: i as f64,
y: y as f64,
})
.collect();
Self::from_values(values)
.for_each(|value| bounds.extend_with(value));
bounds
}
}
// ----------------------------------------------------------------------------
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum MarkerShape {
Circle,
Diamond,
Square,
Cross,
Plus,
Up,
Down,
Left,
Right,
Asterisk,
}
impl MarkerShape {
/// Get a vector containing all marker shapes.
pub fn all() -> Vec<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.
@ -214,13 +271,289 @@ impl Curve {
self
}
/// Name of this curve.
/// Name of this line.
///
/// If a curve is given a name it will show up in the plot legend
/// (if legends are turned on).
/// This name will show up in the plot legend, if legends are turned on.
#[allow(clippy::needless_pass_by_value)]
pub fn name(mut self, name: impl ToString) -> Self {
self.name = name.to_string();
self
}
}
impl PlotItem for Line {
fn get_shapes(&self, transform: &ScreenTransform, shapes: &mut Vec<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};
pub use items::{Curve, Value};
use items::PlotItem;
pub use items::{HLine, VLine};
pub use items::{Line, MarkerShape, Points, Value, Values};
use legend::LegendEntry;
use transform::{Bounds, ScreenTransform};
use crate::*;
use color::Hsva;
use self::legend::LegendEntry;
// ----------------------------------------------------------------------------
/// Information about the plot that has to persist between frames.
@ -23,32 +23,32 @@ use self::legend::LegendEntry;
struct PlotMemory {
bounds: Bounds,
auto_bounds: bool,
hidden_curves: HashSet<String>,
hidden_items: HashSet<String>,
}
// ----------------------------------------------------------------------------
/// A 2D plot, e.g. a graph of a function.
///
/// `Plot` supports multiple curves.
/// `Plot` supports multiple lines and points.
///
/// ```
/// # let ui = &mut egui::Ui::__test();
/// use egui::plot::{Curve, Plot, Value};
/// use egui::plot::{Line, Plot, Value, Values};
/// let sin = (0..1000).map(|i| {
/// let x = i as f64 * 0.01;
/// Value::new(x, x.sin())
/// });
/// let curve = Curve::from_values_iter(sin);
/// let line = Line::new(Values::from_values_iter(sin));
/// ui.add(
/// Plot::new("Test Plot").curve(curve).view_aspect(2.0)
/// Plot::new("Test Plot").line(line).view_aspect(2.0)
/// );
/// ```
pub struct Plot {
name: String,
next_auto_color_idx: usize,
curves: Vec<Curve>,
items: Vec<Box<dyn PlotItem>>,
hlines: Vec<HLine>,
vlines: Vec<VLine>,
@ -77,7 +77,7 @@ impl Plot {
name: name.to_string(),
next_auto_color_idx: 0,
curves: Default::default(),
items: Default::default(),
hlines: Default::default(),
vlines: Default::default(),
@ -100,23 +100,43 @@ impl Plot {
}
}
fn auto_color(&mut self, color: &mut Color32) {
if *color == Color32::TRANSPARENT {
fn auto_color(&mut self) -> Color32 {
let i = self.next_auto_color_idx;
self.next_auto_color_idx += 1;
let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875
let h = i as f32 * golden_ratio;
*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.
/// You can add multiple curves.
pub fn curve(mut self, mut curve: Curve) -> Self {
if !curve.no_data() {
self.auto_color(&mut curve.stroke.color);
self.curves.push(curve);
/// Add a data lines.
/// You can add multiple lines.
pub fn line(mut self, mut line: Line) -> Self {
if line.series.is_empty() {
return self;
};
// Give the stroke an automatic color if no color has been assigned.
if line.stroke.color == Color32::TRANSPARENT {
line.stroke.color = self.auto_color();
}
self.items.push(Box::new(line));
self
}
/// Add data points.
/// You can add multiple sets of points.
pub fn points(mut self, mut points: Points) -> Self {
if points.series.is_empty() {
return self;
};
// Give the points an automatic color if no color has been assigned.
if points.color == Color32::TRANSPARENT {
points.color = self.auto_color();
}
self.items.push(Box::new(points));
self
}
@ -124,7 +144,9 @@ impl Plot {
/// Can be useful e.g. to show min/max bounds or similar.
/// Always fills the full width of the plot.
pub fn hline(mut self, mut hline: HLine) -> Self {
self.auto_color(&mut hline.stroke.color);
if hline.stroke.color == Color32::TRANSPARENT {
hline.stroke.color = self.auto_color();
}
self.hlines.push(hline);
self
}
@ -133,7 +155,9 @@ impl Plot {
/// Can be useful e.g. to show min/max bounds or similar.
/// Always fills the full height of the plot.
pub fn vline(mut self, mut vline: VLine) -> Self {
self.auto_color(&mut vline.stroke.color);
if vline.stroke.color == Color32::TRANSPARENT {
vline.stroke.color = self.auto_color();
}
self.vlines.push(vline);
self
}
@ -238,7 +262,7 @@ impl Plot {
self
}
/// Whether to show a legend including all named curves. Default: `true`.
/// Whether to show a legend including all named items. Default: `true`.
pub fn show_legend(mut self, show: bool) -> Self {
self.show_legend = show;
self
@ -250,7 +274,7 @@ impl Widget for Plot {
let Self {
name,
next_auto_color_idx: _,
mut curves,
mut items,
hlines,
vlines,
center_x_axis,
@ -276,14 +300,14 @@ impl Widget for Plot {
.get_mut_or_insert_with(plot_id, || PlotMemory {
bounds: min_auto_bounds,
auto_bounds: !min_auto_bounds.is_valid(),
hidden_curves: HashSet::new(),
hidden_items: HashSet::new(),
})
.clone();
let PlotMemory {
mut bounds,
mut auto_bounds,
mut hidden_curves,
mut hidden_items,
} = memory;
// Determine the size of the plot in the UI
@ -324,23 +348,26 @@ impl Widget for Plot {
// --- Legend ---
if show_legend {
// Collect the legend entries. If multiple curves have the same name, they share a
// Collect the legend entries. If multiple items have the same name, they share a
// checkbox. If their colors don't match, we pick a neutral color for the checkbox.
let mut legend_entries: BTreeMap<String, LegendEntry> = BTreeMap::new();
curves
let neutral_color = ui.visuals().noninteractive().fg_stroke.color;
items
.iter()
.filter(|curve| !curve.name.is_empty())
.for_each(|curve| {
let checked = !hidden_curves.contains(&curve.name);
let text = curve.name.clone();
.filter(|item| !item.name().is_empty())
.for_each(|item| {
let checked = !hidden_items.contains(item.name());
let text = item.name();
legend_entries
.entry(curve.name.clone())
.entry(item.name().to_string())
.and_modify(|entry| {
if entry.color != curve.stroke.color {
entry.color = ui.visuals().noninteractive().fg_stroke.color
if entry.color != item.color() {
entry.color = neutral_color
}
})
.or_insert_with(|| LegendEntry::new(text, curve.stroke.color, checked));
.or_insert_with(|| {
LegendEntry::new(text.to_string(), item.color(), checked)
});
});
// Show the legend.
@ -353,28 +380,27 @@ impl Widget for Plot {
}
});
// Get the names of the hidden curves.
hidden_curves = legend_entries
// Get the names of the hidden items.
hidden_items = legend_entries
.values()
.filter(|entry| !entry.checked)
.map(|entry| entry.text.clone())
.collect();
// Highlight the hovered curves.
// Highlight the hovered items.
legend_entries
.values()
.filter(|entry| entry.hovered)
.for_each(|entry| {
curves
.iter_mut()
.filter(|curve| curve.name == entry.text)
.for_each(|curve| {
curve.stroke.width *= 2.0;
items.iter_mut().for_each(|item| {
if item.name() == entry.text {
item.highlight();
}
});
});
// Remove deselected curves.
curves.retain(|curve| !hidden_curves.contains(&curve.name));
// Remove deselected items.
items.retain(|item| !hidden_items.contains(item.name()));
}
// ---
@ -386,7 +412,9 @@ impl Widget for Plot {
bounds = min_auto_bounds;
hlines.iter().for_each(|line| bounds.extend_with_y(line.y));
vlines.iter().for_each(|line| bounds.extend_with_x(line.x));
curves.iter().for_each(|curve| bounds.merge(&curve.bounds));
items
.iter()
.for_each(|item| bounds.merge(&item.series().get_bounds()));
bounds.add_relative_margin(margin_fraction);
}
// Make sure they are not empty.
@ -437,14 +465,15 @@ impl Widget for Plot {
}
// Initialize values from functions.
curves
.iter_mut()
.for_each(|curve| curve.generate_points(transform.bounds().range_x()));
items.iter_mut().for_each(|item| {
item.series_mut()
.generate_points(transform.bounds().range_x())
});
let bounds = *transform.bounds();
let prepared = Prepared {
curves,
items,
hlines,
vlines,
show_x,
@ -458,7 +487,7 @@ impl Widget for Plot {
PlotMemory {
bounds,
auto_bounds,
hidden_curves,
hidden_items,
},
);
@ -471,7 +500,7 @@ impl Widget for Plot {
}
struct Prepared {
curves: Vec<Curve>,
items: Vec<Box<dyn PlotItem>>,
hlines: Vec<HLine>,
vlines: Vec<VLine>,
show_x: bool,
@ -480,15 +509,15 @@ struct Prepared {
}
impl Prepared {
fn ui(&self, ui: &mut Ui, response: &Response) {
let Self { transform, .. } = self;
fn ui(self, ui: &mut Ui, response: &Response) {
let mut shapes = Vec::new();
for d in 0..2 {
self.paint_axis(ui, d, &mut shapes);
}
let transform = &self.transform;
for &hline in &self.hlines {
let HLine { y, stroke } = hline;
let points = [
@ -507,22 +536,8 @@ impl Prepared {
shapes.push(Shape::line_segment(points, stroke));
}
for curve in &self.curves {
let stroke = curve.stroke;
let values = &curve.values;
let shape = if values.len() == 1 {
let point = transform.position_from_value(&values[0]);
Shape::circle_filled(point, stroke.width / 2.0, stroke.color)
} else {
Shape::line(
values
.iter()
.map(|v| transform.position_from_value(v))
.collect(),
stroke,
)
};
shapes.push(shape);
for item in &self.items {
item.get_shapes(transform, &mut shapes);
}
if let Some(pointer) = response.hover_pos() {
@ -626,7 +641,7 @@ impl Prepared {
transform,
show_x,
show_y,
curves,
items,
..
} = self;
@ -636,24 +651,24 @@ impl Prepared {
let interact_radius: f32 = 16.0;
let mut closest_value = None;
let mut closest_curve = None;
let mut closest_item = None;
let mut closest_dist_sq = interact_radius.powi(2);
for curve in curves {
for value in &curve.values {
for item in items {
for value in &item.series().values {
let pos = transform.position_from_value(value);
let dist_sq = pointer.distance_sq(pos);
if dist_sq < closest_dist_sq {
closest_dist_sq = dist_sq;
closest_value = Some(value);
closest_curve = Some(curve);
closest_item = Some(item.name());
}
}
}
let mut prefix = String::new();
if let Some(curve) = closest_curve {
if !curve.name.is_empty() {
prefix = format!("{}\n", curve.name);
if let Some(name) = closest_item {
if !name.is_empty() {
prefix = format!("{}\n", name);
}
}

View file

@ -1,9 +1,9 @@
use egui::plot::{Curve, Plot, Value};
use egui::plot::{Line, MarkerShape, Plot, Points, Value, Values};
use egui::*;
use std::f64::consts::TAU;
#[derive(PartialEq)]
pub struct PlotDemo {
struct LineDemo {
animate: bool,
time: f64,
circle_radius: f64,
@ -13,7 +13,7 @@ pub struct PlotDemo {
proportional: bool,
}
impl Default for PlotDemo {
impl Default for LineDemo {
fn default() -> Self {
Self {
animate: true,
@ -27,29 +27,8 @@ impl Default for PlotDemo {
}
}
impl super::Demo for PlotDemo {
fn name(&self) -> &'static str {
"🗠 Plot"
}
fn show(&mut self, ctx: &CtxRef, open: &mut bool) {
use super::View;
Window::new(self.name())
.open(open)
.default_size(vec2(400.0, 400.0))
.scroll(false)
.show(ctx, |ui| self.ui(ui));
}
}
impl PlotDemo {
impl LineDemo {
fn options_ui(&mut self, ui: &mut Ui) {
ui.vertical_centered(|ui| {
egui::reset_button(ui, self);
ui.add(crate::__egui_github_link_file!());
});
ui.separator();
let Self {
animate,
time: _,
@ -58,6 +37,7 @@ impl PlotDemo {
square,
legend,
proportional,
..
} = self;
ui.horizontal(|ui| {
@ -88,25 +68,14 @@ impl PlotDemo {
ui.vertical(|ui| {
ui.style_mut().wrap = Some(false);
ui.checkbox(animate, "animate");
ui.add_space(8.0);
ui.checkbox(square, "square view");
ui.checkbox(legend, "legend");
ui.checkbox(proportional, "proportional data axes");
});
});
ui.label("Pan by dragging, or scroll (+ shift = horizontal).");
if cfg!(target_arch = "wasm32") {
ui.label("Zoom with ctrl / ⌘ + mouse wheel, or with pinch gesture.");
} else if cfg!(target_os = "macos") {
ui.label("Zoom with ctrl / ⌘ + scroll.");
} else {
ui.label("Zoom with ctrl + scroll.");
}
ui.label("Reset view with double-click.");
}
fn circle(&self) -> Curve {
fn circle(&self) -> Line {
let n = 512;
let circle = (0..=n).map(|i| {
let t = remap(i as f64, 0.0..=(n as f64), 0.0..=TAU);
@ -116,55 +85,192 @@ impl PlotDemo {
r * t.sin() + self.circle_center.y as f64,
)
});
Curve::from_values_iter(circle)
Line::new(Values::from_values_iter(circle))
.color(Color32::from_rgb(100, 200, 100))
.name("circle")
}
fn sin(&self) -> Curve {
fn sin(&self) -> Line {
let time = self.time;
Curve::from_explicit_callback(
Line::new(Values::from_explicit_callback(
move |x| 0.5 * (2.0 * x).sin() * time.sin(),
f64::NEG_INFINITY..=f64::INFINITY,
512,
)
))
.color(Color32::from_rgb(200, 100, 100))
.name("wave")
}
fn thingy(&self) -> Curve {
fn thingy(&self) -> Line {
let time = self.time;
Curve::from_parametric_callback(
Line::new(Values::from_parametric_callback(
move |t| ((2.0 * t + time).sin(), (3.0 * t).sin()),
0.0..=TAU,
512,
)
256,
))
.color(Color32::from_rgb(100, 150, 250))
.name("x = sin(2t), y = sin(3t)")
}
}
impl super::View for PlotDemo {
fn ui(&mut self, ui: &mut Ui) {
impl Widget for &mut LineDemo {
fn ui(self, ui: &mut Ui) -> Response {
self.options_ui(ui);
if self.animate {
ui.ctx().request_repaint();
self.time += ui.input().unstable_dt.at_most(1.0 / 30.0) as f64;
};
let mut plot = Plot::new("Demo Plot")
.curve(self.circle())
.curve(self.sin())
.curve(self.thingy())
.show_legend(self.legend)
.min_size(Vec2::new(200.0, 200.0));
let mut plot = Plot::new("Lines Demo")
.line(self.circle())
.line(self.sin())
.line(self.thingy())
.show_legend(self.legend);
if self.square {
plot = plot.view_aspect(1.0);
}
if self.proportional {
plot = plot.data_aspect(1.0);
}
ui.add(plot);
ui.add(plot)
}
}
#[derive(PartialEq)]
struct MarkerDemo {
fill_markers: bool,
marker_radius: f32,
custom_marker_color: bool,
marker_color: Color32,
}
impl Default for MarkerDemo {
fn default() -> Self {
Self {
fill_markers: true,
marker_radius: 5.0,
custom_marker_color: false,
marker_color: Color32::GRAY,
}
}
}
impl MarkerDemo {
fn markers(&self) -> Vec<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 {
use egui::plot::{Line, Plot, Value, Values};
let n = 128;
let curve = egui::plot::Curve::from_values_iter((0..=n).map(|i| {
let line = Line::new(Values::from_values_iter((0..=n).map(|i| {
use std::f64::consts::TAU;
let x = egui::remap(i as f64, 0.0..=(n as f64), -TAU..=TAU);
egui::plot::Value::new(x, x.sin())
}));
egui::plot::Plot::new("Example Plot")
.curve(curve)
Value::new(x, x.sin())
})));
Plot::new("Example Plot")
.line(line)
.height(32.0)
.data_aspect(1.0)
}