use super::format::smart_format;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum Scale {
#[default]
Linear,
Log10,
SymLog,
}
impl Scale {
pub fn transform(&self, value: f64) -> f64 {
match self {
Scale::Linear => value,
Scale::Log10 => {
if value <= 0.0 {
f64::MIN_POSITIVE.log10()
} else {
value.log10()
}
}
Scale::SymLog => {
let sign = value.signum();
sign * (value.abs() + 1.0).log10()
}
}
}
pub fn inverse(&self, value: f64) -> f64 {
match self {
Scale::Linear => value,
Scale::Log10 => 10f64.powf(value),
Scale::SymLog => {
let sign = value.signum();
sign * (10f64.powf(value.abs()) - 1.0)
}
}
}
pub fn is_logarithmic(&self) -> bool {
matches!(self, Scale::Log10 | Scale::SymLog)
}
}
pub fn log_ticks(min: f64, max: f64, max_ticks: usize) -> Vec<f64> {
if max_ticks < 2 || min <= 0.0 || max <= 0.0 || min.is_nan() || max.is_nan() {
return vec![min.max(f64::MIN_POSITIVE), max.max(f64::MIN_POSITIVE)];
}
let min_exp = min.log10().floor() as i32;
let max_exp = max.log10().ceil() as i32;
let mut ticks: Vec<f64> = (min_exp..=max_exp).map(|e| 10f64.powi(e)).collect();
while ticks.len() > max_ticks {
let step = (ticks.len() / max_ticks).max(2);
ticks = ticks.into_iter().step_by(step).collect();
}
ticks
}
pub fn format_log_tick(value: f64) -> String {
smart_format(value, None)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_linear_transform() {
assert_eq!(Scale::Linear.transform(42.0), 42.0);
}
#[test]
fn test_linear_inverse() {
assert_eq!(Scale::Linear.inverse(42.0), 42.0);
}
#[test]
fn test_linear_roundtrip() {
let value = 123.456;
let result = Scale::Linear.inverse(Scale::Linear.transform(value));
assert!((result - value).abs() < 1e-10);
}
#[test]
fn test_linear_not_logarithmic() {
assert!(!Scale::Linear.is_logarithmic());
}
#[test]
fn test_log10_transform() {
assert!((Scale::Log10.transform(100.0) - 2.0).abs() < 1e-10);
}
#[test]
fn test_log10_transform_one() {
assert!((Scale::Log10.transform(1.0)).abs() < 1e-10);
}
#[test]
fn test_log10_transform_ten() {
assert!((Scale::Log10.transform(10.0) - 1.0).abs() < 1e-10);
}
#[test]
fn test_log10_transform_zero_clamped() {
let result = Scale::Log10.transform(0.0);
assert!(result < 0.0); }
#[test]
fn test_log10_transform_negative_clamped() {
let result = Scale::Log10.transform(-5.0);
assert!(result < 0.0);
}
#[test]
fn test_log10_inverse() {
assert!((Scale::Log10.inverse(2.0) - 100.0).abs() < 1e-10);
}
#[test]
fn test_log10_roundtrip() {
let value = 500.0;
let result = Scale::Log10.inverse(Scale::Log10.transform(value));
assert!((result - value).abs() < 1e-6);
}
#[test]
fn test_log10_is_logarithmic() {
assert!(Scale::Log10.is_logarithmic());
}
#[test]
fn test_symlog_transform_positive() {
let result = Scale::SymLog.transform(9.0);
assert!((result - 1.0).abs() < 1e-10); }
#[test]
fn test_symlog_transform_zero() {
assert!((Scale::SymLog.transform(0.0)).abs() < 1e-10); }
#[test]
fn test_symlog_transform_negative() {
let result = Scale::SymLog.transform(-9.0);
assert!((result + 1.0).abs() < 1e-10); }
#[test]
fn test_symlog_symmetry() {
let pos = Scale::SymLog.transform(100.0);
let neg = Scale::SymLog.transform(-100.0);
assert!((pos + neg).abs() < 1e-10);
}
#[test]
fn test_symlog_inverse_positive() {
let value = 50.0;
let result = Scale::SymLog.inverse(Scale::SymLog.transform(value));
assert!((result - value).abs() < 1e-6);
}
#[test]
fn test_symlog_inverse_negative() {
let value = -50.0;
let result = Scale::SymLog.inverse(Scale::SymLog.transform(value));
assert!((result - value).abs() < 1e-6);
}
#[test]
fn test_symlog_is_logarithmic() {
assert!(Scale::SymLog.is_logarithmic());
}
#[test]
fn test_log_ticks_basic() {
let ticks = log_ticks(1.0, 10000.0, 5);
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_limited() {
let ticks = log_ticks(1.0, 1_000_000_000.0, 3);
assert!(ticks.len() <= 3);
}
#[test]
fn test_log_ticks_small_range() {
let ticks = log_ticks(1.0, 10.0, 5);
assert!(!ticks.is_empty());
}
#[test]
fn test_log_ticks_zero_min() {
let ticks = log_ticks(0.0, 100.0, 5);
assert!(!ticks.is_empty());
}
#[test]
fn test_format_log_tick_one() {
assert_eq!(format_log_tick(1.0), "1");
}
#[test]
fn test_format_log_tick_thousand() {
assert_eq!(format_log_tick(1000.0), "1K");
}
#[test]
fn test_format_log_tick_large() {
assert_eq!(format_log_tick(1_000_000.0), "1M");
}
#[test]
fn test_default_is_linear() {
assert_eq!(Scale::default(), Scale::Linear);
}
#[test]
fn test_clone() {
let scale = Scale::Log10;
let cloned = scale.clone();
assert_eq!(scale, cloned);
}
#[test]
fn test_debug() {
let debug = format!("{:?}", Scale::SymLog);
assert_eq!(debug, "SymLog");
}
}