use crate::format::NumberFormatter;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AxisPosition {
Bottom, Left, Right, Top, }
#[derive(Debug, Clone)]
pub struct TickMark {
pub position: f64, pub value: f64, pub label: String, }
#[derive(Debug, Clone)]
pub struct CategoryTickMark {
pub position: f64, pub label: String, pub bandwidth: f64, }
pub fn adaptive_tick_count(axis_length_px: f64) -> usize {
((axis_length_px / 50.0).floor() as usize).clamp(3, 10)
}
pub struct AxisLayout {
position: AxisPosition,
tick_count: usize,
formatter: Option<NumberFormatter>,
}
impl AxisLayout {
pub fn new(position: AxisPosition) -> Self {
Self {
position,
tick_count: 5, formatter: None,
}
}
pub fn bottom() -> Self { Self::new(AxisPosition::Bottom) }
pub fn left() -> Self { Self::new(AxisPosition::Left) }
pub fn right() -> Self { Self::new(AxisPosition::Right) }
pub fn tick_count(mut self, count: usize) -> Self {
self.tick_count = count;
self
}
pub fn formatter(mut self, fmt: NumberFormatter) -> Self {
self.formatter = Some(fmt);
self
}
pub fn generate_continuous_ticks(
&self,
domain: (f64, f64),
range: (f64, f64),
) -> Vec<TickMark> {
use crate::scales::ScaleLinear;
let scale = ScaleLinear::new(domain, range);
let tick_values = scale.ticks(self.tick_count);
tick_values.iter().map(|&value| {
let position = scale.map(value);
let label = match &self.formatter {
Some(fmt) => fmt.format(value),
None => default_format(value),
};
TickMark { position, value, label }
}).collect()
}
pub fn generate_band_ticks(
&self,
labels: &[String],
range: (f64, f64),
) -> Vec<CategoryTickMark> {
use crate::scales::ScaleBand;
let scale = ScaleBand::new(labels.to_vec(), range);
let bandwidth = scale.bandwidth();
labels.iter().map(|label| {
let position = scale.map(label).unwrap_or(0.0) + bandwidth / 2.0; CategoryTickMark {
position,
label: label.clone(),
bandwidth,
}
}).collect()
}
pub fn position(&self) -> AxisPosition { self.position }
}
fn default_format(value: f64) -> String {
if value == value.floor() && value.abs() < 1e15 {
format!("{}", value as i64)
} else {
let s = format!("{:.6}", value);
s.trim_end_matches('0').trim_end_matches('.').to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn continuous_ticks_count() {
let axis = AxisLayout::bottom().tick_count(5);
let ticks = axis.generate_continuous_ticks((0.0, 100.0), (0.0, 500.0));
assert!(ticks.len() >= 3 && ticks.len() <= 10,
"Expected 3-10 ticks, got {}", ticks.len());
}
#[test]
fn continuous_ticks_positions() {
let axis = AxisLayout::bottom().tick_count(5);
let ticks = axis.generate_continuous_ticks((0.0, 100.0), (0.0, 500.0));
for tick in &ticks {
assert!(tick.position >= 0.0 && tick.position <= 500.0,
"Tick position {} out of range [0, 500]", tick.position);
}
}
#[test]
fn continuous_ticks_with_formatter() {
let fmt = NumberFormatter::new("$,.0f");
let axis = AxisLayout::bottom().tick_count(5).formatter(fmt);
let ticks = axis.generate_continuous_ticks((0.0, 100.0), (0.0, 500.0));
for tick in &ticks {
assert!(tick.label.starts_with('$') || tick.label.starts_with("-$"),
"Expected label starting with '$', got '{}'", tick.label);
}
}
#[test]
fn band_ticks_centered() {
let labels: Vec<String> = vec!["A".into(), "B".into(), "C".into()];
let axis = AxisLayout::bottom();
let ticks = axis.generate_band_ticks(&labels, (0.0, 300.0));
for tick in &ticks {
assert!(tick.position >= 0.0 && tick.position <= 300.0,
"Band tick position {} out of range", tick.position);
}
}
#[test]
fn band_ticks_count_matches_labels() {
let labels: Vec<String> = vec!["A".into(), "B".into(), "C".into(), "D".into()];
let axis = AxisLayout::bottom();
let ticks = axis.generate_band_ticks(&labels, (0.0, 400.0));
assert_eq!(ticks.len(), labels.len(),
"Expected {} ticks, got {}", labels.len(), ticks.len());
}
#[test]
fn default_format_integer() {
assert_eq!(default_format(100.0), "100");
}
#[test]
fn default_format_decimal() {
assert_eq!(default_format(2.71), "2.71");
}
#[test]
fn adaptive_tick_count_small() {
assert_eq!(adaptive_tick_count(150.0), 3); }
#[test]
fn adaptive_tick_count_medium() {
assert_eq!(adaptive_tick_count(350.0), 7);
}
#[test]
fn adaptive_tick_count_large() {
assert_eq!(adaptive_tick_count(600.0), 10); }
}