use super::scale::AxisScale;
#[derive(Debug, Clone)]
pub struct SecondaryAxis {
pub axis: AxisType,
pub label: Option<String>,
pub scale: AxisScale,
pub range: Option<(f64, f64)>,
pub show_grid: bool,
pub color: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AxisType {
X,
Y,
}
impl SecondaryAxis {
pub fn twinx() -> Self {
Self {
axis: AxisType::Y,
label: None,
scale: AxisScale::Linear,
range: None,
show_grid: false,
color: None,
}
}
pub fn twiny() -> Self {
Self {
axis: AxisType::X,
label: None,
scale: AxisScale::Linear,
range: None,
show_grid: false,
color: None,
}
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn scale(mut self, scale: AxisScale) -> Self {
self.scale = scale;
self
}
pub fn range(mut self, min: f64, max: f64) -> Self {
self.range = Some((min, max));
self
}
pub fn show_grid(mut self, show: bool) -> Self {
self.show_grid = show;
self
}
pub fn color(mut self, color: impl Into<String>) -> Self {
self.color = Some(color.into());
self
}
pub fn generate_ticks(&self, range: (f64, f64), n_ticks: usize) -> Vec<(f64, String)> {
let (min, max) = range;
let range_size = max - min;
if range_size <= 0.0 || n_ticks == 0 {
return vec![];
}
let raw_step = range_size / n_ticks as f64;
let magnitude = 10.0_f64.powf(raw_step.log10().floor());
let residual = raw_step / magnitude;
let nice_step = if residual <= 1.5 {
1.0 * magnitude
} else if residual <= 3.0 {
2.0 * magnitude
} else if residual <= 7.0 {
5.0 * magnitude
} else {
10.0 * magnitude
};
let tick_min = (min / nice_step).ceil() * nice_step;
let mut ticks = Vec::new();
let mut tick = tick_min;
while tick <= max + nice_step * 1e-10 {
let label = format_tick(tick, nice_step);
ticks.push((tick, label));
tick += nice_step;
}
ticks
}
pub fn normalize(&self, value: f64) -> f64 {
let (min, max) = self.range.unwrap_or((0.0, 1.0));
match self.scale {
AxisScale::Linear => (value - min) / (max - min),
AxisScale::Log => {
let log_min = min.max(1e-10).log10();
let log_max = max.log10();
let log_val = value.max(1e-10).log10();
(log_val - log_min) / (log_max - log_min)
}
_ => (value - min) / (max - min), }
}
pub fn denormalize(&self, norm: f64) -> f64 {
let (min, max) = self.range.unwrap_or((0.0, 1.0));
match self.scale {
AxisScale::Linear => min + norm * (max - min),
AxisScale::Log => {
let log_min = min.max(1e-10).log10();
let log_max = max.log10();
10.0_f64.powf(log_min + norm * (log_max - log_min))
}
_ => min + norm * (max - min),
}
}
}
fn format_tick(value: f64, step: f64) -> String {
if step >= 1.0 && value.abs() < 1e10 {
format!("{:.0}", value)
} else if step >= 0.1 {
format!("{:.1}", value)
} else if step >= 0.01 {
format!("{:.2}", value)
} else if step >= 0.001 {
format!("{:.3}", value)
} else {
format!("{:.2e}", value)
}
}
#[derive(Debug, Clone)]
pub struct DualAxes {
pub primary_y: (f64, f64),
pub secondary_y: Option<SecondaryAxis>,
pub primary_x: (f64, f64),
pub secondary_x: Option<SecondaryAxis>,
}
impl Default for DualAxes {
fn default() -> Self {
Self {
primary_y: (0.0, 1.0),
secondary_y: None,
primary_x: (0.0, 1.0),
secondary_x: None,
}
}
}
impl DualAxes {
pub fn new(x_range: (f64, f64), y_range: (f64, f64)) -> Self {
Self {
primary_x: x_range,
primary_y: y_range,
..Default::default()
}
}
pub fn twinx(mut self, config: SecondaryAxis) -> Self {
self.secondary_y = Some(config);
self
}
pub fn twiny(mut self, config: SecondaryAxis) -> Self {
self.secondary_x = Some(config);
self
}
pub fn has_secondary_y(&self) -> bool {
self.secondary_y.is_some()
}
pub fn has_secondary_x(&self) -> bool {
self.secondary_x.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_secondary_axis_ticks() {
let axis = SecondaryAxis::twinx().range(0.0, 100.0);
let ticks = axis.generate_ticks((0.0, 100.0), 5);
assert!(!ticks.is_empty());
assert!(ticks[0].0 >= 0.0);
assert!(ticks.last().unwrap().0 <= 100.0);
}
#[test]
fn test_normalize_linear() {
let axis = SecondaryAxis::twinx().range(0.0, 100.0);
assert!((axis.normalize(0.0) - 0.0).abs() < 1e-10);
assert!((axis.normalize(50.0) - 0.5).abs() < 1e-10);
assert!((axis.normalize(100.0) - 1.0).abs() < 1e-10);
}
#[test]
fn test_denormalize() {
let axis = SecondaryAxis::twinx().range(0.0, 100.0);
assert!((axis.denormalize(0.0) - 0.0).abs() < 1e-10);
assert!((axis.denormalize(0.5) - 50.0).abs() < 1e-10);
assert!((axis.denormalize(1.0) - 100.0).abs() < 1e-10);
}
#[test]
fn test_dual_axes() {
let dual = DualAxes::new((0.0, 10.0), (0.0, 100.0))
.twinx(SecondaryAxis::twinx().range(0.0, 1.0).label("Secondary"));
assert!(dual.has_secondary_y());
assert!(!dual.has_secondary_x());
}
}