#[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
}
}
#[derive(Debug, Clone, Copy)]
pub struct Bins {
pub lo: f64,
pub step: f64,
pub n: usize,
}
impl Bins {
pub fn hi(&self) -> f64 {
self.lo + self.step * self.n as f64
}
pub fn index(&self, x: f64) -> usize {
let k = ((x - self.lo) / self.step).floor();
if k <= 0.0 {
0
} else if k as usize >= self.n {
self.n - 1
} else {
k as usize
}
}
}
fn expand(min: f64, max: f64, step: f64) -> Bins {
let lo = (min / step).floor() * step;
let hi = (max / step).ceil() * step;
Bins {
lo,
step,
n: (((hi - lo) / step).round() as usize).max(1),
}
}
pub fn bins_auto(min: f64, mut max: f64, target: usize) -> Bins {
debug_assert!(target >= 1);
debug_assert!(min <= max);
if !(max - min).is_normal() {
max = min + 1.0;
}
expand(min, max, nice_num((max - min) / target as f64, false))
}
pub fn bins_maxbins(min: f64, mut max: f64, n: usize) -> Bins {
debug_assert!(n >= 1);
debug_assert!(min <= max);
if !(max - min).is_normal() {
max = min + 1.0;
}
let mut step = nice_num((max - min) / n as f64, false);
let mut bins = expand(min, max, step);
while bins.n > n {
if min < 0.0 && max > 0.0 && bins.n <= 2 {
break;
}
step = next_nice(step);
bins = expand(min, max, step);
}
bins
}
pub fn bins_step(min: f64, mut max: f64, step: f64) -> Bins {
debug_assert!(step > 0.0);
debug_assert!(min <= max);
if !(max - min).is_normal() {
max = min + 1.0;
}
expand(min, max, step)
}
pub fn cell_edges(n: usize, plot_w: usize) -> Vec<usize> {
debug_assert!(n >= 1);
(0..=n)
.map(|k| (k as f64 / n as f64 * plot_w as f64).round() as usize)
.collect()
}
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));
}
#[test]
fn bins_auto_sweep_stays_nice() {
let spans = [
(0.0, 0.001),
(0.0, 1.0),
(0.0, 100.0),
(0.0, 1e9),
(-50.0, 50.0),
(-1000.0, -10.0),
(3.0, 97.0),
(-0.001, 0.002),
(12_345.0, 67_890.0),
];
for &(min, max) in &spans {
for target in 5..=20 {
let b = bins_auto(min, max, target);
let mantissa = b.step / 10f64.powf(b.step.log10().floor());
assert!(
[1.0, 2.0, 5.0, 10.0]
.iter()
.any(|m| (mantissa - m).abs() < 1e-6),
"step {} is not 1/2/5x10^k (min={min}, max={max}, target={target})",
b.step
);
let edge = b.lo / b.step;
assert!(
(edge - edge.round()).abs() < 1e-6,
"lo {} is not a multiple of step {}",
b.lo,
b.step
);
assert!(b.lo <= min + 1e-6 * b.step, "lo {} > min {min}", b.lo);
assert!(b.hi() >= max - 1e-6 * b.step, "hi {} < max {max}", b.hi());
assert!(
b.n >= 2 && b.n <= target + 2,
"count {} out of [2, {}] (min={min}, max={max})",
b.n,
target + 2
);
}
}
}
#[test]
fn bins_degenerate_span_contains_the_value() {
let b = bins_auto(7.3, 7.3, 8);
assert!(b.n >= 1);
assert!(b.lo <= 7.3 && 7.3 <= b.hi());
assert!(b.index(7.3) < b.n);
}
#[test]
fn bins_maxbins_coarsens_below_ceiling() {
let b = bins_maxbins(5.0, 95.0, 9);
assert!(b.n <= 9);
assert_eq!((b.lo, b.step, b.n), (0.0, 20.0, 5));
}
#[test]
fn bins_maxbins_never_exceeds_ceiling() {
let spans = [
(0.0, 0.001),
(0.0, 100.0),
(0.0, 1e9),
(-50.0, 50.0),
(-1000.0, -10.0),
(3.0, 97.0),
];
for &(min, max) in &spans {
for n in 2..=20 {
let b = bins_maxbins(min, max, n);
assert!(
b.n <= n,
"count {} exceeds maxbins {n} (min={min}, max={max})",
b.n
);
}
}
}
#[test]
fn bins_maxbins_one_terminates() {
let b = bins_maxbins(-50.0, 50.0, 1);
assert!(b.n <= 2);
assert_eq!(bins_maxbins(3.0, 97.0, 1).n, 1);
}
#[test]
fn bins_step_uses_width_verbatim() {
let b = bins_step(3.0, 97.0, 10.0);
assert_eq!((b.lo, b.step, b.n), (0.0, 10.0, 10));
assert_eq!(b.hi(), 100.0);
let edges: Vec<f64> = (0..=b.n).map(|k| b.lo + k as f64 * b.step).collect();
assert_eq!(
edges,
vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0]
);
}
#[test]
fn bins_index_is_half_open_final_closed() {
let b = bins_step(0.0, 100.0, 10.0);
assert_eq!((b.n, b.hi()), (10, 100.0));
assert_eq!(b.index(0.0), 0);
assert_eq!(b.index(10.0), 1);
assert_eq!(b.index(50.0), 5);
assert_eq!(b.index(9.999), 0);
assert_eq!(b.index(100.0), 9);
assert_eq!(b.index(99.999), 9);
assert_eq!(b.index(-5.0), 0);
assert_eq!(b.index(150.0), b.n - 1);
}
#[test]
fn cell_edges_tile_the_plot() {
assert_eq!(cell_edges(4, 10), vec![0, 3, 5, 8, 10]);
for &(n, w) in &[(1usize, 1usize), (3, 7), (5, 5), (7, 100), (1, 0), (4, 3)] {
let e = cell_edges(n, w);
assert_eq!(e.len(), n + 1);
assert_eq!(e[0], 0);
assert_eq!(*e.last().unwrap(), w);
assert!(e.windows(2).all(|p| p[0] <= p[1]), "not monotone: {e:?}");
}
}
}