use super::{ExplicitTick, Scale, ScaleDomain, ScaleTrait, Tick, mapper::VisualMapper};
#[derive(Debug, Clone)]
pub struct LinearScale {
domain: (f64, f64),
mapper: Option<VisualMapper>,
}
impl LinearScale {
pub fn new(domain: (f64, f64), mapper: Option<VisualMapper>) -> Self {
Self { domain, mapper }
}
fn calculate_nice_step(&self, count: usize) -> f64 {
let (min, max) = self.domain;
let range = max - min;
if range.abs() < 1e-12 {
return 1.0;
}
let rough_step = range / (count.max(2) as f64);
let exp = 10f64.powf(rough_step.log10().floor());
let f = rough_step / exp;
let nice = if f < 1.5 {
1.0
} else if f < 3.0 {
2.0
} else if f < 7.0 {
5.0
} else {
10.0
};
nice * exp
}
}
impl ScaleTrait for LinearScale {
fn scale_type(&self) -> Scale {
Scale::Linear
}
fn normalize(&self, value: f64) -> f64 {
let (d_min, d_max) = self.domain;
let diff = d_max - d_min;
if diff.abs() < f64::EPSILON {
return 0.5; }
(value - d_min) / diff
}
fn normalize_string(&self, _value: &str) -> f64 {
f64::NAN
}
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 step = self.calculate_nice_step(count);
let tolerance = step * 1e-9;
let start = (min / step).ceil() * step;
let mut values = Vec::new();
let mut curr = start;
let mut iterations = 0;
while curr <= max + tolerance && iterations < count * 2 {
let clean_val = if curr.abs() < 1e-12 { 0.0 } else { curr };
values.push(clean_val);
curr += step;
iterations += 1;
}
super::format_ticks(&values)
}
fn create_explicit_ticks(&self, explicit: &[ExplicitTick]) -> Vec<Tick> {
let (min, max) = self.domain;
let range = (max - min).abs();
let tolerance = if range < f64::EPSILON {
1e-10
} else {
range * 1e-10
};
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 >= min - tolerance && *val <= max + tolerance {
Some(if val.abs() < 1e-12 { 0.0 } else { *val })
} else {
out_of_domain += 1;
None
}
}
_ => {
type_mismatch += 1;
None
}
}
})
.collect();
if type_mismatch > 0 || out_of_domain > 0 {
eprintln!(
"LinearScale: Filtered {} ticks ({} type mismatch, {} out of domain [{}, {}]).",
type_mismatch + out_of_domain,
type_mismatch,
out_of_domain,
min,
max
);
}
super::format_ticks(&valid_values)
}
fn get_domain_enum(&self) -> ScaleDomain {
ScaleDomain::Continuous(self.domain.0, self.domain.1)
}
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 step = (max - min) / (n - 1) as f64;
let values: Vec<f64> = (0..n)
.map(|i| {
let val = if i == n - 1 {
max
} else {
min + i as f64 * step
};
if val.abs() < 1e-12 { 0.0 } else { val }
})
.collect();
super::format_ticks(&values)
}
}