use std::ops::{Bound, RangeBounds, RangeInclusive}; use egui::{Pos2, Shape, Stroke, Vec2}; use crate::transform::PlotBounds; /// A point coordinate in the plot. /// /// Uses f64 for improved accuracy to enable plotting /// large values (e.g. unix time on x axis). #[derive(Clone, Copy, Debug, PartialEq)] pub struct PlotPoint { /// This is often something monotonically increasing, such as time, but doesn't have to be. /// Goes from left to right. pub x: f64, /// Goes from bottom to top (inverse of everything else in egui!). pub y: f64, } impl From<[f64; 2]> for PlotPoint { #[inline] fn from([x, y]: [f64; 2]) -> Self { Self { x, y } } } impl PlotPoint { #[inline(always)] pub fn new(x: impl Into, y: impl Into) -> Self { Self { x: x.into(), y: y.into(), } } #[inline(always)] pub fn to_pos2(self) -> Pos2 { Pos2::new(self.x as f32, self.y as f32) } #[inline(always)] pub fn to_vec2(self) -> Vec2 { Vec2::new(self.x as f32, self.y as f32) } } // ---------------------------------------------------------------------------- /// Solid, dotted, dashed, etc. #[derive(Debug, PartialEq, Clone, Copy)] pub enum LineStyle { Solid, Dotted { spacing: f32 }, Dashed { length: f32 }, } impl LineStyle { pub fn dashed_loose() -> Self { Self::Dashed { length: 10.0 } } pub fn dashed_dense() -> Self { Self::Dashed { length: 5.0 } } pub fn dotted_loose() -> Self { Self::Dotted { spacing: 10.0 } } pub fn dotted_dense() -> Self { Self::Dotted { spacing: 5.0 } } pub(super) fn style_line( &self, line: Vec, mut stroke: Stroke, highlight: bool, shapes: &mut Vec, ) { match line.len() { 0 => {} 1 => { let mut radius = stroke.width / 2.0; if highlight { radius *= 2f32.sqrt(); } shapes.push(Shape::circle_filled(line[0], radius, stroke.color)); } _ => { match self { LineStyle::Solid => { if highlight { stroke.width *= 2.0; } shapes.push(Shape::line(line, stroke)); } LineStyle::Dotted { spacing } => { // Take the stroke width for the radius even though it's not "correct", otherwise // the dots would become too small. let mut radius = stroke.width; if highlight { radius *= 2f32.sqrt(); } shapes.extend(Shape::dotted_line(&line, stroke.color, *spacing, radius)); } LineStyle::Dashed { length } => { if highlight { stroke.width *= 2.0; } let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875 shapes.extend(Shape::dashed_line( &line, stroke, *length, length * golden_ratio, )); } } } } } } impl ToString for LineStyle { fn to_string(&self) -> String { match self { LineStyle::Solid => "Solid".into(), LineStyle::Dotted { spacing } => format!("Dotted{spacing}Px"), LineStyle::Dashed { length } => format!("Dashed{length}Px"), } } } // ---------------------------------------------------------------------------- /// Determines whether a plot element is vertically or horizontally oriented. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Orientation { Horizontal, Vertical, } impl Default for Orientation { fn default() -> Self { Self::Vertical } } // ---------------------------------------------------------------------------- /// Represents many [`PlotPoint`]s. /// /// These can be an owned `Vec` or generated with a function. pub enum PlotPoints { Owned(Vec), Generator(ExplicitGenerator), // Borrowed(&[PlotPoint]), // TODO: Lifetimes are tricky in this case. } impl Default for PlotPoints { fn default() -> Self { Self::Owned(Vec::new()) } } impl From<[f64; 2]> for PlotPoints { fn from(coordinate: [f64; 2]) -> Self { Self::new(vec![coordinate]) } } impl From> for PlotPoints { fn from(coordinates: Vec<[f64; 2]>) -> Self { Self::new(coordinates) } } impl FromIterator<[f64; 2]> for PlotPoints { fn from_iter>(iter: T) -> Self { Self::Owned(iter.into_iter().map(|point| point.into()).collect()) } } impl PlotPoints { pub fn new(points: Vec<[f64; 2]>) -> Self { Self::from_iter(points) } pub fn points(&self) -> &[PlotPoint] { match self { PlotPoints::Owned(points) => points.as_slice(), PlotPoints::Generator(_) => &[], } } /// Draw a line based on a function `y=f(x)`, a range (which can be infinite) for x and the number of points. pub fn from_explicit_callback( function: impl Fn(f64) -> f64 + 'static, x_range: impl RangeBounds, points: usize, ) -> Self { let start = match x_range.start_bound() { Bound::Included(x) | Bound::Excluded(x) => *x, Bound::Unbounded => f64::NEG_INFINITY, }; let end = match x_range.end_bound() { Bound::Included(x) | Bound::Excluded(x) => *x, Bound::Unbounded => f64::INFINITY, }; let x_range = start..=end; let generator = ExplicitGenerator { function: Box::new(function), x_range, points, }; Self::Generator(generator) } /// Draw a line based on a function `(x,y)=f(t)`, a range for t and the number of points. /// The range may be specified as start..end or as start..=end. pub fn from_parametric_callback( function: impl Fn(f64) -> (f64, f64), t_range: impl RangeBounds, points: usize, ) -> Self { let start = match t_range.start_bound() { Bound::Included(x) => x, Bound::Excluded(_) => unreachable!(), Bound::Unbounded => panic!("The range for parametric functions must be bounded!"), }; let end = match t_range.end_bound() { Bound::Included(x) | Bound::Excluded(x) => x, Bound::Unbounded => panic!("The range for parametric functions must be bounded!"), }; let last_point_included = matches!(t_range.end_bound(), Bound::Included(_)); let increment = if last_point_included { (end - start) / (points - 1) as f64 } else { (end - start) / points as f64 }; (0..points) .map(|i| { let t = start + i as f64 * increment; function(t).into() }) .collect() } /// From a series of y-values. /// The x-values will be the indices of these values pub fn from_ys_f32(ys: &[f32]) -> Self { ys.iter() .enumerate() .map(|(i, &y)| [i as f64, y as f64]) .collect() } /// From a series of y-values. /// The x-values will be the indices of these values pub fn from_ys_f64(ys: &[f64]) -> Self { ys.iter().enumerate().map(|(i, &y)| [i as f64, y]).collect() } /// Returns true if there are no data points available and there is no function to generate any. pub(crate) fn is_empty(&self) -> bool { match self { PlotPoints::Owned(points) => points.is_empty(), PlotPoints::Generator(_) => false, } } /// If initialized with a generator function, this will generate `n` evenly spaced points in the /// given range. pub(super) fn generate_points(&mut self, x_range: RangeInclusive) { if let Self::Generator(generator) = self { *self = Self::range_intersection(&x_range, &generator.x_range) .map(|intersection| { let increment = (intersection.end() - intersection.start()) / (generator.points - 1) as f64; (0..generator.points) .map(|i| { let x = intersection.start() + i as f64 * increment; let y = (generator.function)(x); [x, y] }) .collect() }) .unwrap_or_default(); } } /// Returns the intersection of two ranges if they intersect. fn range_intersection( range1: &RangeInclusive, range2: &RangeInclusive, ) -> Option> { let start = range1.start().max(*range2.start()); let end = range1.end().min(*range2.end()); (start < end).then_some(start..=end) } pub(super) fn bounds(&self) -> PlotBounds { match self { PlotPoints::Owned(points) => { let mut bounds = PlotBounds::NOTHING; for point in points { bounds.extend_with(point); } bounds } PlotPoints::Generator(generator) => generator.estimate_bounds(), } } } // ---------------------------------------------------------------------------- /// Circle, Diamond, Square, Cross, … #[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() -> impl ExactSizeIterator { [ Self::Circle, Self::Diamond, Self::Square, Self::Cross, Self::Plus, Self::Up, Self::Down, Self::Left, Self::Right, Self::Asterisk, ] .iter() .copied() } } // ---------------------------------------------------------------------------- /// Query the points of the plot, for geometric relations like closest checks pub(crate) enum PlotGeometry<'a> { /// No geometry based on single elements (examples: text, image, horizontal/vertical line) None, /// Point values (X-Y graphs) Points(&'a [PlotPoint]), /// Rectangles (examples: boxes or bars) // Has currently no data, as it would require copying rects or iterating a list of pointers. // Instead, geometry-based functions are directly implemented in the respective PlotItem impl. Rects, } // ---------------------------------------------------------------------------- /// Describes a function y = f(x) with an optional range for x and a number of points. pub struct ExplicitGenerator { function: Box f64>, x_range: RangeInclusive, points: usize, } impl ExplicitGenerator { fn estimate_bounds(&self) -> PlotBounds { let mut bounds = PlotBounds::NOTHING; let mut add_x = |x: f64| { // avoid infinities, as we cannot auto-bound on them! if x.is_finite() { bounds.extend_with_x(x); } let y = (self.function)(x); if y.is_finite() { bounds.extend_with_y(y); } }; let min_x = *self.x_range.start(); let max_x = *self.x_range.end(); add_x(min_x); add_x(max_x); if min_x.is_finite() && max_x.is_finite() { // Sample some points in the interval: const N: u32 = 8; for i in 1..N { let t = i as f64 / (N - 1) as f64; let x = crate::lerp(min_x..=max_x, t); add_x(x); } } else { // Try adding some points anyway: for x in [-1, 0, 1] { let x = x as f64; if min_x <= x && x <= max_x { add_x(x); } } } bounds } } // ---------------------------------------------------------------------------- /// Result of [`super::PlotItem::find_closest()`] search, identifies an element inside the item for immediate use pub(crate) struct ClosestElem { /// Position of hovered-over value (or bar/box-plot/...) in PlotItem pub index: usize, /// Squared distance from the mouse cursor (needed to compare against other PlotItems, which might be nearer) pub dist_sq: f32, }