use super::{ExplicitTick, Scale, ScaleDomain, ScaleTrait, Tick, mapper::VisualMapper};
use ahash::AHashMap;
#[derive(Debug, Clone)]
pub struct DiscreteScale {
domain: Vec<String>,
index_map: AHashMap<String, usize>,
expanded_range: (f64, f64),
mapper: Option<VisualMapper>,
}
impl DiscreteScale {
pub fn new(
domain: Vec<String>,
expansion: crate::scale::Expansion,
mapper: Option<VisualMapper>,
) -> Self {
let mut index_map = AHashMap::with_capacity(domain.len());
for (i, val) in domain.iter().enumerate() {
index_map.insert(val.clone(), i);
}
let n = domain.len();
let expanded_range = if n == 0 {
(0.0, 0.0)
} else {
let range = (n - 1) as f64;
let lower_padding = range * expansion.mult.0 + expansion.add.0;
let upper_padding = range * expansion.mult.1 + expansion.add.1;
(0.0 - lower_padding, range + upper_padding)
};
Self {
domain,
index_map,
expanded_range,
mapper,
}
}
pub fn count(&self) -> usize {
self.domain.len()
}
}
impl ScaleTrait for DiscreteScale {
fn scale_type(&self) -> Scale {
Scale::Discrete
}
fn normalize(&self, value: f64) -> f64 {
let (min, max) = self.expanded_range;
let range = max - min;
if range.abs() < 1e-9 {
return 0.5;
}
(value - min) / range
}
fn normalize_string(&self, value: &str) -> f64 {
match self.index_map.get(value) {
Some(&index) => self.normalize(index as f64),
None => f64::NAN,
}
}
fn domain(&self) -> (f64, f64) {
self.expanded_range
}
fn logical_max(&self) -> f64 {
let n = self.domain.len();
if n == 0 { 0.0 } else { (n - 1) as f64 }
}
fn mapper(&self) -> Option<&VisualMapper> {
self.mapper.as_ref()
}
fn suggest_ticks(&self, _count: usize) -> Vec<Tick> {
self.domain
.iter()
.enumerate()
.map(|(i, label)| Tick {
value: i as f64,
label: label.clone(),
})
.collect()
}
fn create_explicit_ticks(&self, explicit: &[ExplicitTick]) -> Vec<Tick> {
let mut type_mismatch = 0;
let mut not_in_domain = 0;
let ticks: Vec<Tick> = explicit
.iter()
.filter_map(|tick| {
match tick {
ExplicitTick::Discrete(label) => {
if let Some(index) = self.domain.iter().position(|d| d == label) {
Some(Tick {
value: index as f64,
label: label.clone(),
})
} else {
not_in_domain += 1;
None
}
}
_ => {
type_mismatch += 1;
None
}
}
})
.collect();
if type_mismatch > 0 || not_in_domain > 0 {
eprintln!(
"Warning [DiscreteScale]: Filtered {} ticks ({} type mismatch, {} not found in domain).",
type_mismatch + not_in_domain,
type_mismatch,
not_in_domain
);
}
ticks
}
fn get_domain_enum(&self) -> ScaleDomain {
ScaleDomain::Discrete(self.domain.clone())
}
fn sample_n(&self, n: usize) -> Vec<Tick> {
let len = self.domain.len();
if len <= n || n == 0 {
return self.suggest_ticks(n);
}
(0..n)
.map(|i| {
let idx = if i == n - 1 {
len - 1
} else {
((i as f64 * (len - 1) as f64) / (n - 1) as f64).round() as usize
};
Tick {
value: idx as f64,
label: self.domain[idx].clone(),
}
})
.collect()
}
}