use std::cmp::Ordering;
type GridSpacerFn<'a> = dyn Fn(GridInput) -> Vec<GridMark> + 'a;
pub type GridSpacer<'a> = Box<GridSpacerFn<'a>>;
pub struct GridInput {
pub bounds: (f64, f64),
pub base_step_size: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GridMark {
pub value: f64,
pub step_size: f64,
}
pub fn log_grid_spacer(log_base: i64) -> GridSpacer<'static> {
let log_base = log_base as f64;
let step_sizes = move |input: GridInput| -> Vec<GridMark> {
if input.base_step_size.abs() < f64::EPSILON {
return Vec::new();
}
let smallest_visible_unit = next_power(input.base_step_size, log_base);
let step_sizes = [
smallest_visible_unit,
smallest_visible_unit * log_base,
smallest_visible_unit * log_base * log_base,
];
generate_marks(step_sizes, input.bounds)
};
Box::new(step_sizes)
}
pub fn uniform_grid_spacer<'a>(spacer: impl Fn(GridInput) -> [f64; 3] + 'a) -> GridSpacer<'a> {
let get_marks = move |input: GridInput| -> Vec<GridMark> {
let bounds = input.bounds;
let step_sizes = spacer(input);
generate_marks(step_sizes, bounds)
};
Box::new(get_marks)
}
fn next_power(value: f64, base: f64) -> f64 {
debug_assert_ne!(value, 0.0, "Bad input"); base.powi(value.abs().log(base).ceil() as i32)
}
fn generate_marks(step_sizes: [f64; 3], bounds: (f64, f64)) -> Vec<GridMark> {
let mut steps = vec![];
fill_marks_between(&mut steps, step_sizes[0], bounds);
fill_marks_between(&mut steps, step_sizes[1], bounds);
fill_marks_between(&mut steps, step_sizes[2], bounds);
steps.sort_by(|a, b| cmp_f64(a.value, b.value));
let min_step = step_sizes.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let eps = 0.1 * min_step;
let mut deduplicated: Vec<GridMark> = Vec::with_capacity(steps.len());
for step in steps {
if let Some(last) = deduplicated.last_mut()
&& (last.value - step.value).abs() < eps
{
if last.step_size < step.step_size {
*last = step;
}
continue;
}
deduplicated.push(step);
}
deduplicated
}
#[test]
fn test_generate_marks() {
fn approx_eq(a: &GridMark, b: &GridMark) -> bool {
(a.value - b.value).abs() < 1e-10 && a.step_size == b.step_size
}
let gm = |value, step_size| GridMark { value, step_size };
let marks = generate_marks([0.01, 0.1, 1.0], (2.855, 3.015));
let expected = vec![
gm(2.86, 0.01),
gm(2.87, 0.01),
gm(2.88, 0.01),
gm(2.89, 0.01),
gm(2.90, 0.1),
gm(2.91, 0.01),
gm(2.92, 0.01),
gm(2.93, 0.01),
gm(2.94, 0.01),
gm(2.95, 0.01),
gm(2.96, 0.01),
gm(2.97, 0.01),
gm(2.98, 0.01),
gm(2.99, 0.01),
gm(3.00, 1.),
gm(3.01, 0.01),
];
let mut problem = if marks.len() == expected.len() {
None
} else {
Some(format!(
"Different lengths: got {}, expected {}",
marks.len(),
expected.len()
))
};
for (i, (a, b)) in marks.iter().zip(&expected).enumerate() {
if !approx_eq(a, b) {
problem = Some(format!("Mismatch at index {i}: {a:?} != {b:?}"));
break;
}
}
if let Some(problem) = problem {
panic!("Test failed: {problem}. Got: {marks:#?}, expected: {expected:#?}");
}
}
fn cmp_f64(a: f64, b: f64) -> Ordering {
match a.partial_cmp(&b) {
Some(ord) => ord,
None => a.is_nan().cmp(&b.is_nan()),
}
}
fn fill_marks_between(out: &mut Vec<GridMark>, step_size: f64, (min, max): (f64, f64)) {
debug_assert!(min <= max, "Bad plot bounds: min: {min}, max: {max}");
let first = (min / step_size).ceil() as i64;
let last = (max / step_size).ceil() as i64;
let marks_iter = (first..last).map(|i| {
let value = (i as f64) * step_size;
GridMark { value, step_size }
});
out.extend(marks_iter);
}