use crate::display::{Number, NumberFormat};
const STEPS: f64 = 4.0;
const MAX_DECIMALS: u8 = 12;
#[derive(Clone, Debug, PartialEq)]
pub(super) struct Axis {
ticks: Vec<f64>,
decimals: u8,
}
impl Axis {
pub(super) fn over<'a>(values: impl IntoIterator<Item = &'a f64>) -> Option<Self> {
let (min, max) = values
.into_iter()
.fold(None, |range: Option<(f64, f64)>, &value| {
Some(range.map_or((value, value), |(min, max)| {
(min.min(value), max.max(value))
}))
})?;
let (min, max) = (min.min(0.0), max.max(0.0));
if min == max {
return Some(Self::new(0.0, 1.0, 1.0));
}
let raw = (max / 2.0 - min / 2.0) * 2.0 / STEPS;
let magnitude = 10f64.powf(raw.log10().floor());
let step = [1.0, 2.0, 5.0, 10.0]
.into_iter()
.map(|factor| factor * magnitude)
.find(|&step| step >= raw)
.unwrap_or(10.0 * magnitude);
let (low, high) = ((min / step).floor(), (max / step).ceil());
if step.is_finite() && step > 0.0 && (low * step).is_finite() && (high * step).is_finite() {
Some(Self::new(low, high, step))
} else {
Some(Self {
ticks: vec![min, max],
decimals: 0,
})
}
}
fn new(low: f64, high: f64, step: f64) -> Self {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let decimals = if step >= 1.0 {
0
} else {
(-step.log10().floor()).min(f64::from(MAX_DECIMALS)) as u8
};
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let count = (high - low) as usize;
let ticks = (0..=count)
.map(|index| (low + index as f64) * step + 0.0)
.collect();
Self { ticks, decimals }
}
pub(super) fn ticks(&self) -> impl Iterator<Item = f64> + '_ {
self.ticks.iter().copied()
}
pub(super) fn share(&self, value: f64) -> f64 {
let (low, high) = (self.ticks[0], self.ticks[self.ticks.len() - 1]);
((high / 2.0 - value / 2.0) / (high / 2.0 - low / 2.0)).clamp(0.0, 1.0)
}
pub(super) fn format(&self, tick: f64, numbers: &NumberFormat) -> String {
numbers.spell(Number::Float(tick), self.decimals)
}
}
#[cfg(test)]
mod tests;