egui_plot_bintrade/items/
values.rs

1use std::ops::{Bound, RangeBounds, RangeInclusive};
2
3use egui::{
4    Pos2, Rect, Shape, Stroke, Vec2,
5    epaint::{ColorMode, PathStroke},
6    lerp, pos2,
7};
8
9use crate::transform::PlotBounds;
10
11/// A point coordinate in the plot.
12///
13/// Uses f64 for improved accuracy to enable plotting
14/// large values (e.g. unix time on x axis).
15#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct PlotPoint {
17    /// This is often something monotonically increasing, such as time, but doesn't have to be.
18    /// Goes from left to right.
19    pub x: f64,
20
21    /// Goes from bottom to top (inverse of everything else in egui!).
22    pub y: f64,
23}
24
25impl From<[f64; 2]> for PlotPoint {
26    #[inline]
27    fn from([x, y]: [f64; 2]) -> Self {
28        Self { x, y }
29    }
30}
31
32impl PlotPoint {
33    #[inline(always)]
34    pub fn new(x: impl Into<f64>, y: impl Into<f64>) -> Self {
35        Self {
36            x: x.into(),
37            y: y.into(),
38        }
39    }
40
41    #[inline(always)]
42    pub fn to_pos2(self) -> Pos2 {
43        Pos2::new(self.x as f32, self.y as f32)
44    }
45
46    #[inline(always)]
47    pub fn to_vec2(self) -> Vec2 {
48        Vec2::new(self.x as f32, self.y as f32)
49    }
50}
51
52// ----------------------------------------------------------------------------
53
54/// Solid, dotted, dashed, etc.
55#[derive(Debug, PartialEq, Clone, Copy)]
56#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
57pub enum LineStyle {
58    Solid,
59    Dotted { spacing: f32 },
60    Dashed { length: f32 },
61}
62
63impl LineStyle {
64    pub fn dashed_loose() -> Self {
65        Self::Dashed { length: 10.0 }
66    }
67
68    pub fn dashed_dense() -> Self {
69        Self::Dashed { length: 5.0 }
70    }
71
72    pub fn dotted_loose() -> Self {
73        Self::Dotted { spacing: 10.0 }
74    }
75
76    pub fn dotted_dense() -> Self {
77        Self::Dotted { spacing: 5.0 }
78    }
79
80    pub(super) fn style_line(
81        &self,
82        line: Vec<Pos2>,
83        mut stroke: PathStroke,
84        highlight: bool,
85        shapes: &mut Vec<Shape>,
86    ) {
87        let path_stroke_color = match &stroke.color {
88            ColorMode::Solid(c) => *c,
89            ColorMode::UV(callback) => {
90                callback(Rect::from_min_max(pos2(0., 0.), pos2(0., 0.)), pos2(0., 0.))
91            }
92        };
93        match line.len() {
94            0 => {}
95            1 => {
96                let mut radius = stroke.width / 2.0;
97                if highlight {
98                    radius *= 2f32.sqrt();
99                }
100                shapes.push(Shape::circle_filled(line[0], radius, path_stroke_color));
101            }
102            _ => {
103                match self {
104                    Self::Solid => {
105                        if highlight {
106                            stroke.width *= 2.0;
107                        }
108                        shapes.push(Shape::line(line, stroke));
109                    }
110                    Self::Dotted { spacing } => {
111                        // Take the stroke width for the radius even though it's not "correct", otherwise
112                        // the dots would become too small.
113                        let mut radius = stroke.width;
114                        if highlight {
115                            radius *= 2f32.sqrt();
116                        }
117                        shapes.extend(Shape::dotted_line(
118                            &line,
119                            path_stroke_color,
120                            *spacing,
121                            radius,
122                        ));
123                    }
124                    Self::Dashed { length } => {
125                        if highlight {
126                            stroke.width *= 2.0;
127                        }
128                        let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875
129                        shapes.extend(Shape::dashed_line(
130                            &line,
131                            Stroke::new(stroke.width, path_stroke_color),
132                            *length,
133                            length * golden_ratio,
134                        ));
135                    }
136                }
137            }
138        }
139    }
140}
141
142impl std::fmt::Display for LineStyle {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        match self {
145            Self::Solid => write!(f, "Solid"),
146            Self::Dotted { spacing } => write!(f, "Dotted({spacing} px)"),
147            Self::Dashed { length } => write!(f, "Dashed({length} px)"),
148        }
149    }
150}
151
152// ----------------------------------------------------------------------------
153
154/// Determines whether a plot element is vertically or horizontally oriented.
155#[derive(Copy, Clone, Debug, PartialEq, Eq)]
156pub enum Orientation {
157    Horizontal,
158    Vertical,
159}
160
161impl Default for Orientation {
162    fn default() -> Self {
163        Self::Vertical
164    }
165}
166
167// ----------------------------------------------------------------------------
168
169/// Represents many [`PlotPoint`]s.
170///
171/// These can be an owned `Vec`
172/// or generated on-the-fly by a function
173/// or borrowed from a slice.
174pub enum PlotPoints<'a> {
175    Owned(Vec<PlotPoint>),
176    Generator(ExplicitGenerator<'a>),
177    Borrowed(&'a [PlotPoint]),
178}
179
180impl Default for PlotPoints<'_> {
181    fn default() -> Self {
182        Self::Owned(Vec::new())
183    }
184}
185
186impl From<[f64; 2]> for PlotPoints<'_> {
187    fn from(coordinate: [f64; 2]) -> Self {
188        Self::new(vec![coordinate])
189    }
190}
191
192impl From<Vec<[f64; 2]>> for PlotPoints<'_> {
193    #[inline]
194    fn from(coordinates: Vec<[f64; 2]>) -> Self {
195        Self::new(coordinates)
196    }
197}
198
199impl<'a> From<&'a [PlotPoint]> for PlotPoints<'a> {
200    #[inline]
201    fn from(points: &'a [PlotPoint]) -> Self {
202        Self::Borrowed(points)
203    }
204}
205
206impl FromIterator<[f64; 2]> for PlotPoints<'_> {
207    fn from_iter<T: IntoIterator<Item = [f64; 2]>>(iter: T) -> Self {
208        Self::Owned(iter.into_iter().map(|point| point.into()).collect())
209    }
210}
211
212impl<'a> PlotPoints<'a> {
213    pub fn new(points: Vec<[f64; 2]>) -> Self {
214        Self::from_iter(points)
215    }
216
217    pub fn points(&self) -> &[PlotPoint] {
218        match self {
219            Self::Owned(points) => points.as_slice(),
220            Self::Generator(_) => &[],
221            Self::Borrowed(points) => points,
222        }
223    }
224
225    /// Draw a line based on a function `y=f(x)`, a range (which can be infinite) for x and the number of points.
226    pub fn from_explicit_callback(
227        function: impl Fn(f64) -> f64 + 'a,
228        x_range: impl RangeBounds<f64>,
229        points: usize,
230    ) -> Self {
231        let start = match x_range.start_bound() {
232            Bound::Included(x) | Bound::Excluded(x) => *x,
233            Bound::Unbounded => f64::NEG_INFINITY,
234        };
235        let end = match x_range.end_bound() {
236            Bound::Included(x) | Bound::Excluded(x) => *x,
237            Bound::Unbounded => f64::INFINITY,
238        };
239        let x_range = start..=end;
240
241        let generator = ExplicitGenerator {
242            function: Box::new(function),
243            x_range,
244            points,
245        };
246
247        Self::Generator(generator)
248    }
249
250    /// Draw a line based on a function `(x,y)=f(t)`, a range for t and the number of points.
251    /// The range may be specified as start..end or as start..=end.
252    pub fn from_parametric_callback(
253        function: impl Fn(f64) -> (f64, f64),
254        t_range: impl RangeBounds<f64>,
255        points: usize,
256    ) -> Self {
257        let start = match t_range.start_bound() {
258            Bound::Included(x) => x,
259            Bound::Excluded(_) => unreachable!(),
260            Bound::Unbounded => panic!("The range for parametric functions must be bounded!"),
261        };
262        let end = match t_range.end_bound() {
263            Bound::Included(x) | Bound::Excluded(x) => x,
264            Bound::Unbounded => panic!("The range for parametric functions must be bounded!"),
265        };
266        let last_point_included = matches!(t_range.end_bound(), Bound::Included(_));
267        let increment = if last_point_included {
268            (end - start) / (points - 1) as f64
269        } else {
270            (end - start) / points as f64
271        };
272        (0..points)
273            .map(|i| {
274                let t = start + i as f64 * increment;
275                function(t).into()
276            })
277            .collect()
278    }
279
280    /// From a series of y-values.
281    /// The x-values will be the indices of these values
282    pub fn from_ys_f32(ys: &[f32]) -> Self {
283        ys.iter()
284            .enumerate()
285            .map(|(i, &y)| [i as f64, y as f64])
286            .collect()
287    }
288
289    /// From a series of y-values.
290    /// The x-values will be the indices of these values
291    pub fn from_ys_f64(ys: &[f64]) -> Self {
292        ys.iter().enumerate().map(|(i, &y)| [i as f64, y]).collect()
293    }
294
295    /// Returns true if there are no data points available and there is no function to generate any.
296    pub(crate) fn is_empty(&self) -> bool {
297        match self {
298            Self::Owned(points) => points.is_empty(),
299            Self::Generator(_) => false,
300            Self::Borrowed(points) => points.is_empty(),
301        }
302    }
303
304    /// If initialized with a generator function, this will generate `n` evenly spaced points in the
305    /// given range.
306    pub(super) fn generate_points(&mut self, x_range: RangeInclusive<f64>) {
307        if let Self::Generator(generator) = self {
308            *self = Self::range_intersection(&x_range, &generator.x_range)
309                .map(|intersection| {
310                    let increment =
311                        (intersection.end() - intersection.start()) / (generator.points - 1) as f64;
312                    (0..generator.points)
313                        .map(|i| {
314                            let x = intersection.start() + i as f64 * increment;
315                            let y = (generator.function)(x);
316                            [x, y]
317                        })
318                        .collect()
319                })
320                .unwrap_or_default();
321        }
322    }
323
324    /// Returns the intersection of two ranges if they intersect.
325    fn range_intersection(
326        range1: &RangeInclusive<f64>,
327        range2: &RangeInclusive<f64>,
328    ) -> Option<RangeInclusive<f64>> {
329        let start = range1.start().max(*range2.start());
330        let end = range1.end().min(*range2.end());
331        (start < end).then_some(start..=end)
332    }
333
334    pub(super) fn bounds(&self) -> PlotBounds {
335        match self {
336            Self::Owned(points) => {
337                let mut bounds = PlotBounds::NOTHING;
338                for point in points {
339                    bounds.extend_with(point);
340                }
341                bounds
342            }
343            Self::Generator(generator) => generator.estimate_bounds(),
344            Self::Borrowed(points) => {
345                let mut bounds = PlotBounds::NOTHING;
346                for point in *points {
347                    bounds.extend_with(point);
348                }
349                bounds
350            }
351        }
352    }
353}
354
355// ----------------------------------------------------------------------------
356
357/// Circle, Diamond, Square, Cross, …
358#[derive(Debug, PartialEq, Eq, Clone, Copy)]
359pub enum MarkerShape {
360    Circle,
361    Diamond,
362    Square,
363    Cross,
364    Plus,
365    Up,
366    Down,
367    Left,
368    Right,
369    Asterisk,
370}
371
372impl MarkerShape {
373    /// Get a vector containing all marker shapes.
374    pub fn all() -> impl ExactSizeIterator<Item = Self> {
375        [
376            Self::Circle,
377            Self::Diamond,
378            Self::Square,
379            Self::Cross,
380            Self::Plus,
381            Self::Up,
382            Self::Down,
383            Self::Left,
384            Self::Right,
385            Self::Asterisk,
386        ]
387        .iter()
388        .copied()
389    }
390}
391
392// ----------------------------------------------------------------------------
393
394/// Query the points of the plot, for geometric relations like closest checks
395pub enum PlotGeometry<'a> {
396    /// No geometry based on single elements (examples: text, image, horizontal/vertical line)
397    None,
398
399    /// Point values (X-Y graphs)
400    Points(&'a [PlotPoint]),
401
402    /// Rectangles (examples: boxes or bars)
403    // Has currently no data, as it would require copying rects or iterating a list of pointers.
404    // Instead, geometry-based functions are directly implemented in the respective PlotItem impl.
405    Rects,
406}
407
408// ----------------------------------------------------------------------------
409
410/// Describes a function y = f(x) with an optional range for x and a number of points.
411pub struct ExplicitGenerator<'a> {
412    function: Box<dyn Fn(f64) -> f64 + 'a>,
413    x_range: RangeInclusive<f64>,
414    points: usize,
415}
416
417impl ExplicitGenerator<'_> {
418    fn estimate_bounds(&self) -> PlotBounds {
419        let mut bounds = PlotBounds::NOTHING;
420
421        let mut add_x = |x: f64| {
422            // avoid infinities, as we cannot auto-bound on them!
423            if x.is_finite() {
424                bounds.extend_with_x(x);
425            }
426            let y = (self.function)(x);
427            if y.is_finite() {
428                bounds.extend_with_y(y);
429            }
430        };
431
432        let min_x = *self.x_range.start();
433        let max_x = *self.x_range.end();
434
435        add_x(min_x);
436        add_x(max_x);
437
438        if min_x.is_finite() && max_x.is_finite() {
439            // Sample some points in the interval:
440            const N: u32 = 8;
441            for i in 1..N {
442                let t = i as f64 / (N - 1) as f64;
443                let x = lerp(min_x..=max_x, t);
444                add_x(x);
445            }
446        } else {
447            // Try adding some points anyway:
448            for x in [-1, 0, 1] {
449                let x = x as f64;
450                if min_x <= x && x <= max_x {
451                    add_x(x);
452                }
453            }
454        }
455
456        bounds
457    }
458}
459
460// ----------------------------------------------------------------------------
461
462/// Result of [`super::PlotItem::find_closest()`] search, identifies an element inside the item for immediate use
463pub struct ClosestElem {
464    /// Position of hovered-over value (or bar/box-plot/…) in `PlotItem`
465    pub index: usize,
466
467    /// Squared distance from the mouse cursor (needed to compare against other `PlotItems`, which might be nearer)
468    pub dist_sq: f32,
469}