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;
if max - min <= 0.0 || n_ticks == 0 {
return vec![];
}
super::ticks::generate_ticks(min, max, n_ticks)
.into_iter()
.map(|tick| (tick, super::ticks::format_tick_label(tick)))
.collect()
}
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),
}
}
}
#[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_secondary_axis_uses_the_canonical_generator_and_formatter() {
let axis = SecondaryAxis::twinx().range(0.0, 100.0);
for (min, max, count) in [(0.0, 100.0, 5), (0.7, 9.3, 6), (-5.0, 5.0, 8)] {
let expected: Vec<(f64, String)> = super::super::ticks::generate_ticks(min, max, count)
.into_iter()
.map(|tick| (tick, super::super::ticks::format_tick_label(tick)))
.collect();
assert_eq!(
axis.generate_ticks((min, max), count),
expected,
"secondary axis diverged from the canonical tick pipeline for ({min}, {max}, {count})"
);
}
}
#[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());
}
}