use super::AxisScale;
pub fn generate_ticks(min: f64, max: f64, target_count: usize) -> Vec<f64> {
if target_count == 0 || (max - min).abs() < f64::EPSILON {
return vec![min, max];
}
let (min, max) = if min <= max { (min, max) } else { (max, min) };
let max_ticks = target_count.clamp(3, 10);
generate_nice_ticks(min, max, max_ticks)
}
pub fn generate_minor_ticks(major_ticks: &[f64], count: usize) -> Vec<f64> {
if major_ticks.len() < 2 || count == 0 {
return Vec::new();
}
let mut minor_ticks = Vec::new();
for window in major_ticks.windows(2) {
let start = window[0];
let end = window[1];
let step = (end - start) / (count + 1) as f64;
for i in 1..=count {
minor_ticks.push(start + step * i as f64);
}
}
minor_ticks
}
pub fn generate_ticks_for_scale(
min: f64,
max: f64,
target_count: usize,
scale: &AxisScale,
) -> Vec<f64> {
match scale {
AxisScale::Linear => generate_ticks(min, max, target_count),
AxisScale::Log => generate_log_ticks(min, max, target_count),
AxisScale::SymLog { linthresh } => {
generate_symlog_ticks(min, max, *linthresh, target_count)
}
}
}
pub fn generate_log_ticks(min: f64, max: f64, target_count: usize) -> Vec<f64> {
if target_count == 0 || (max - min).abs() < f64::EPSILON {
return vec![min.max(f64::EPSILON), max.max(f64::EPSILON)];
}
let (min, max) = if min <= max { (min, max) } else { (max, min) };
if min <= 0.0 || max <= 0.0 {
return vec![min.max(f64::EPSILON), max.max(f64::EPSILON)];
}
let log_min = min.log10().floor() as i32;
let log_max = max.log10().ceil() as i32;
let decades = (log_max - log_min) as usize;
let mut ticks = Vec::new();
if decades <= target_count {
for exp in log_min..=log_max {
let tick = 10.0_f64.powi(exp);
if tick >= min && tick <= max {
ticks.push(tick);
}
}
if decades <= target_count / 2 {
for exp in log_min..log_max {
let base = 10.0_f64.powi(exp);
for &mult in &[2.0, 5.0] {
let tick = base * mult;
if tick >= min && tick <= max {
ticks.push(tick);
}
}
}
}
} else {
let step = ((decades as f64) / (target_count as f64)).ceil() as i32;
let start_exp = log_min;
let mut exp = start_exp;
while exp <= log_max {
let tick = 10.0_f64.powi(exp);
if tick >= min && tick <= max {
ticks.push(tick);
}
exp += step;
}
}
ticks.sort_by(|a, b| a.partial_cmp(b).unwrap());
ticks.dedup_by(|a, b| (*a - *b).abs() < f64::EPSILON);
ticks
}
pub fn generate_symlog_ticks(min: f64, max: f64, linthresh: f64, target_count: usize) -> Vec<f64> {
if target_count == 0 || (max - min).abs() < f64::EPSILON {
return vec![min, max];
}
let (min, max) = if min <= max { (min, max) } else { (max, min) };
let mut ticks = Vec::new();
let lin_min = min.max(-linthresh);
let lin_max = max.min(linthresh);
if lin_min < lin_max {
if min < 0.0 && max > 0.0 {
ticks.push(0.0);
}
if lin_min < 0.0 {
ticks.push(lin_min);
}
if lin_max > 0.0 {
ticks.push(lin_max);
}
}
if max > linthresh {
let log_ticks = generate_log_ticks(linthresh, max, target_count / 2);
for tick in log_ticks {
if tick > linthresh && tick <= max {
ticks.push(tick);
}
}
}
if min < -linthresh {
let log_ticks = generate_log_ticks(linthresh, -min, target_count / 2);
for tick in log_ticks {
let neg_tick = -tick;
if neg_tick < -linthresh && neg_tick >= min {
ticks.push(neg_tick);
}
}
}
ticks.sort_by(|a, b| a.partial_cmp(b).unwrap());
ticks.dedup_by(|a, b| (*a - *b).abs() < f64::EPSILON);
ticks
}
pub fn generate_log_minor_ticks(major_ticks: &[f64]) -> Vec<f64> {
if major_ticks.len() < 2 {
return Vec::new();
}
let mut minor_ticks = Vec::new();
for window in major_ticks.windows(2) {
let start = window[0];
let end = window[1];
if (end / start - 10.0).abs() < 0.01 {
for mult in 2..=9 {
let tick = start * mult as f64;
if tick > start && tick < end {
minor_ticks.push(tick);
}
}
}
}
minor_ticks
}
fn generate_nice_ticks(min: f64, max: f64, max_ticks: usize) -> Vec<f64> {
let range = max - min;
if range <= 0.0 {
return vec![min];
}
let rough_step = range / (max_ticks - 1) as f64;
if rough_step <= f64::EPSILON {
return vec![min, max];
}
let magnitude = 10.0_f64.powf(rough_step.log10().floor());
let normalized_step = rough_step / magnitude;
let nice_step = if normalized_step <= 1.0 {
1.0
} else if normalized_step <= 2.0 {
2.0
} else if normalized_step <= 5.0 {
5.0
} else {
10.0
};
let step = nice_step * magnitude;
let start = (min / step).floor() * step;
let end = (max / step).ceil() * step;
let mut ticks = Vec::new();
let mut tick = start;
let epsilon = step * 1e-10;
while tick <= end + epsilon {
if tick >= min - epsilon && tick <= max + epsilon {
let clean_tick = clean_float(tick, step);
ticks.push(clean_tick);
}
tick += step;
}
ticks
}
fn clean_float(value: f64, step: f64) -> f64 {
let decimals = if step >= 1.0 {
0
} else {
(-step.log10().floor()) as i32 + 1
};
let mult = 10.0_f64.powi(decimals);
(value * mult).round() / mult
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_ticks_basic() {
let ticks = generate_ticks(0.0, 10.0, 5);
assert!(!ticks.is_empty());
assert!(ticks.len() <= 10);
assert!(ticks[0] >= 0.0);
assert!(*ticks.last().unwrap() <= 10.0);
}
#[test]
fn test_generate_ticks_nice_numbers() {
let ticks = generate_ticks(0.0, 100.0, 6);
for tick in &ticks {
let tick_int = *tick as i64;
assert!(tick_int % 10 == 0 || tick_int % 5 == 0 || tick_int % 2 == 0);
}
}
#[test]
fn test_generate_minor_ticks() {
let major = vec![0.0, 10.0, 20.0];
let minor = generate_minor_ticks(&major, 4);
assert_eq!(minor.len(), 8); assert!(minor[0] > 0.0 && minor[0] < 10.0);
}
#[test]
fn test_invalid_range() {
let ticks = generate_ticks(10.0, 5.0, 5);
assert!(!ticks.is_empty());
assert!(ticks.windows(2).all(|window| window[0] <= window[1]));
assert!(ticks[0] >= 5.0);
assert!(*ticks.last().unwrap() <= 10.0);
}
#[test]
fn test_log_ticks_powers_of_10() {
let ticks = generate_log_ticks(1.0, 10000.0, 10);
assert!(ticks.contains(&1.0));
assert!(ticks.contains(&10.0));
assert!(ticks.contains(&100.0));
assert!(ticks.contains(&1000.0));
assert!(ticks.contains(&10000.0));
}
#[test]
fn test_log_ticks_few_decades() {
let ticks = generate_log_ticks(1.0, 100.0, 10);
assert!(ticks.len() > 3); }
#[test]
fn test_log_ticks_invalid_range() {
let ticks = generate_log_ticks(-10.0, 100.0, 5);
assert!(!ticks.is_empty());
}
#[test]
fn test_symlog_ticks_includes_zero() {
let ticks = generate_symlog_ticks(-100.0, 100.0, 1.0, 10);
assert!(ticks.contains(&0.0));
}
#[test]
fn test_symlog_ticks_both_regions() {
let ticks = generate_symlog_ticks(-1000.0, 1000.0, 1.0, 10);
let has_positive = ticks.iter().any(|&t| t > 1.0);
let has_negative = ticks.iter().any(|&t| t < -1.0);
assert!(has_positive);
assert!(has_negative);
}
#[test]
fn test_log_minor_ticks() {
let major = vec![1.0, 10.0, 100.0];
let minor = generate_log_minor_ticks(&major);
assert_eq!(minor.len(), 16); assert!(minor.contains(&2.0));
assert!(minor.contains(&5.0));
assert!(minor.contains(&20.0));
assert!(minor.contains(&50.0));
}
#[test]
fn test_generate_ticks_for_scale() {
let linear_ticks = generate_ticks_for_scale(0.0, 100.0, 5, &AxisScale::Linear);
assert!(!linear_ticks.is_empty());
let reversed_linear_ticks = generate_ticks_for_scale(4.0, 0.0, 5, &AxisScale::Linear);
assert_eq!(reversed_linear_ticks.first().copied(), Some(0.0));
assert_eq!(reversed_linear_ticks.last().copied(), Some(4.0));
let log_ticks = generate_ticks_for_scale(1.0, 1000.0, 5, &AxisScale::Log);
assert!(log_ticks.contains(&10.0));
assert!(log_ticks.contains(&100.0));
let reversed_log_ticks = generate_ticks_for_scale(1000.0, 1.0, 5, &AxisScale::Log);
assert_eq!(reversed_log_ticks.first().copied(), Some(1.0));
assert_eq!(reversed_log_ticks.last().copied(), Some(1000.0));
let symlog_ticks = generate_ticks_for_scale(-100.0, 100.0, 10, &AxisScale::symlog(1.0));
assert!(symlog_ticks.contains(&0.0) || symlog_ticks.iter().any(|&t| t.abs() < 0.1));
}
}