1use crate::config::{AxisOptions, AxisScale, Config, LabelStyle};
2
3#[derive(Debug, Clone, PartialEq)]
4#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
5pub enum LabelFormat {
6 Decimal,
8 Scientific,
10 Power,
12 Timestamp(TimestampLabelFormat),
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub enum TimestampUnit {
20 #[default]
21 Seconds,
22 Milliseconds,
23 Microseconds,
24 Nanoseconds,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub enum TimestampZone {
30 #[default]
31 Utc,
32 FixedOffsetMinutes(i32),
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Default)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38pub enum TimestampLabelMode {
39 #[default]
40 Auto,
41 Pattern(String),
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47pub enum FractionalSecondDigits {
48 #[default]
49 Auto,
50 Fixed(u8),
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
54#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
55pub enum TimestampTickPolicy {
56 #[default]
58 AutoCalendar,
59 NumericSpacing,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Default)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65#[cfg_attr(feature = "serde", serde(default))]
66pub struct TimestampLabelFormat {
67 pub unit: TimestampUnit,
68 pub timezone: TimestampZone,
69 pub label: TimestampLabelMode,
70 pub fractional: FractionalSecondDigits,
71 pub tick_policy: TimestampTickPolicy,
72}
73
74fn auto_sig_linear(min: f64, max: f64, step: f64) -> u8 {
75 let max_abs = f64::max(min.abs(), max.abs());
76 if max_abs == 0.0 || step <= 0.0 {
77 return 1;
78 }
79 let order_max = max_abs.log10().floor() as i32;
80 let order_step = step.log10().floor() as i32;
81 let sig = if step >= 1.0 {
84 order_max + 1
85 } else {
86 order_max - order_step + 1
87 };
88 sig.clamp(1, 15) as u8
89}
90
91fn auto_sig_log(_min: f64, _max: f64, _major_spacing: f64) -> u8 {
92 2
93}
94
95impl LabelStyle {
96 pub fn compute_auto_significant_digits(
97 scale: AxisScale,
98 min: f64,
99 max: f64,
100 major_spacing: f64,
101 ) -> u8 {
102 match scale {
103 AxisScale::Linear => auto_sig_linear(min, max, major_spacing),
104 AxisScale::Logarithmic => auto_sig_log(min, max, major_spacing),
105 }
106 }
107}
108
109impl AxisOptions {
110 pub fn auto_significant_digits(&mut self) {
111 let sig = LabelStyle::compute_auto_significant_digits(
112 self.scale.clone(),
113 self.min,
114 self.max,
115 self.major_spacing,
116 );
117 self.label_style.significant_digits = sig.max(1);
118 }
119}
120
121impl Config {
122 pub fn auto_significant_digits_all(&mut self) {
123 self.top_x.auto_significant_digits();
124 self.bottom_x.auto_significant_digits();
125 self.left_y.auto_significant_digits();
126 self.right_y.auto_significant_digits();
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::default::default_config;
134
135 #[test]
137 fn sig_digits_bounds() {
138 for (min, max, step) in [
139 (0.0, 1.0, 0.2),
140 (0.0, 1000.0, 200.0),
141 (0.0, 0.01, 0.002),
142 (-5.0, 5.0, 1.0),
143 (100.0, 110.0, 2.0),
144 (0.0, 0.0, 0.0),
145 ] {
146 let s = LabelStyle::compute_auto_significant_digits(AxisScale::Linear, min, max, step);
147 assert!((1..=15).contains(&s), "sig out of bounds: {}", s);
148 }
149 }
150
151 #[test]
152 fn sig_digits_spec_examples() {
153 let cases: &[(f64, f64, f64, u8)] = &[
154 (0.0, 1.0, 0.2, 2),
155 (0.0, 1000.0, 200.0, 4),
156 (0.0, 0.01, 0.002, 2),
157 (-5.0, 5.0, 1.0, 1),
158 (100.0, 110.0, 2.0, 3),
159 ];
160 for &(min, max, step, expected) in cases {
161 let got =
162 LabelStyle::compute_auto_significant_digits(AxisScale::Linear, min, max, step);
163 assert_eq!(got, expected, "min={}, max={}, step={}", min, max, step);
164 }
165 }
166
167 #[test]
168 fn log_sig_digits_default_2() {
169 let s =
170 LabelStyle::compute_auto_significant_digits(AxisScale::Logarithmic, 0.001, 1000.0, 1.0);
171 assert_eq!(s, 2);
172 }
173
174 #[cfg(feature = "serde")]
175 #[test]
176 fn timestamp_format_serde_roundtrip() {
177 let fmt = LabelFormat::Timestamp(TimestampLabelFormat {
178 unit: TimestampUnit::Milliseconds,
179 timezone: TimestampZone::FixedOffsetMinutes(540),
180 label: TimestampLabelMode::Pattern("%Y-%m-%d %H:%M:%S.%f".into()),
181 fractional: FractionalSecondDigits::Fixed(3),
182 tick_policy: TimestampTickPolicy::NumericSpacing,
183 });
184 let json = serde_json::to_string(&fmt).unwrap();
185 let got: LabelFormat = serde_json::from_str(&json).unwrap();
186 assert_eq!(got, fmt);
187 }
188
189 #[test]
191 fn auto_sig_changes_only_sig_field() {
192 let mut cfg = default_config();
193 let before = cfg.clone();
194 cfg.top_x.auto_significant_digits();
195 let mut expected = before.clone();
196 expected.top_x.label_style.significant_digits = LabelStyle::compute_auto_significant_digits(
197 AxisScale::Linear,
198 expected.top_x.min,
199 expected.top_x.max,
200 expected.top_x.major_spacing,
201 );
202 assert_eq!(cfg, expected);
203 }
204
205 #[test]
207 fn auto_sig_all_changes_only_sig_fields() {
208 let mut cfg = default_config();
209 let before = cfg.clone();
210 cfg.auto_significant_digits_all();
211 assert!(cfg.top_x.label_style.significant_digits >= 1);
212 assert!(cfg.bottom_x.label_style.significant_digits >= 1);
213 assert!(cfg.left_y.label_style.significant_digits >= 1);
214 assert!(cfg.right_y.label_style.significant_digits >= 1);
215 assert_eq!(cfg.chart_area, before.chart_area);
216 assert_eq!(cfg.grid, before.grid);
217 assert_eq!(cfg.chart_title, before.chart_title);
218 }
219}