fn nice_num(x: f32, round: bool) -> f32 {
if x <= 0.0 || !x.is_finite() {
return 1.0;
}
let exp = x.log10().floor();
let base = 10.0_f32.powf(exp);
let frac = x / base;
let nice = if round {
match frac {
f if f < 1.5 => 1.0,
f if f < 3.0 => 2.0,
f if f < 7.0 => 5.0,
_ => 10.0,
}
} else {
match frac {
f if f <= 1.0 => 1.0,
f if f <= 2.0 => 2.0,
f if f <= 5.0 => 5.0,
_ => 10.0,
}
};
nice * base
}
pub fn nice_ticks(lo: f32, hi: f32, target: usize) -> Vec<f32> {
let (lo, hi) = if lo.is_finite() && hi.is_finite() && hi > lo {
(lo, hi)
} else if lo.is_finite() && lo == hi {
(lo.floor(), lo.floor() + 1.0)
} else {
(0.0, 1.0)
};
let range = nice_num(hi - lo, false);
let step = nice_num(range / target.max(1) as f32, true);
let start = (lo / step).floor() * step;
let end = (hi / step).ceil() * step;
let count = ((end - start) / step).round() as usize;
(0..=count).map(|i| start + i as f32 * step).collect()
}
pub fn tick_label(value: f32) -> String {
let (scaled, suffix) = if value.abs() >= 1_000_000.0 {
(value / 1_000_000.0, "M")
} else if value.abs() >= 1_000.0 {
(value / 1_000.0, "k")
} else {
(value, "")
};
let text = format!("{scaled:.2}");
let text = text.trim_end_matches('0').trim_end_matches('.');
format!("{text}{suffix}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ticks_cover_the_range_with_round_steps() {
assert_eq!(nice_ticks(0.0, 100.0, 5), vec![0.0, 20.0, 40.0, 60.0, 80.0, 100.0]);
assert_eq!(nice_ticks(0.0, 7.0, 5), vec![0.0, 2.0, 4.0, 6.0, 8.0]);
let ticks = nice_ticks(-3.0, 14.0, 5);
assert_eq!(ticks, vec![-5.0, 0.0, 5.0, 10.0, 15.0]);
}
#[test]
fn ticks_expand_outward() {
let ticks = nice_ticks(1.2, 9.7, 5);
assert!(*ticks.first().unwrap() <= 1.2);
assert!(*ticks.last().unwrap() >= 9.7);
for pair in ticks.windows(2) {
assert!(pair[1] > pair[0]);
}
}
#[test]
fn small_and_fractional_ranges_stay_round() {
let ticks = nice_ticks(0.0, 0.9, 4);
assert_eq!(ticks, vec![0.0, 0.2, 0.4, 0.6, 0.8, 1.0]);
}
#[test]
fn degenerate_ranges_fall_back() {
assert_eq!(nice_ticks(5.0, 5.0, 4).len() >= 2, true);
assert_eq!(nice_ticks(f32::NAN, 3.0, 4), nice_ticks(0.0, 1.0, 4));
assert!(nice_ticks(9.0, 2.0, 4).len() >= 2);
}
#[test]
fn labels_are_compact() {
assert_eq!(tick_label(0.0), "0");
assert_eq!(tick_label(2.5), "2.5");
assert_eq!(tick_label(20.0), "20");
assert_eq!(tick_label(1500.0), "1.5k");
assert_eq!(tick_label(2_000_000.0), "2M");
assert_eq!(tick_label(-500.0), "-500");
assert_eq!(tick_label(-1500.0), "-1.5k");
}
}