#[derive(Debug, Clone)]
pub struct LinearScale {
d0: f64,
d1: f64,
p0: f64,
p1: f64,
}
impl LinearScale {
pub fn new(d0: f64, d1: f64, p0: f64, p1: f64) -> Self {
Self { d0, d1, p0, p1 }
}
pub fn map(&self, v: f64) -> f64 {
let span = self.d1 - self.d0;
if span == 0.0 {
return self.p0;
}
let t = (v - self.d0) / span;
self.p0 + t * (self.p1 - self.p0)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct NiceTicks {
pub min: f64,
pub max: f64,
pub step: f64,
pub ticks: Vec<f64>,
}
pub fn nice_ticks(data_min: f64, data_max: f64, target_count: usize) -> NiceTicks {
const MAX_TICK_INTERVALS: usize = 1_000;
let count = target_count.clamp(1, MAX_TICK_INTERVALS);
let (data_min, data_max, range) = if data_max - data_min <= 0.0 {
(data_min, data_min + 1.0, 1.0)
} else {
(data_min, data_max, data_max - data_min)
};
let raw_step = range / count as f64;
let magnitude = 10f64.powf(raw_step.log10().floor());
let norm = raw_step / magnitude; let step = magnitude
* if norm <= 1.0 {
1.0
} else if norm <= 2.0 {
2.0
} else if norm <= 5.0 {
5.0
} else {
10.0
};
let nice_min = (data_min / step).floor() * step;
let nice_max = (data_max / step).ceil() * step;
if !nice_min.is_finite() || !nice_max.is_finite() || !step.is_finite() || step <= 0.0 {
return bounded_ticks(data_min, data_max, count);
}
let intervals = ((nice_max - nice_min) / step).round();
if !intervals.is_finite() || intervals < 1.0 || intervals > MAX_TICK_INTERVALS as f64 {
return bounded_ticks(data_min, data_max, count);
}
let n = intervals as usize;
let ticks = (0..=n).map(|i| nice_min + i as f64 * step).collect();
NiceTicks {
min: nice_min,
max: nice_max,
step,
ticks,
}
}
fn bounded_ticks(data_min: f64, data_max: f64, count: usize) -> NiceTicks {
let min = if data_min.is_finite() { data_min } else { 0.0 };
let mut max = if data_max.is_finite() {
data_max
} else {
min + 1.0
};
if max <= min {
max = min + 1.0;
}
let range = max - min;
let step = if range.is_finite() && range > 0.0 {
range / count as f64
} else {
max / count as f64 - min / count as f64
};
let ticks = (0..=count)
.map(|i| {
if i == count {
max
} else {
let t = i as f64 / count as f64;
min * (1.0 - t) + max * t
}
})
.collect();
NiceTicks {
min,
max,
step,
ticks,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nice_ticks_round_numbers() {
let t = nice_ticks(0.0, 200.0, 5);
assert_eq!(t.ticks, vec![0.0, 50.0, 100.0, 150.0, 200.0]);
assert_eq!(t.min, 0.0);
assert_eq!(t.max, 200.0);
assert_eq!(t.step, 50.0);
}
#[test]
fn nice_ticks_non_round_range() {
let t = nice_ticks(0.0, 173.0, 5);
assert_eq!(t.step, 50.0);
assert_eq!(t.min, 0.0);
assert_eq!(t.max, 200.0);
assert_eq!(t.ticks, vec![0.0, 50.0, 100.0, 150.0, 200.0]);
}
#[test]
fn nice_ticks_handles_negative_min() {
let t = nice_ticks(-30.0, 70.0, 5);
assert_eq!(t.step, 20.0);
assert_eq!(t.min, -40.0);
assert_eq!(t.max, 80.0);
assert_eq!(t.ticks, vec![-40.0, -20.0, 0.0, 20.0, 40.0, 60.0, 80.0]);
}
#[test]
fn nice_ticks_flat_range_does_not_panic() {
let t = nice_ticks(5.0, 5.0, 5);
assert!(t.step > 0.0);
assert!(!t.ticks.is_empty());
assert!(t.min <= 5.0 && t.max >= 5.0);
}
#[test]
fn nice_ticks_extreme_finite_range_is_bounded() {
let t = nice_ticks(0.0, f64::MAX, 5);
assert_eq!(t.min, 0.0);
assert_eq!(t.max, f64::MAX);
assert!(t.step.is_finite());
assert_eq!(t.ticks.len(), 6);
assert!(t.ticks.iter().all(|v| v.is_finite()));
}
#[test]
fn nice_ticks_caps_requested_tick_count() {
let t = nice_ticks(0.0, 10.0, usize::MAX);
assert!(t.ticks.len() <= 1_001);
}
#[test]
fn nice_ticks_near_f64_max_span_has_finite_domain() {
let t = nice_ticks(-8e307, 9e307, 10);
assert!(t.min.is_finite());
assert!(t.max.is_finite());
let span = t.max - t.min;
assert!(span.is_finite(), "span={span}");
assert!(t.step.is_finite());
assert!(t.ticks.iter().all(|v| v.is_finite()));
}
#[test]
fn nice_ticks_full_f64_range_is_bounded() {
let t = nice_ticks(-f64::MAX, f64::MAX, 5);
assert_eq!(t.min, -f64::MAX);
assert_eq!(t.max, f64::MAX);
assert!(t.step.is_finite());
assert_eq!(t.ticks.len(), 6);
assert!(t.ticks.iter().all(|v| v.is_finite()));
assert!(t.ticks.windows(2).all(|w| w[0] < w[1]));
}
#[test]
fn linear_scale_maps_endpoints_and_midpoint() {
let s = LinearScale::new(0.0, 200.0, 0.0, 400.0);
assert!((s.map(0.0) - 0.0).abs() < 1e-9);
assert!((s.map(100.0) - 200.0).abs() < 1e-9);
assert!((s.map(200.0) - 400.0).abs() < 1e-9);
}
#[test]
fn linear_scale_inverted_pixel_range() {
let s = LinearScale::new(0.0, 100.0, 300.0, 0.0);
assert!((s.map(0.0) - 300.0).abs() < 1e-9);
assert!((s.map(100.0) - 0.0).abs() < 1e-9);
assert!((s.map(50.0) - 150.0).abs() < 1e-9);
}
#[test]
fn linear_scale_zero_domain_does_not_panic() {
let s = LinearScale::new(5.0, 5.0, 0.0, 400.0);
assert!(s.map(5.0).is_finite());
}
#[test]
fn chartjs_compat_0_to_100() {
let t = nice_ticks(0.0, 100.0, 10);
assert_eq!(t.step, 10.0);
assert_eq!(t.min, 0.0);
assert_eq!(t.max, 100.0);
assert_eq!(t.ticks.len(), 11);
assert_eq!(t.ticks[0], 0.0);
assert_eq!(t.ticks[10], 100.0);
}
#[test]
fn chartjs_compat_0_to_173() {
let t = nice_ticks(0.0, 173.0, 10);
assert_eq!(t.step, 20.0);
assert_eq!(t.min, 0.0);
assert_eq!(t.max, 180.0);
assert_eq!(t.ticks.len(), 10);
}
#[test]
fn chartjs_compat_neg30_to_70() {
let t = nice_ticks(-30.0, 70.0, 10);
assert_eq!(t.step, 10.0);
assert_eq!(t.min, -30.0);
assert_eq!(t.max, 70.0);
assert_eq!(t.ticks.len(), 11);
}
#[test]
fn chartjs_compat_0_to_1() {
let t = nice_ticks(0.0, 1.0, 10);
assert!((t.step - 0.1).abs() < 1e-9, "step={}", t.step);
assert_eq!(t.min, 0.0);
assert_eq!(t.max, 1.0);
assert_eq!(t.ticks.len(), 11);
}
#[test]
fn chartjs_compat_100_to_10000() {
let t = nice_ticks(100.0, 10000.0, 10);
assert_eq!(t.step, 1000.0);
assert_eq!(t.min, 0.0);
assert_eq!(t.max, 10000.0);
assert_eq!(t.ticks.len(), 11);
}
}