#[allow(unused_imports)]
use super::*;
#[derive(Debug, Clone)]
pub(crate) struct TickSpec {
pub(crate) values: Vec<f64>,
pub(crate) step: f64,
}
pub(super) fn build_ticks(min: f64, max: f64, target: usize) -> TickSpec {
let (min, max) = normalize_bounds(min, max);
let span = (max - min).abs().max(f64::EPSILON);
let range = nice_number(span, false);
let raw_step = range / (target.max(2) as f64 - 1.0);
let step = nice_number(raw_step, true).max(f64::EPSILON);
let nice_min = (min / step).floor() * step;
let nice_max = (max / step).ceil() * step;
let mut values = Vec::new();
let mut value = nice_min;
let limit = nice_max + step * 0.5;
let mut guard = 0usize;
while value <= limit && guard < 128 {
values.push(value);
value += step;
guard = guard.saturating_add(1);
}
if values.is_empty() {
values.push(min);
values.push(max);
}
TickSpec { values, step }
}
pub(super) fn build_tui_ticks(data_min: f64, data_max: f64, cell_count: usize) -> TickSpec {
let (data_min, data_max) = normalize_bounds(data_min, data_max);
if cell_count < 4 {
let step = (data_max - data_min).abs().max(f64::EPSILON);
return TickSpec {
values: vec![data_min, data_max],
step,
};
}
let last = cell_count.saturating_sub(1).max(1);
let span = (data_max - data_min).abs().max(f64::EPSILON);
let log = span.log10().floor();
let min_spacing = if last >= 14 { 2 } else { 1 };
let mut candidates: Vec<(f64, f64, usize, usize)> = Vec::new();
for exp_off in -1..=1i32 {
let base = 10.0_f64.powf(log + f64::from(exp_off));
for &mult in &[1.0, 2.0, 2.5, 5.0] {
let step = base * mult;
if step <= 0.0 || !step.is_finite() {
continue;
}
let lo = (data_min / step).floor() * step;
let hi = (data_max / step).ceil() * step;
let n = ((hi - lo) / step + 0.5) as usize;
if (3..=8).contains(&n) && last / n >= min_spacing {
let rem = last % n;
candidates.push((step, lo, n, rem));
}
}
}
candidates.sort_by(|a, b| {
a.3.cmp(&b.3).then_with(|| {
let da = (a.2 as i32 - 5).unsigned_abs();
let db = (b.2 as i32 - 5).unsigned_abs();
da.cmp(&db)
})
});
if let Some(&(step, lo, n, _)) = candidates.first() {
let values: Vec<f64> = (0..=n).map(|i| lo + step * i as f64).collect();
return TickSpec { values, step };
}
build_ticks(data_min, data_max, 5)
}
pub(super) fn nice_number(value: f64, round: bool) -> f64 {
if value <= 0.0 || !value.is_finite() {
return 1.0;
}
let exponent = value.log10().floor();
let power = 10.0_f64.powf(exponent);
let fraction = value / power;
let nice_fraction = if round {
if fraction < 1.5 {
1.0
} else if fraction < 3.0 {
2.0
} else if fraction < 7.0 {
5.0
} else {
10.0
}
} else if fraction <= 1.0 {
1.0
} else if fraction <= 2.0 {
2.0
} else if fraction <= 5.0 {
5.0
} else {
10.0
};
nice_fraction * power
}
pub(super) fn format_number(value: f64, step: f64) -> String {
if !value.is_finite() {
return "0".to_string();
}
let abs_step = step.abs().max(f64::EPSILON);
let precision = if abs_step >= 1.0 {
0
} else {
(-abs_step.log10().floor() as i32 + 1).clamp(0, 6) as usize
};
format!("{value:.precision$}")
}
pub(super) fn resolve_bounds<I>(values: I, manual: Option<(f64, f64)>) -> (f64, f64)
where
I: Iterator<Item = f64>,
{
if let Some((min, max)) = manual
&& min.is_finite()
&& max.is_finite()
{
return normalize_bounds(min, max);
}
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
for value in values {
if !value.is_finite() {
continue;
}
min = min.min(value);
max = max.max(value);
}
if !min.is_finite() || !max.is_finite() {
return (0.0, 1.0);
}
normalize_bounds(min, max)
}
pub(super) fn normalize_bounds(min: f64, max: f64) -> (f64, f64) {
const MAX_SAFE_BOUND: f64 = f64::MAX / 16.0;
if !min.is_finite() || !max.is_finite() {
return (0.0, 1.0);
}
let (mut min, mut max) = if min <= max { (min, max) } else { (max, min) };
min = min.clamp(-MAX_SAFE_BOUND, MAX_SAFE_BOUND);
max = max.clamp(-MAX_SAFE_BOUND, MAX_SAFE_BOUND);
if (max - min).abs() < f64::EPSILON {
let pad = if min.abs() < 1.0 {
1.0
} else {
min.abs() * 0.1
};
min = (min - pad).max(-MAX_SAFE_BOUND);
max = (max + pad).min(MAX_SAFE_BOUND);
if min == max {
return (min - 1.0, max);
}
}
(min, max)
}
pub(crate) fn finite_ratio(value: f64, min: f64, max: f64) -> Option<f64> {
if !value.is_finite() || !min.is_finite() || !max.is_finite() {
return None;
}
let (min, max) = normalize_bounds(min, max);
let scale = value.abs().max(min.abs()).max(max.abs()).max(1.0);
let scaled_min = min / scale;
let scaled_span = max / scale - scaled_min;
if !scaled_span.is_finite() || scaled_span.abs() < f64::EPSILON {
return Some(0.5);
}
Some(((value / scale - scaled_min) / scaled_span).clamp(0.0, 1.0))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reversed_and_extreme_bounds_normalize_to_finite_ordered_span() {
assert_eq!(normalize_bounds(10.0, -10.0), (-10.0, 10.0));
let (min, max) = normalize_bounds(-f64::MAX, f64::MAX);
assert!(min.is_finite() && max.is_finite());
assert!(min < max);
assert!((max - min).is_finite());
}
#[test]
fn invalid_manual_bounds_fall_back_to_finite_data() {
let values = [2.0, 4.0, f64::NAN];
let bounds = resolve_bounds(values.into_iter(), Some((f64::NAN, f64::INFINITY)));
assert_eq!(bounds, (2.0, 4.0));
}
#[test]
fn finite_ratio_handles_extreme_spans_without_overflow() {
assert_eq!(finite_ratio(-f64::MAX, -f64::MAX, f64::MAX), Some(0.0));
assert_eq!(finite_ratio(0.0, -f64::MAX, f64::MAX), Some(0.5));
assert_eq!(finite_ratio(f64::MAX, -f64::MAX, f64::MAX), Some(1.0));
assert_eq!(finite_ratio(f64::NAN, 0.0, 1.0), None);
}
#[test]
fn tick_generation_remains_finite_for_extreme_input() {
let ticks = build_tui_ticks(-f64::MAX, f64::MAX, 40);
assert!(!ticks.values.is_empty());
assert!(ticks.step.is_finite());
assert!(ticks.values.iter().all(|value| value.is_finite()));
}
}