use chrono::NaiveDate;
#[derive(Clone, Debug, PartialEq)]
pub struct BandScale {
domain: Vec<String>,
range_start: f64,
range_end: f64,
step: f64,
bandwidth: f64,
padding_inner: f64,
padding_outer: f64,
}
impl BandScale {
pub fn new(domain: Vec<String>, range: (f64, f64), padding_inner: f64) -> Self {
Self::with_padding(domain, range, padding_inner, padding_inner)
}
pub fn with_padding(
domain: Vec<String>,
range: (f64, f64),
padding_inner: f64,
padding_outer: f64,
) -> Self {
let n = domain.len().max(1) as f64;
let range_start = range.0.min(range.1);
let range_end = range.0.max(range.1);
let padding_inner = padding_inner.clamp(0.0, 1.0);
let padding_outer = padding_outer.clamp(0.0, 1.0);
let inner_gaps = padding_inner * (n - 1.0).max(0.0);
let step = (range_end - range_start) / (n + inner_gaps + padding_outer * 2.0).max(1.0);
let bandwidth = step * (1.0 - padding_inner);
Self {
domain,
range_start,
range_end,
step,
bandwidth,
padding_inner,
padding_outer,
}
}
pub fn bandwidth(&self) -> f64 {
self.bandwidth
}
pub fn step(&self) -> f64 {
self.step
}
pub fn scale(&self, category: &str) -> Option<f64> {
let index = self.domain.iter().position(|c| c == category)?;
self.scale_by_index(index)
}
pub fn scale_by_index(&self, index: usize) -> Option<f64> {
if index >= self.domain.len() {
return None;
}
let band_start =
self.range_start + self.padding_outer * self.step + self.step * index as f64;
Some(band_start + self.bandwidth / 2.0)
}
pub fn domain(&self) -> &[String] {
&self.domain
}
pub fn index_at(&self, position: f64) -> Option<usize> {
if self.domain.is_empty() {
return None;
}
let pos = position.clamp(self.range_start, self.range_end);
let relative = pos - self.range_start - self.padding_outer * self.step;
if relative < 0.0 {
return None;
}
let idx = (relative / self.step).floor() as usize;
if idx >= self.domain.len() {
None
} else {
Some(idx)
}
}
pub fn position_to_fraction(&self, position: f64) -> Option<f64> {
let idx = self.index_at(position)?;
Some((idx as f64 + 0.5) / self.domain.len() as f64)
}
pub fn band_rect(&self, index: usize) -> Option<(f64, f64)> {
if index >= self.domain.len() {
return None;
}
let x = self.range_start + self.padding_outer * self.step + self.step * index as f64;
Some((x, self.bandwidth))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LinearScale {
domain_min: f64,
domain_max: f64,
range_start: f64,
range_end: f64,
}
impl LinearScale {
pub fn new(domain: (f64, f64), range: (f64, f64)) -> Self {
Self {
domain_min: domain.0,
domain_max: domain.1,
range_start: range.0,
range_end: range.1,
}
}
pub fn scale(&self, value: f64) -> f64 {
let domain_span = self.domain_max - self.domain_min;
if domain_span == 0.0 {
return (self.range_start + self.range_end) / 2.0;
}
let t = (value - self.domain_min) / domain_span;
self.range_start + t * (self.range_end - self.range_start)
}
pub fn invert(&self, pixel: f64) -> f64 {
let range_span = self.range_end - self.range_start;
if range_span == 0.0 {
return (self.domain_min + self.domain_max) / 2.0;
}
let t = (pixel - self.range_start) / range_span;
self.domain_min + t * (self.domain_max - self.domain_min)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LogScale {
inner: LinearScale,
}
impl LogScale {
pub fn new(domain: (f64, f64), range: (f64, f64)) -> Option<Self> {
if domain.0 <= 0.0 || domain.1 <= 0.0 {
return None;
}
Some(Self {
inner: LinearScale::new((domain.0.log10(), domain.1.log10()), range),
})
}
pub fn scale(&self, value: f64) -> Option<f64> {
if value <= 0.0 {
return None;
}
Some(self.inner.scale(value.log10()))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TimeScale {
domain: Vec<NaiveDate>,
inner: LinearScale,
}
impl TimeScale {
pub fn new(dates: Vec<NaiveDate>, range: (f64, f64)) -> Self {
let max_index = dates.len().saturating_sub(1).max(1) as f64;
Self {
domain: dates,
inner: LinearScale::new((0.0, max_index), range),
}
}
pub fn scale(&self, date: NaiveDate) -> Option<f64> {
let index = self.domain.iter().position(|d| *d == date)?;
Some(self.inner.scale(index as f64))
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDate;
#[test]
fn band_scale_maps_categories() {
let scale = BandScale::new(vec!["A".into(), "B".into(), "C".into()], (0.0, 300.0), 0.1);
assert!(scale.bandwidth() > 0.0);
let b = scale.scale("B").unwrap();
assert!((b - 136.76470588235292).abs() < 1e-9);
assert!(scale.scale("missing").is_none());
}
#[test]
fn band_scale_offsets_first_band_from_range_start() {
let scale = BandScale::new(vec!["A".into(), "B".into()], (0.0, 200.0), 0.1);
let center = scale.scale("A").unwrap();
let band_start = center - scale.bandwidth() / 2.0;
assert!(band_start > 0.0);
}
#[test]
fn linear_scale_interpolates() {
let scale = LinearScale::new((0.0, 100.0), (0.0, 200.0));
assert_eq!(scale.scale(50.0), 100.0);
assert_eq!(scale.scale(0.0), 0.0);
}
#[test]
fn log_scale_requires_positive_domain() {
assert!(LogScale::new((0.0, 100.0), (0.0, 100.0)).is_none());
let scale = LogScale::new((1.0, 1000.0), (0.0, 100.0)).unwrap();
assert!((scale.scale(10.0).unwrap() - 33.33333333333333).abs() < f64::EPSILON);
assert!(scale.scale(0.0).is_none());
}
#[test]
fn time_scale_maps_by_index() {
let dates = vec![
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
NaiveDate::from_ymd_opt(2024, 3, 1).unwrap(),
];
let scale = TimeScale::new(dates.clone(), (0.0, 100.0));
assert_eq!(scale.scale(dates[1]).unwrap(), 50.0);
}
}