use std::f64::consts::LN_10;
#[derive(Debug, Clone, Copy)]
pub struct TickSettings {
pub length_pt: f64,
pub max_space: f64,
pub try_min: usize,
pub distance: Option<f64>,
pub scale: Option<(f64, f64, f64)>,
}
fn order(v: f64) -> i32 {
v.abs().log10().floor() as i32 + 1
}
fn tex_distance(min: f64, max: f64, n: usize, (dmin, dmax, size): (f64, f64, f64)) -> f64 {
let f = data_scale(dmin, dmax, size);
let shift = dmin * f;
let sp = |v: f64| (v * 65536.0).round();
let lo = sp(min * f - shift);
let hi = sp(max * f - shift) + sp(0.001);
let h_sp = ((hi - lo).abs() / (n.max(2) - 1) as f64).trunc();
h_sp / 65536.0 / f
}
pub fn data_scale(dmin: f64, dmax: f64, size: f64) -> f64 {
let m = match (dmin == 0.0, dmax == 0.0) {
(true, true) => 1,
(true, false) => order(dmax),
(false, true) => order(dmin),
_ => order(dmin).max(order(dmax)),
};
let mut exp = order(size) - m;
for _ in 0..30 {
let f = 10f64.powi(exp);
let (tmax, diff) = ((dmax * f).abs(), (dmax - dmin) * f);
if tmax < 1500.0 && (0.0001..100.0).contains(&diff) {
exp += 1;
} else {
break;
}
}
10f64.powi(exp)
}
pub fn normalize_distance(h: f64) -> f64 {
if !(h.is_finite() && h > 0.0) {
return h;
}
let mut e = h.log10().floor();
let mut m = h / 10f64.powf(e);
if m >= 10.0 {
m /= 10.0;
e += 1.0;
} else if m < 1.0 {
m *= 10.0;
e -= 1.0;
}
let snapped = if m < 2.0 {
if m < 1.5 {
1.0
} else {
2.0
}
} else if m < 4.9999 {
if m < 3.5 {
2.0
} else {
5.0
}
} else if m < 7.5 {
5.0
} else {
e += 1.0;
1.0
};
snapped * 10f64.powf(e)
}
fn desired(s: &TickSettings) -> usize {
let n = if s.max_space > 0.0 { (s.length_pt / s.max_space).floor().max(0.0) as usize + 1 } else { 1 };
n.max(s.try_min)
}
pub fn linear(min: f64, max: f64, s: TickSettings) -> (Vec<f64>, f64) {
let mut s = s;
for _ in 0..15 {
let (h, fixed) = match s.distance {
Some(d) if d.is_finite() && d > 0.0 => (d.abs(), true),
_ => {
let n = desired(&s);
match s.scale.filter(|(a, b, z)| a.is_finite() && b.is_finite() && *z > 0.0) {
Some(sc) => (tex_distance(min, max, n, sc), false),
None => ((max - min).abs() / (n.max(2) - 1) as f64, false),
}
}
};
let h = if fixed { h } else { normalize_distance(h) };
if !(h.is_finite() && h > 0.0) {
return (Vec::new(), 0.0);
}
let first = (min / h).trunc() * h - h;
let top = max + 1e-9 * (max - min).abs().max(h);
if !fixed && top < first + 2.0 * h {
s.try_min += 1;
continue;
}
let mut ticks = Vec::new();
let mut k = 0usize;
loop {
let v = first + k as f64 * h;
if v > top + 0.5 * h || ticks.len() > 10_000 {
break;
}
ticks.push(clean(v, h));
k += 1;
}
return (ticks, h);
}
(Vec::new(), 0.0)
}
fn clean(v: f64, h: f64) -> f64 {
if v.abs() < h * 1e-9 {
return 0.0;
}
let scale = 10f64.powi((-(h.abs().log10().floor()) as i32 + 12).clamp(-300, 300));
let r = (v * scale).round() / scale;
if r.is_finite() {
r
} else {
v
}
}
pub fn logarithmic(min: f64, max: f64, s: TickSettings) -> (Vec<f64>, f64, bool) {
let n = desired(&s);
let range = (max - min).abs();
let (h_ten, uniform) = match s.distance {
Some(d) if d.is_finite() && d > 0.0 => (d.ln() / LN_10, false),
_ => {
let raw = range / (n.max(2) - 1) as f64 / LN_10;
let j = if raw < 2.0 { 1.0 } else { raw.floor() };
(j, j == 1.0)
}
};
let h = h_ten * LN_10;
if !(h.is_finite() && h > 0.0) {
return (Vec::new(), 0.0, false);
}
let m = min / LN_10;
let mut i = m.trunc();
if m < 0.0 {
i -= 1.0;
}
let mut first = i * LN_10;
if !uniform && 2.0 * (min - first) - h < 0.0 {
first += LN_10;
}
let top = max + 1e-9 + 0.5 * h;
let mut ticks = Vec::new();
let mut k = 0usize;
if max + 1e-9 < first + 2.0 * h && s.distance.is_none() {
let mut n2 = n.saturating_sub(1).max(1);
if n2 > 2 {
n2 -= 1;
}
let hh = normalize_distance(range / LN_10 / n2 as f64);
let mut ii = (min / LN_10 / hh).trunc();
if min < 0.0 {
ii -= 1.0;
}
let start = ii * hh * LN_10;
let step = hh * LN_10;
loop {
let v = start + k as f64 * step;
if v > max + 1e-9 + 0.5 * step || ticks.len() > 1000 {
break;
}
ticks.push(v);
k += 1;
}
return (ticks, step, false);
}
loop {
let v = first + k as f64 * h;
if v > top || ticks.len() > 10_000 {
break;
}
ticks.push(v);
k += 1;
}
(ticks, h, uniform)
}
pub fn scale_exponent(min: f64, max: f64, above: i32, below: i32) -> i32 {
let exp = |v: f64| if v == 0.0 || !v.is_finite() { None } else { Some(v.abs().log10().floor() as i32 + correction(v)) };
let (a, b) = match (exp(min), exp(max)) {
(None, None) => (0, 0),
(None, Some(b)) => (b, b),
(Some(a), None) => (a, a),
(Some(a), Some(b)) => (a, b),
};
let e = a.max(b);
if e > above || e < below {
-e
} else {
0
}
}
fn correction(v: f64) -> i32 {
let s = format!("{:e}", v.abs());
let e: i32 = s.split_once('e').and_then(|(_, e)| e.parse().ok()).unwrap_or(0);
e - (v.abs().log10().floor() as i32)
}
#[cfg(test)]
mod tests {
use super::*;
fn settings(length_pt: f64) -> TickSettings {
TickSettings { length_pt, max_space: 35.0, try_min: 4, distance: None, scale: None }
}
fn visible(ticks: &[f64], min: f64, max: f64) -> Vec<f64> {
ticks.iter().copied().filter(|t| *t >= min - 1e-9 && *t <= max + 1e-9).collect()
}
#[test]
fn snapping() {
assert_eq!(normalize_distance(2.4), 2.0);
assert_eq!(normalize_distance(0.24), 0.2);
assert_eq!(normalize_distance(1.6), 2.0);
assert_eq!(normalize_distance(3.6), 5.0);
assert_eq!(normalize_distance(8.0), 10.0);
assert_eq!(normalize_distance(1.2), 1.0);
assert_eq!(normalize_distance(600.0), 500.0);
}
#[test]
fn default_axis_ticks() {
let (t, h) = linear(-6.0, 6.0, settings(195.0));
assert_eq!(h, 2.0);
assert_eq!(visible(&t, -6.0, 6.0), vec![-6.0, -4.0, -2.0, 0.0, 2.0, 4.0, 6.0]);
let (t, h) = linear(-2.5, 27.5, TickSettings { scale: Some((0.0, 25.0, 207.0)), ..settings(162.0) });
assert_eq!(h, 10.0);
assert_eq!(visible(&t, -2.5, 27.5), vec![0.0, 10.0, 20.0]);
let (_, h) = linear(-0.5, 2.5, TickSettings { scale: Some((0.0, 2.0, 207.0)), ..settings(162.0) });
assert_eq!(h, 1.0);
let (t, _) = linear(0.0, 1.0, settings(195.0));
assert_eq!(visible(&t, 0.0, 1.0), vec![0.0, 0.2, 0.4, 0.6, 0.8, 1.0]);
}
#[test]
fn log_ticks_at_powers_of_ten() {
let (t, _, uniform) = logarithmic(0.0, 4.0 * LN_10, TickSettings { try_min: 3, ..settings(162.0) });
assert!(uniform);
let decades: Vec<f64> = t.iter().map(|v| (v / LN_10 * 1e6).round() / 1e6).filter(|v| (0.0..=4.0).contains(v)).collect();
assert_eq!(decades, vec![0.0, 1.0, 2.0, 3.0, 4.0]);
}
#[test]
fn tick_scaling() {
assert_eq!(scale_exponent(0.0, 10000.0, 3, -1), -4);
assert_eq!(scale_exponent(0.0, 1000.0, 3, -1), 0);
assert_eq!(scale_exponent(0.0, 0.01, 3, -1), 2);
assert_eq!(scale_exponent(-5.0, 5.0, 3, -1), 0);
}
}