1use std::cmp::Ordering;
2
3type GridSpacerFn<'a> = dyn Fn(GridInput) -> Vec<GridMark> + 'a;
4pub type GridSpacer<'a> = Box<GridSpacerFn<'a>>;
5
6pub struct GridInput {
10 pub bounds: (f64, f64),
13
14 pub base_step_size: f64,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct GridMark {
28 pub value: f64,
30
31 pub step_size: f64,
39}
40
41pub 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 if input.base_step_size.abs() < f64::EPSILON {
50 return Vec::new();
51 }
52
53 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
69pub 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
88fn next_power(value: f64, base: f64) -> f64 {
97 debug_assert_ne!(value, 0.0, "Bad input"); base.powi(value.abs().log(base).ceil() as i32)
99}
100
101fn 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 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; 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 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
194fn 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}