const NICE_STEPS: [f64; 5] = [1.0, 2.0, 2.5, 5.0, 10.0];
pub fn nice(domain: (f64, f64), target: usize) -> Vec<f64> {
let (mut lo, mut hi) = domain;
if lo > hi {
std::mem::swap(&mut lo, &mut hi);
}
if lo == hi || target < 2 {
return vec![lo];
}
let step = nice_step((hi - lo) / target as f64);
let start = (lo / step).ceil() * step;
let stop = (hi / step).floor() * step;
let n = ((stop - start) / step).round() as i64;
if n < 0 {
return Vec::new();
}
let decimals = ((-floor_log10(step)).max(0.0) as i32 + 2) as i32;
(0..=n)
.map(|i| round_to_decimals(start + i as f64 * step, decimals))
.collect()
}
pub fn log_nice(domain: (f64, f64), base: f64) -> Vec<f64> {
let (mut lo, mut hi) = domain;
if lo > hi {
std::mem::swap(&mut lo, &mut hi);
}
if lo <= 0.0 || hi <= 0.0 || lo == hi {
return Vec::new();
}
let lb = base.ln();
let lo_exp = (lo.ln() / lb).floor() as i32;
let hi_exp = (hi.ln() / lb).ceil() as i32;
(lo_exp..=hi_exp)
.map(|e| base.powi(e))
.filter(|v| *v >= lo && *v <= hi)
.collect()
}
pub fn format(v: f64) -> String {
if !v.is_finite() {
return "—".to_string();
}
if v == v.trunc() && v.abs() < 1e16 {
return format!("{}", v as i64);
}
let abs = v.abs();
if abs >= 1_000_000.0 || (abs < 0.001 && v != 0.0) {
return format!("{:.2e}", v);
}
let s = format!("{:.3}", v);
let trimmed = s.trim_end_matches('0').trim_end_matches('.');
trimmed.to_string()
}
fn nice_step(raw: f64) -> f64 {
if raw <= 0.0 {
return 1.0;
}
let exp = raw.log10().floor();
let pow = 10f64.powf(exp);
let fraction = raw / pow;
let step_frac = NICE_STEPS
.iter()
.copied()
.find(|s| *s >= fraction)
.unwrap_or(*NICE_STEPS.last().unwrap());
step_frac * pow
}
fn floor_log10(x: f64) -> f64 {
if x > 0.0 {
x.log10().floor()
} else {
0.0
}
}
fn round_to_decimals(value: f64, decimals: i32) -> f64 {
let pow = 10f64.powi(decimals);
(value * pow).round() / pow
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nice_picks_round_steps() {
let ticks = nice((0.0, 9.7), 5);
assert_eq!(&ticks[..3], &[0.0, 2.0, 4.0]);
}
#[test]
fn log_picks_powers_of_ten() {
assert_eq!(log_nice((0.5, 250.0), 10.0), vec![1.0, 10.0, 100.0]);
}
#[test]
fn format_strips_trailing_zeros() {
assert_eq!(format(1.5), "1.5");
assert_eq!(format(2.0), "2");
}
}