use super::{ExplicitTick, Scale, ScaleDomain, ScaleTrait, Tick, mapper::VisualMapper};
use crate::error::ChartonError;
#[derive(Debug, Clone)]
pub struct LogScale {
domain: (f64, f64),
base: f64,
mapper: Option<VisualMapper>,
}
impl LogScale {
pub fn new(
domain: (f64, f64),
base: f64,
mapper: Option<VisualMapper>,
) -> Result<Self, ChartonError> {
if domain.0 <= 0.0 || domain.1 <= 0.0 {
return Err(ChartonError::Scale(
"Log scale domain must be strictly positive".into(),
));
}
if base <= 1.0 {
return Err(ChartonError::Scale(
"Log scale base must be greater than 1".into(),
));
}
Ok(Self {
domain,
base,
mapper,
})
}
pub fn base(&self) -> f64 {
self.base
}
}
impl ScaleTrait for LogScale {
fn scale_type(&self) -> Scale {
Scale::Log
}
fn normalize(&self, value: f64) -> f64 {
let (d_min, d_max) = self.domain;
let log_min = d_min.ln();
let log_max = d_max.ln();
let log_val = value.max(d_min).ln();
let diff = log_max - log_min;
if diff.abs() < f64::EPSILON {
return 0.5;
}
(log_val - log_min) / diff
}
fn normalize_string(&self, _value: &str) -> f64 {
0.0
}
fn domain(&self) -> (f64, f64) {
self.domain
}
fn logical_max(&self) -> f64 {
1.0
}
fn mapper(&self) -> Option<&VisualMapper> {
self.mapper.as_ref()
}
fn suggest_ticks(&self, _count: usize) -> Vec<Tick> {
let (min, max) = self.domain;
let mut tick_values = Vec::new();
let log_base = self.base.ln();
let log_min = min.ln() / log_base;
let log_max = max.ln() / log_base;
let start_exp = log_min.floor() as i32;
let end_exp = log_max.ceil() as i32;
for exp in start_exp..=end_exp {
let val = self.base.powi(exp);
if val >= min * 0.99 && val <= max * 1.01 {
tick_values.push(val);
}
}
if tick_values.len() < 2 {
tick_values.clear();
tick_values.push(min);
tick_values.push(max);
}
super::format_ticks(&tick_values)
}
fn create_explicit_ticks(&self, explicit: &[ExplicitTick]) -> Vec<Tick> {
let (min, max) = self.domain;
let lower_bound = min * 0.9999999999;
let upper_bound = max * 1.0000000001;
let mut type_mismatch = 0;
let mut out_of_domain = 0;
let valid_values: Vec<f64> = explicit
.iter()
.filter_map(|tick| {
match tick {
ExplicitTick::Continuous(val) => {
if *val > 0.0 && *val >= lower_bound && *val <= upper_bound {
Some(*val)
} else {
out_of_domain += 1;
None
}
}
_ => {
type_mismatch += 1;
None
}
}
})
.collect();
if type_mismatch > 0 || out_of_domain > 0 {
eprintln!(
"Warning [LogScale]: Filtered {} ticks ({} type mismatch, {} out of domain or <= 0).",
type_mismatch + out_of_domain,
type_mismatch,
out_of_domain
);
}
super::format_ticks(&valid_values)
}
fn get_domain_enum(&self) -> ScaleDomain {
let (min, max) = self.domain;
ScaleDomain::Continuous(min, max)
}
fn sample_n(&self, n: usize) -> Vec<Tick> {
let (min, max) = self.domain;
if n == 0 {
return Vec::new();
}
if n == 1 {
return super::format_ticks(&[min]);
}
let log_min = min.ln();
let log_max = max.ln();
let log_step = (log_max - log_min) / (n - 1) as f64;
let values: Vec<f64> = (0..n)
.map(|i| {
let log_val = if i == n - 1 {
log_max
} else {
log_min + i as f64 * log_step
};
log_val.exp()
})
.collect();
super::format_ticks(&values)
}
}