maelstrom_plot/items/
values.rs

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