Skip to main content

egui_plot/
grid.rs

1use std::cmp::Ordering;
2
3type GridSpacerFn<'a> = dyn Fn(GridInput) -> Vec<GridMark> + 'a;
4pub type GridSpacer<'a> = Box<GridSpacerFn<'a>>;
5
6/// Input for "grid spacer" functions.
7///
8/// See [`crate::Plot::x_grid_spacer()`] and [`crate::Plot::y_grid_spacer()`].
9pub struct GridInput {
10    /// Min/max of the visible data range (the values at the two edges of the
11    /// plot, for the current axis).
12    pub bounds: (f64, f64),
13
14    /// Recommended (but not required) lower-bound on the step size returned by
15    /// custom grid spacers.
16    ///
17    /// Computed as the ratio between the diagram's bounds (in plot coordinates)
18    /// and the viewport (in frame/window coordinates), scaled up to
19    /// represent the minimal possible step.
20    ///
21    /// Always positive.
22    pub base_step_size: f64,
23}
24
25/// One mark (horizontal or vertical line) in the background grid of a plot.
26#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct GridMark {
28    /// X or Y value in the plot.
29    pub value: f64,
30
31    /// The (approximate) distance to the next value of same thickness.
32    ///
33    /// Determines how thick the grid line is painted. It's not important that
34    /// `step_size` matches the difference between two `value`s precisely,
35    /// but rather that grid marks of same thickness have same `step_size`.
36    /// For example, months can have a different number of days, but
37    /// consistently using a `step_size` of 30 days is a valid approximation.
38    pub step_size: f64,
39}
40
41/// Recursively splits the grid into `base` subdivisions (e.g. 100, 10, 1).
42///
43/// The logarithmic base, expressing how many times each grid unit is
44/// subdivided. 10 is a typical value, others are possible though.
45pub fn log_grid_spacer(log_base: i64) -> GridSpacer<'static> {
46    let log_base = log_base as f64;
47    let step_sizes = move |input: GridInput| -> Vec<GridMark> {
48        // handle degenerate cases
49        if input.base_step_size.abs() < f64::EPSILON {
50            return Vec::new();
51        }
52
53        // The distance between two of the thinnest grid lines is "rounded" up
54        // to the next-bigger power of base
55        let smallest_visible_unit = next_power(input.base_step_size, log_base);
56
57        let step_sizes = [
58            smallest_visible_unit,
59            smallest_visible_unit * log_base,
60            smallest_visible_unit * log_base * log_base,
61        ];
62
63        generate_marks(step_sizes, input.bounds)
64    };
65
66    Box::new(step_sizes)
67}
68
69/// Splits the grid into uniform-sized spacings (e.g. 100, 25, 1).
70///
71/// This function should return 3 positive step sizes, designating where the
72/// lines in the grid are drawn. Lines are thicker for larger step sizes.
73/// Ordering of returned value is irrelevant.
74///
75/// Why only 3 step sizes? Three is the number of different line thicknesses
76/// that egui typically uses in the grid. Ideally, those 3 are not hardcoded
77/// values, but depend on the visible range (accessible through `GridInput`).
78pub fn uniform_grid_spacer<'a>(spacer: impl Fn(GridInput) -> [f64; 3] + 'a) -> GridSpacer<'a> {
79    let get_marks = move |input: GridInput| -> Vec<GridMark> {
80        let bounds = input.bounds;
81        let step_sizes = spacer(input);
82        generate_marks(step_sizes, bounds)
83    };
84
85    Box::new(get_marks)
86}
87
88/// Returns next bigger power in given base
89/// e.g.
90/// ```ignore
91/// use egui_plot::next_power;
92/// assert_eq!(next_power(0.01, 10.0), 0.01);
93/// assert_eq!(next_power(0.02, 10.0), 0.1);
94/// assert_eq!(next_power(0.2,  10.0), 1);
95/// ```
96fn next_power(value: f64, base: f64) -> f64 {
97    debug_assert_ne!(value, 0.0, "Bad input"); // can be negative (typical for Y axis)
98    base.powi(value.abs().log(base).ceil() as i32)
99}
100
101/// Fill in all values between [min, max] which are a multiple of `step_size`
102fn generate_marks(step_sizes: [f64; 3], bounds: (f64, f64)) -> Vec<GridMark> {
103    let mut steps = vec![];
104    fill_marks_between(&mut steps, step_sizes[0], bounds);
105    fill_marks_between(&mut steps, step_sizes[1], bounds);
106    fill_marks_between(&mut steps, step_sizes[2], bounds);
107
108    // Remove duplicates:
109    // This can happen because we have overlapping steps, e.g.:
110    // step_size[0] =   10  =>  [-10, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100,
111    // 110, 120] step_size[1] =  100  =>  [     0,
112    // 100          ] step_size[2] = 1000  =>  [     0
113    // ]
114
115    steps.sort_by(|a, b| cmp_f64(a.value, b.value));
116
117    let min_step = step_sizes.iter().fold(f64::INFINITY, |a, &b| a.min(b));
118    let eps = 0.1 * min_step; // avoid putting two ticks too closely together
119
120    let mut deduplicated: Vec<GridMark> = Vec::with_capacity(steps.len());
121    for step in steps {
122        if let Some(last) = deduplicated.last_mut()
123            && (last.value - step.value).abs() < eps
124        {
125            // Keep the one with the largest step size
126            if last.step_size < step.step_size {
127                *last = step;
128            }
129            continue;
130        }
131        deduplicated.push(step);
132    }
133
134    deduplicated
135}
136
137#[test]
138fn test_generate_marks() {
139    fn approx_eq(a: &GridMark, b: &GridMark) -> bool {
140        (a.value - b.value).abs() < 1e-10 && a.step_size == b.step_size
141    }
142
143    let gm = |value, step_size| GridMark { value, step_size };
144
145    let marks = generate_marks([0.01, 0.1, 1.0], (2.855, 3.015));
146    let expected = vec![
147        gm(2.86, 0.01),
148        gm(2.87, 0.01),
149        gm(2.88, 0.01),
150        gm(2.89, 0.01),
151        gm(2.90, 0.1),
152        gm(2.91, 0.01),
153        gm(2.92, 0.01),
154        gm(2.93, 0.01),
155        gm(2.94, 0.01),
156        gm(2.95, 0.01),
157        gm(2.96, 0.01),
158        gm(2.97, 0.01),
159        gm(2.98, 0.01),
160        gm(2.99, 0.01),
161        gm(3.00, 1.),
162        gm(3.01, 0.01),
163    ];
164
165    let mut problem = if marks.len() == expected.len() {
166        None
167    } else {
168        Some(format!(
169            "Different lengths: got {}, expected {}",
170            marks.len(),
171            expected.len()
172        ))
173    };
174
175    for (i, (a, b)) in marks.iter().zip(&expected).enumerate() {
176        if !approx_eq(a, b) {
177            problem = Some(format!("Mismatch at index {i}: {a:?} != {b:?}"));
178            break;
179        }
180    }
181
182    if let Some(problem) = problem {
183        panic!("Test failed: {problem}. Got: {marks:#?}, expected: {expected:#?}");
184    }
185}
186
187fn cmp_f64(a: f64, b: f64) -> Ordering {
188    match a.partial_cmp(&b) {
189        Some(ord) => ord,
190        None => a.is_nan().cmp(&b.is_nan()),
191    }
192}
193
194/// Fill in all values between [min, max] which are a multiple of `step_size`
195fn fill_marks_between(out: &mut Vec<GridMark>, step_size: f64, (min, max): (f64, f64)) {
196    debug_assert!(min <= max, "Bad plot bounds: min: {min}, max: {max}");
197    let first = (min / step_size).ceil() as i64;
198    let last = (max / step_size).ceil() as i64;
199
200    let marks_iter = (first..last).map(|i| {
201        let value = (i as f64) * step_size;
202        GridMark { value, step_size }
203    });
204    out.extend(marks_iter);
205}