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
|
||||
|
||||
### 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.
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
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
|
||||
}
|
||||
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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue