pub struct ScaleBand {
domain: Vec<String>,
range: (f64, f64),
padding_inner: f64,
padding_outer: f64,
step: f64,
bandwidth: f64,
}
impl ScaleBand {
pub fn new(domain: Vec<String>, range: (f64, f64)) -> Self {
let mut scale = Self {
domain,
range,
padding_inner: 0.1,
padding_outer: 0.1,
step: 0.0,
bandwidth: 0.0,
};
scale.recalculate();
scale
}
pub fn padding_inner(mut self, padding: f64) -> Self {
self.padding_inner = padding;
self.recalculate();
self
}
pub fn padding_outer(mut self, padding: f64) -> Self {
self.padding_outer = padding;
self.recalculate();
self
}
pub fn padding(mut self, padding: f64) -> Self {
self.padding_inner = padding;
self.padding_outer = padding;
self.recalculate();
self
}
pub fn map(&self, value: &str) -> Option<f64> {
let index = self.domain.iter().position(|d| d == value)?;
let start = self.range.0.min(self.range.1);
Some(start + self.padding_outer * self.step + index as f64 * self.step)
}
pub fn bandwidth(&self) -> f64 {
self.bandwidth
}
pub fn step(&self) -> f64 {
self.step
}
pub fn domain(&self) -> &[String] {
&self.domain
}
pub fn range(&self) -> (f64, f64) {
self.range
}
fn recalculate(&mut self) {
let n = self.domain.len() as f64;
let range_size = (self.range.1 - self.range.0).abs();
if n == 0.0 {
self.step = 0.0;
self.bandwidth = 0.0;
return;
}
self.step = range_size / (n - self.padding_inner + 2.0 * self.padding_outer).max(1.0);
self.bandwidth = self.step * (1.0 - self.padding_inner);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn domain_abc() -> Vec<String> {
vec!["A".to_string(), "B".to_string(), "C".to_string()]
}
#[test]
fn band_scale_basic() {
let scale = ScaleBand::new(domain_abc(), (0.0, 300.0));
let a = scale.map("A").unwrap();
let c = scale.map("C").unwrap();
assert!(a < c, "A position {} should be less than C position {}", a, c);
assert!(a > 0.0, "A should have some outer padding offset");
assert!(c + scale.bandwidth() <= 300.0 + 1e-10);
}
#[test]
fn band_scale_bandwidth() {
let scale = ScaleBand::new(domain_abc(), (0.0, 300.0));
assert!(scale.bandwidth() > 0.0);
assert!(scale.bandwidth() < 300.0);
assert!(scale.bandwidth() <= scale.step());
}
#[test]
fn band_scale_unknown_value() {
let scale = ScaleBand::new(domain_abc(), (0.0, 300.0));
assert!(scale.map("D").is_none());
}
#[test]
fn band_scale_empty_domain() {
let scale = ScaleBand::new(vec![], (0.0, 300.0));
assert_eq!(scale.bandwidth(), 0.0);
assert_eq!(scale.step(), 0.0);
}
#[test]
fn band_scale_single_item() {
let scale = ScaleBand::new(vec!["A".to_string()], (0.0, 300.0));
let a = scale.map("A").unwrap();
assert!(a >= 0.0);
assert!(scale.bandwidth() > 0.0);
assert!(a + scale.bandwidth() <= 300.0 + 1e-10);
}
#[test]
fn band_scale_custom_padding() {
let scale_default = ScaleBand::new(domain_abc(), (0.0, 300.0));
let scale_padded = ScaleBand::new(domain_abc(), (0.0, 300.0)).padding(0.2);
assert!(
scale_padded.bandwidth() < scale_default.bandwidth(),
"padded bandwidth {} should be less than default {}",
scale_padded.bandwidth(),
scale_default.bandwidth()
);
}
#[test]
fn band_scale_no_padding() {
let scale = ScaleBand::new(domain_abc(), (0.0, 300.0))
.padding_inner(0.0)
.padding_outer(0.0);
let expected = 300.0 / 3.0;
assert!(
(scale.bandwidth() - expected).abs() < 1e-10,
"bandwidth should be {} but got {}",
expected,
scale.bandwidth()
);
assert!(
(scale.step() - expected).abs() < 1e-10,
"step should be {} but got {}",
expected,
scale.step()
);
let a = scale.map("A").unwrap();
assert!((a - 0.0).abs() < 1e-10, "A should be at 0, got {}", a);
}
#[test]
fn band_scale_step() {
let scale = ScaleBand::new(domain_abc(), (0.0, 300.0));
assert!(
scale.step() >= scale.bandwidth(),
"step {} should be >= bandwidth {}",
scale.step(),
scale.bandwidth()
);
}
}