use super::linear::LinearScale;
use super::{Scale, Tick, TickPriority};
const DEFAULT_EXPONENT: f64 = 1.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PowScale {
pub min: f64,
pub max: f64,
pub exponent: f64,
}
fn signed_pow(v: f64, exponent: f64) -> f64 {
v.signum() * v.abs().powf(exponent)
}
fn signed_unpow(y: f64, exponent: f64) -> f64 {
y.signum() * y.abs().powf(1.0 / exponent)
}
impl PowScale {
pub fn new(min: f64, max: f64, exponent: f64) -> Self {
let exponent = if exponent.is_finite() { exponent } else { DEFAULT_EXPONENT };
Self { min, max, exponent }
}
pub fn sqrt(min: f64, max: f64) -> Self {
Self::new(min, max, 0.5)
}
}
impl Scale for PowScale {
fn domain(&self) -> (f64, f64) {
(self.min, self.max)
}
fn map(&self, v: f64) -> f64 {
let (p_min, p_max) = (signed_pow(self.min, self.exponent), signed_pow(self.max, self.exponent));
let range = p_max - p_min;
if range.abs() < f64::EPSILON {
return 0.5;
}
(signed_pow(v, self.exponent) - p_min) / range
}
fn invert(&self, t: f64) -> f64 {
if self.exponent.abs() < f64::EPSILON {
return self.min + t * (self.max - self.min);
}
let (p_min, p_max) = (signed_pow(self.min, self.exponent), signed_pow(self.max, self.exponent));
signed_unpow(p_min + t * (p_max - p_min), self.exponent)
}
fn ticks(&self, target_count: usize) -> Vec<Tick> {
LinearScale::new(self.min, self.max).ticks(target_count)
}
fn tick_priority(&self, v: f64) -> TickPriority {
LinearScale::new(self.min, self.max).tick_priority(v)
}
fn windowed(&self, min: f64, max: f64) -> Option<Box<dyn Scale>> {
Some(Box::new(PowScale::new(min, max, self.exponent)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sqrt_convenience_constructor_sets_exponent_one_half() {
let scale = PowScale::sqrt(0.0, 100.0);
assert!((scale.exponent - 0.5).abs() < 1e-9);
}
#[test]
fn boundary_values_map_to_exactly_zero_and_one() {
let scale = PowScale::sqrt(0.0, 100.0);
assert!((scale.map(0.0) - 0.0).abs() < 1e-9);
assert!((scale.map(100.0) - 1.0).abs() < 1e-9);
}
#[test]
fn map_and_invert_round_trip_for_several_exponents() {
for exponent in [0.5, 1.0, 2.0, 3.0] {
let scale = PowScale::new(1.0, 1000.0, exponent);
for v in [1.0, 10.0, 250.0, 1000.0] {
let t = scale.map(v);
let back = scale.invert(t);
assert!((back - v).abs() / v < 1e-6, "exponent={exponent}: round trip failed for {v}, got {back}");
}
}
}
#[test]
fn sqrt_scale_is_monotonically_increasing() {
let scale = PowScale::sqrt(0.0, 10_000.0);
let mapped: Vec<f64> = (0..=20).map(|i| scale.map(i as f64 * 500.0)).collect();
for w in mapped.windows(2) {
assert!(w[1] > w[0], "PowScale::sqrt must be strictly increasing, got {mapped:?}");
}
}
#[test]
fn negative_domain_uses_sign_preserving_power_and_round_trips() {
let scale = PowScale::new(-1000.0, 1000.0, 0.5);
for v in [-1000.0, -1.0, 0.0, 1.0, 1000.0] {
let t = scale.map(v);
assert!(t.is_finite(), "map({v}) produced a non-finite value under a signed sqrt scale");
let back = scale.invert(t);
assert!((back - v).abs() < 1e-3, "round trip failed for {v}, got {back}");
}
assert!(scale.map(-500.0) < scale.map(0.0));
assert!(scale.map(0.0) < scale.map(500.0));
}
#[test]
fn degenerate_domain_does_not_panic() {
let scale = PowScale::sqrt(5.0, 5.0);
assert_eq!(scale.map(5.0), 0.5);
let ticks = scale.ticks(5);
assert_eq!(ticks.len(), 1);
}
#[test]
fn zero_exponent_does_not_panic_or_produce_nan() {
let scale = PowScale::new(0.0, 100.0, 0.0);
let t = scale.map(50.0);
assert!(t.is_finite());
let back = scale.invert(t);
assert!(back.is_finite());
}
#[test]
fn non_finite_exponent_falls_back_to_the_default() {
let scale = PowScale::new(0.0, 100.0, f64::NAN);
assert_eq!(scale.exponent, DEFAULT_EXPONENT);
}
#[test]
fn ticks_match_a_plain_linear_scale_over_the_same_domain() {
let pow = PowScale::sqrt(0.0, 1000.0);
let linear = LinearScale::new(0.0, 1000.0);
assert_eq!(pow.ticks(5), linear.ticks(5), "PowScale ticks must be identical to LinearScale ticks over the same domain — only the mapping differs");
}
#[test]
fn tick_priority_matches_a_plain_linear_scale_over_the_same_domain() {
let pow = PowScale::new(-100.0, 100.0, 0.5);
let linear = LinearScale::new(-100.0, 100.0);
assert_eq!(pow.tick_priority(0.0), linear.tick_priority(0.0));
assert_eq!(pow.tick_priority(0.0), TickPriority::Major);
assert_eq!(pow.tick_priority(50.0), TickPriority::Minor);
}
#[test]
fn windowed_preserves_the_exponent() {
let scale = PowScale::sqrt(0.0, 1000.0);
let windowed = scale.windowed(10.0, 20.0).expect("PowScale supports windowing");
assert_eq!(windowed.domain(), (10.0, 20.0));
let rebuilt = PowScale::new(10.0, 20.0, 0.5);
assert!((windowed.map(15.0) - rebuilt.map(15.0)).abs() < 1e-12);
}
}