#[derive(Debug, Clone, Copy)]
pub struct Linear {
pub min: f64,
pub max: f64,
pub step: f64,
}
impl Linear {
pub fn nice_from(mut min: f64, mut max: f64, target_ticks: usize, include_zero: bool) -> Self {
if include_zero {
min = min.min(0.0);
max = max.max(0.0);
}
if !(max - min).is_normal() {
max = min + 1.0;
}
let step = nice_num((max - min) / (target_ticks.max(2) - 1) as f64, true);
Linear {
min: (min / step).floor() * step,
max: (max / step).ceil() * step,
step,
}
}
pub fn row_aligned(
mut min: f64,
mut max: f64,
target_ticks: usize,
rows: usize,
include_zero: bool,
) -> Self {
if include_zero {
min = min.min(0.0);
max = max.max(0.0);
}
if !(max - min).is_normal() {
max = min + 1.0;
}
let intervals = rows.max(3) - 1;
let step0 = nice_num((max - min) / (target_ticks.max(2) - 1) as f64, true);
let mut step = step0;
loop {
let lo = (min / step).floor() * step;
let hi = (max / step).ceil() * step;
let k = ((hi - lo) / step).round() as usize;
if k >= 1 && intervals.is_multiple_of(k) && intervals / k >= 2 {
return Linear {
min: lo,
max: hi,
step,
};
}
if k <= 2 {
let lo = (min / step0).floor() * step0;
let hi = (max / step0).ceil() * step0;
return Linear {
min: lo,
max: hi,
step: hi - lo,
};
}
step = next_nice(step);
}
}
pub fn indices(n: usize) -> Self {
Linear {
min: 0.0,
max: (n.saturating_sub(1)).max(1) as f64,
step: 1.0,
}
}
pub fn ticks(&self) -> Vec<f64> {
let n = ((self.max - self.min) / self.step).round() as usize;
(0..=n).map(|i| self.min + i as f64 * self.step).collect()
}
pub fn norm(&self, v: f64) -> f64 {
(v - self.min) / (self.max - self.min)
}
}
fn nice_num(x: f64, round: bool) -> f64 {
let exp = x.log10().floor();
let pow = 10f64.powf(exp);
let f = x / pow;
let nf = if round {
if f < 1.5 {
1.0
} else if f < 3.0 {
2.0
} else if f < 7.0 {
5.0
} else {
10.0
}
} else if f <= 1.0 {
1.0
} else if f <= 2.0 {
2.0
} else if f <= 5.0 {
5.0
} else {
10.0
};
nf * pow
}
fn next_nice(step: f64) -> f64 {
let exp = step.log10().floor();
let pow = 10f64.powf(exp);
let f = (step / pow).round();
if f < 2.0 {
2.0 * pow
} else if f < 5.0 {
5.0 * pow
} else {
10.0 * pow
}
}
pub fn fmt_tick(v: f64, step: f64) -> String {
let av = v.abs();
if av >= 1e9 {
return trim_zeros(format!("{:.1}", v / 1e9)) + "G";
}
if av >= 1e6 {
return trim_zeros(format!("{:.1}", v / 1e6)) + "M";
}
if av >= 1e4 {
return trim_zeros(format!("{:.1}", v / 1e3)) + "k";
}
let decimals = if step >= 1.0 {
0
} else {
(-step.log10().floor()) as usize
};
format!("{v:.decimals$}")
}
fn trim_zeros(s: String) -> String {
if s.ends_with(".0") {
s[..s.len() - 2].to_string()
} else {
s
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nice_ticks_cover_domain() {
let s = Linear::nice_from(0.0, 9.4, 5, true);
assert_eq!(s.min, 0.0);
assert_eq!(s.max, 10.0);
assert_eq!(s.ticks(), vec![0.0, 2.0, 4.0, 6.0, 8.0, 10.0]);
}
#[test]
fn fractional_steps_get_decimals() {
let s = Linear::nice_from(0.0, 1.7, 5, true);
assert!(s.step < 1.0);
assert_eq!(fmt_tick(0.5, s.step), "0.5");
}
#[test]
fn large_values_humanize() {
assert_eq!(fmt_tick(2_500_000.0, 500_000.0), "2.5M");
assert_eq!(fmt_tick(40_000.0, 10_000.0), "40k");
}
#[test]
fn row_aligned_keeps_fine_step_when_even() {
let s = Linear::row_aligned(0.0, 6.0, 6, 13, true);
assert_eq!((s.min, s.max, s.step), (0.0, 6.0, 1.0));
}
#[test]
fn row_aligned_coarsens_to_divide_rows() {
let s = Linear::row_aligned(0.0, 6.0, 6, 10, true);
assert_eq!((s.min, s.max, s.step), (0.0, 6.0, 2.0));
}
#[test]
fn row_aligned_climbs_ladder_past_domain_inflation() {
let s = Linear::row_aligned(0.0, 160.0, 6, 7, true);
assert_eq!((s.min, s.max, s.step), (0.0, 200.0, 100.0));
}
#[test]
fn row_aligned_min_spacing_forces_fallback() {
let s = Linear::row_aligned(0.0, 10.0, 6, 6, true);
assert_eq!((s.min, s.max, s.step), (0.0, 10.0, 10.0));
assert_eq!(s.ticks(), vec![0.0, 10.0]);
}
#[test]
fn row_aligned_zero_straddle_terminates() {
let s = Linear::row_aligned(-20.0, 30.0, 6, 6, false);
assert_eq!((s.min, s.max, s.step), (-20.0, 30.0, 50.0));
assert_eq!(s.ticks(), vec![-20.0, 30.0]);
}
#[test]
fn row_aligned_negative_domain_no_zero() {
let s = Linear::row_aligned(-20.0, 30.0, 6, 13, false);
assert_eq!((s.min, s.max, s.step), (-20.0, 40.0, 20.0));
}
#[test]
fn row_aligned_fractional_step() {
let s = Linear::row_aligned(0.0, 1.7, 6, 13, true);
assert_eq!((s.min, s.max, s.step), (0.0, 2.0, 0.5));
}
}