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:
parent
e320ef6c64
commit
8623909d82
5 changed files with 663 additions and 206 deletions
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue