use crate::config::{AxisOptions, AxisScale, Config, LabelStyle};
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum LabelFormat {
Decimal,
Scientific,
Power,
Timestamp(TimestampLabelFormat),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TimestampUnit {
#[default]
Seconds,
Milliseconds,
Microseconds,
Nanoseconds,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TimestampZone {
#[default]
Utc,
FixedOffsetMinutes(i32),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TimestampLabelMode {
#[default]
Auto,
Pattern(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum FractionalSecondDigits {
#[default]
Auto,
Fixed(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TimestampTickPolicy {
#[default]
AutoCalendar,
NumericSpacing,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct TimestampLabelFormat {
pub unit: TimestampUnit,
pub timezone: TimestampZone,
pub label: TimestampLabelMode,
pub fractional: FractionalSecondDigits,
pub tick_policy: TimestampTickPolicy,
}
fn auto_sig_linear(min: f64, max: f64, step: f64) -> u8 {
let max_abs = f64::max(min.abs(), max.abs());
if max_abs == 0.0 || step <= 0.0 {
return 1;
}
let order_max = max_abs.log10().floor() as i32;
let order_step = step.log10().floor() as i32;
let sig = if step >= 1.0 {
order_max + 1
} else {
order_max - order_step + 1
};
sig.clamp(1, 15) as u8
}
fn auto_sig_log(_min: f64, _max: f64, _major_spacing: f64) -> u8 {
2
}
impl LabelStyle {
pub fn compute_auto_significant_digits(
scale: AxisScale,
min: f64,
max: f64,
major_spacing: f64,
) -> u8 {
match scale {
AxisScale::Linear => auto_sig_linear(min, max, major_spacing),
AxisScale::Logarithmic => auto_sig_log(min, max, major_spacing),
}
}
}
impl AxisOptions {
pub fn auto_significant_digits(&mut self) {
let sig = LabelStyle::compute_auto_significant_digits(
self.scale.clone(),
self.min,
self.max,
self.major_spacing,
);
self.label_style.significant_digits = sig.max(1);
}
}
impl Config {
pub fn auto_significant_digits_all(&mut self) {
self.top_x.auto_significant_digits();
self.bottom_x.auto_significant_digits();
self.left_y.auto_significant_digits();
self.right_y.auto_significant_digits();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::default::default_config;
#[test]
fn sig_digits_bounds() {
for (min, max, step) in [
(0.0, 1.0, 0.2),
(0.0, 1000.0, 200.0),
(0.0, 0.01, 0.002),
(-5.0, 5.0, 1.0),
(100.0, 110.0, 2.0),
(0.0, 0.0, 0.0),
] {
let s = LabelStyle::compute_auto_significant_digits(AxisScale::Linear, min, max, step);
assert!((1..=15).contains(&s), "sig out of bounds: {}", s);
}
}
#[test]
fn sig_digits_spec_examples() {
let cases: &[(f64, f64, f64, u8)] = &[
(0.0, 1.0, 0.2, 2),
(0.0, 1000.0, 200.0, 4),
(0.0, 0.01, 0.002, 2),
(-5.0, 5.0, 1.0, 1),
(100.0, 110.0, 2.0, 3),
];
for &(min, max, step, expected) in cases {
let got =
LabelStyle::compute_auto_significant_digits(AxisScale::Linear, min, max, step);
assert_eq!(got, expected, "min={}, max={}, step={}", min, max, step);
}
}
#[test]
fn log_sig_digits_default_2() {
let s =
LabelStyle::compute_auto_significant_digits(AxisScale::Logarithmic, 0.001, 1000.0, 1.0);
assert_eq!(s, 2);
}
#[cfg(feature = "serde")]
#[test]
fn timestamp_format_serde_roundtrip() {
let fmt = LabelFormat::Timestamp(TimestampLabelFormat {
unit: TimestampUnit::Milliseconds,
timezone: TimestampZone::FixedOffsetMinutes(540),
label: TimestampLabelMode::Pattern("%Y-%m-%d %H:%M:%S.%f".into()),
fractional: FractionalSecondDigits::Fixed(3),
tick_policy: TimestampTickPolicy::NumericSpacing,
});
let json = serde_json::to_string(&fmt).unwrap();
let got: LabelFormat = serde_json::from_str(&json).unwrap();
assert_eq!(got, fmt);
}
#[test]
fn auto_sig_changes_only_sig_field() {
let mut cfg = default_config();
let before = cfg.clone();
cfg.top_x.auto_significant_digits();
let mut expected = before.clone();
expected.top_x.label_style.significant_digits = LabelStyle::compute_auto_significant_digits(
AxisScale::Linear,
expected.top_x.min,
expected.top_x.max,
expected.top_x.major_spacing,
);
assert_eq!(cfg, expected);
}
#[test]
fn auto_sig_all_changes_only_sig_fields() {
let mut cfg = default_config();
let before = cfg.clone();
cfg.auto_significant_digits_all();
assert!(cfg.top_x.label_style.significant_digits >= 1);
assert!(cfg.bottom_x.label_style.significant_digits >= 1);
assert!(cfg.left_y.label_style.significant_digits >= 1);
assert!(cfg.right_y.label_style.significant_digits >= 1);
assert_eq!(cfg.chart_area, before.chart_area);
assert_eq!(cfg.grid, before.grid);
assert_eq!(cfg.chart_title, before.chart_title);
}
}