use super::style::AxisStyle;
use super::types::{AxisId, AxisOrientation, AxisPosition};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TimeEpoch {
#[default]
Unix,
J2000,
Custom(i64),
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum ScaleType {
#[default]
Linear,
Logarithmic {
base: f64,
},
Symlog {
lin_threshold: f64,
},
Time {
epoch: TimeEpoch,
},
}
impl ScaleType {
pub fn log10() -> Self {
Self::Logarithmic { base: 10.0 }
}
pub fn ln() -> Self {
Self::Logarithmic {
base: std::f64::consts::E,
}
}
pub fn symlog(threshold: f64) -> Self {
Self::Symlog {
lin_threshold: threshold,
}
}
pub fn time() -> Self {
Self::Time {
epoch: TimeEpoch::Unix,
}
}
pub fn normalize(&self, value: f64, min: f64, max: f64) -> f64 {
if (max - min).abs() < f64::EPSILON {
return 0.5;
}
match self {
Self::Linear | Self::Time { .. } => (value - min) / (max - min),
Self::Logarithmic { base } => {
if value <= 0.0 || min <= 0.0 || max <= 0.0 {
return (value - min) / (max - min);
}
let log_value = value.log(*base);
let log_min = min.log(*base);
let log_max = max.log(*base);
(log_value - log_min) / (log_max - log_min)
}
Self::Symlog { lin_threshold } => {
let symlog = |x: f64| -> f64 {
let thresh = *lin_threshold;
if x.abs() < thresh {
x / thresh
} else {
x.signum() * (1.0 + (x.abs() / thresh).ln())
}
};
let sym_value = symlog(value);
let sym_min = symlog(min);
let sym_max = symlog(max);
(sym_value - sym_min) / (sym_max - sym_min)
}
}
}
pub fn denormalize(&self, normalized: f64, min: f64, max: f64) -> f64 {
match self {
Self::Linear | Self::Time { .. } => min + normalized * (max - min),
Self::Logarithmic { base } => {
if min <= 0.0 || max <= 0.0 {
return min + normalized * (max - min);
}
let log_min = min.log(*base);
let log_max = max.log(*base);
let log_value = log_min + normalized * (log_max - log_min);
base.powf(log_value)
}
Self::Symlog { lin_threshold } => {
let thresh = *lin_threshold;
let symlog = |x: f64| -> f64 {
if x.abs() < thresh {
x / thresh
} else {
x.signum() * (1.0 + (x.abs() / thresh).ln())
}
};
let inv_symlog = |y: f64| -> f64 {
if y.abs() < 1.0 {
y * thresh
} else {
y.signum() * thresh * (y.abs() - 1.0).exp()
}
};
let sym_min = symlog(min);
let sym_max = symlog(max);
let sym_value = sym_min + normalized * (sym_max - sym_min);
inv_symlog(sym_value)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct AxisLink {
pub pan_group: Option<u32>,
pub zoom_group: Option<u32>,
pub inverted: bool,
}
impl AxisLink {
pub fn none() -> Self {
Self::default()
}
pub fn linked(group: u32) -> Self {
Self {
pan_group: Some(group),
zoom_group: Some(group),
inverted: false,
}
}
pub fn pan_only(group: u32) -> Self {
Self {
pan_group: Some(group),
zoom_group: None,
inverted: false,
}
}
pub fn zoom_only(group: u32) -> Self {
Self {
pan_group: None,
zoom_group: Some(group),
inverted: false,
}
}
pub fn inverted(mut self) -> Self {
self.inverted = true;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ExtendedAxisPosition {
Standard(AxisPosition),
AtValue(f64),
AtPercent(f32),
}
impl Default for ExtendedAxisPosition {
fn default() -> Self {
Self::Standard(AxisPosition::Left)
}
}
impl From<AxisPosition> for ExtendedAxisPosition {
fn from(pos: AxisPosition) -> Self {
Self::Standard(pos)
}
}
impl ExtendedAxisPosition {
pub fn is_standard(&self) -> bool {
matches!(self, Self::Standard(_))
}
pub fn standard(&self) -> Option<AxisPosition> {
match self {
Self::Standard(pos) => Some(*pos),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct TickConfig {
pub major_count: Option<usize>,
pub minor_count: usize,
pub show_labels: bool,
pub label_format: Option<String>,
pub custom_ticks: Option<Vec<(f64, String)>>,
pub label_rotation: f32,
pub ticks_inward: bool,
}
impl Default for TickConfig {
fn default() -> Self {
Self {
major_count: Some(5),
minor_count: 0,
show_labels: true,
label_format: None,
custom_ticks: None,
label_rotation: 0.0,
ticks_inward: false,
}
}
}
impl TickConfig {
pub fn with_count(count: usize) -> Self {
Self {
major_count: Some(count),
..Default::default()
}
}
pub fn custom(ticks: Vec<(f64, String)>) -> Self {
Self {
custom_ticks: Some(ticks),
..Default::default()
}
}
pub fn minor(mut self, count: usize) -> Self {
self.minor_count = count;
self
}
pub fn no_labels(mut self) -> Self {
self.show_labels = false;
self
}
pub fn format(mut self, fmt: impl Into<String>) -> Self {
self.label_format = Some(fmt.into());
self
}
pub fn rotated(mut self, degrees: f32) -> Self {
self.label_rotation = degrees;
self
}
pub fn inward(mut self) -> Self {
self.ticks_inward = true;
self
}
}
#[derive(Debug, Clone)]
pub struct EnhancedAxis {
pub id: AxisId,
pub name: Option<String>,
pub label: Option<String>,
pub orientation: AxisOrientation,
pub position: ExtendedAxisPosition,
pub position_offset: f32,
pub min: Option<f64>,
pub max: Option<f64>,
pub scale: ScaleType,
pub ticks: TickConfig,
pub style: AxisStyle,
pub visible: bool,
pub link: AxisLink,
pub auto_range: bool,
pub range_padding: f64,
}
impl Default for EnhancedAxis {
fn default() -> Self {
Self {
id: AxisId::default(),
name: None,
label: None,
orientation: AxisOrientation::Vertical,
position: ExtendedAxisPosition::Standard(AxisPosition::Left),
position_offset: 0.0,
min: None,
max: None,
scale: ScaleType::Linear,
ticks: TickConfig::default(),
style: AxisStyle::default(),
visible: true,
link: AxisLink::none(),
auto_range: true,
range_padding: 0.05,
}
}
}
impl EnhancedAxis {
pub fn x() -> Self {
Self {
id: AxisId::X_PRIMARY,
orientation: AxisOrientation::Horizontal,
position: ExtendedAxisPosition::Standard(AxisPosition::Bottom),
..Default::default()
}
}
pub fn y() -> Self {
Self {
id: AxisId::Y_PRIMARY,
orientation: AxisOrientation::Vertical,
position: ExtendedAxisPosition::Standard(AxisPosition::Left),
..Default::default()
}
}
pub fn x_secondary() -> Self {
Self {
id: AxisId::X_SECONDARY,
orientation: AxisOrientation::Horizontal,
position: ExtendedAxisPosition::Standard(AxisPosition::Top),
..Default::default()
}
}
pub fn y_secondary() -> Self {
Self {
id: AxisId::Y_SECONDARY,
orientation: AxisOrientation::Vertical,
position: ExtendedAxisPosition::Standard(AxisPosition::Right),
..Default::default()
}
}
pub fn custom(id: u32, name: impl Into<String>) -> Self {
Self {
id: AxisId::custom(id),
name: Some(name.into()),
..Default::default()
}
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn with_range(mut self, min: f64, max: f64) -> Self {
self.min = Some(min);
self.max = Some(max);
self.auto_range = false;
self
}
pub fn with_scale(mut self, scale: ScaleType) -> Self {
self.scale = scale;
self
}
pub fn with_position(mut self, position: impl Into<ExtendedAxisPosition>) -> Self {
self.position = position.into();
self
}
pub fn with_offset(mut self, offset: f32) -> Self {
self.position_offset = offset;
self
}
pub fn with_ticks(mut self, ticks: TickConfig) -> Self {
self.ticks = ticks;
self
}
pub fn with_tick_count(mut self, count: usize) -> Self {
self.ticks.major_count = Some(count);
self
}
pub fn with_style(mut self, style: AxisStyle) -> Self {
self.style = style;
self
}
pub fn with_visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
pub fn with_link(mut self, link: AxisLink) -> Self {
self.link = link;
self
}
pub fn auto_ranged(mut self, padding: f64) -> Self {
self.auto_range = true;
self.range_padding = padding;
self
}
pub fn effective_range(&self, data_bounds: Option<(f64, f64)>) -> (f64, f64) {
match (self.min, self.max) {
(Some(min), Some(max)) => (min, max),
(Some(min), None) => {
let max = data_bounds.map(|(_, max)| max).unwrap_or(1.0);
let padded_max = if self.auto_range {
max + (max - min).abs() * self.range_padding
} else {
max
};
(min, padded_max)
}
(None, Some(max)) => {
let min = data_bounds.map(|(min, _)| min).unwrap_or(0.0);
let padded_min = if self.auto_range {
min - (max - min).abs() * self.range_padding
} else {
min
};
(padded_min, max)
}
(None, None) => {
let (min, max) = data_bounds.unwrap_or((0.0, 1.0));
if self.auto_range {
let range = (max - min).abs();
let padding = if range < f64::EPSILON {
0.5 } else {
range * self.range_padding
};
(min - padding, max + padding)
} else {
(min, max)
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_linear_scale() {
let scale = ScaleType::Linear;
assert!((scale.normalize(50.0, 0.0, 100.0) - 0.5).abs() < 0.001);
assert!((scale.denormalize(0.5, 0.0, 100.0) - 50.0).abs() < 0.001);
}
#[test]
fn test_log_scale() {
let scale = ScaleType::log10();
let normalized = scale.normalize(10.0, 1.0, 100.0);
assert!((normalized - 0.5).abs() < 0.001);
let denormalized = scale.denormalize(0.5, 1.0, 100.0);
assert!((denormalized - 10.0).abs() < 0.001);
}
#[test]
fn test_symlog_scale() {
let scale = ScaleType::symlog(1.0);
let norm_zero = scale.normalize(0.0, -10.0, 10.0);
assert!((norm_zero - 0.5).abs() < 0.001);
let value = -5.0;
let normalized = scale.normalize(value, -10.0, 10.0);
let denormalized = scale.denormalize(normalized, -10.0, 10.0);
assert!((denormalized - value).abs() < 0.001);
}
#[test]
fn test_axis_effective_range() {
let axis = EnhancedAxis::y().auto_ranged(0.1);
let range = axis.effective_range(Some((0.0, 100.0)));
assert!(range.0 < 0.0); assert!(range.1 > 100.0); }
#[test]
fn test_axis_link() {
let link = AxisLink::linked(1).inverted();
assert_eq!(link.pan_group, Some(1));
assert_eq!(link.zoom_group, Some(1));
assert!(link.inverted);
}
}