1use std::fmt::Write as _;
2
3use densa_fmt::{civil_from_days, write_fixed, FmtBuf, FmtError};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7#[non_exhaustive]
8pub enum AxisKind {
9 Number,
11 Time,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq)]
21pub struct Axis {
22 start: f64,
23 end: f64,
24 kind: AxisKind,
25}
26
27const DAY: f64 = 86_400.0;
29
30const TIME_STEPS: [f64; 18] = [
33 1.0, 2.0, 5.0, 10.0, 15.0, 30.0, 60.0, 120.0, 300.0, 600.0, 900.0, 1_800.0, 3_600.0, 7_200.0,
34 10_800.0, 21_600.0, 43_200.0, DAY,
35];
36
37const MAX_TICK_INDEX: f64 = 9_007_199_254_740_992.0;
39
40impl Axis {
41 pub fn number(start: f64, end: f64) -> Self {
43 Self::new(start, end, AxisKind::Number)
44 }
45
46 pub fn time(start: f64, end: f64) -> Self {
48 Self::new(start, end, AxisKind::Time)
49 }
50
51 pub fn new(start: f64, end: f64, kind: AxisKind) -> Self {
53 let (start, end) = if start.is_finite() && end.is_finite() {
54 let limit = f64::MAX / 2.0;
56 let (low, high) = (start.min(end), start.max(end));
57 (low.clamp(-limit, limit), high.clamp(-limit, limit))
58 } else {
59 (0.0, 1.0)
60 };
61 let (start, end) = if end > start {
62 (start, end)
63 } else {
64 let half = if start == 0.0 {
65 1.0
66 } else {
67 (start.abs() * 0.01).max(f64::MIN_POSITIVE)
68 };
69 (start - half, start + half)
70 };
71 Self { start, end, kind }
72 }
73
74 pub fn start(&self) -> f64 {
76 self.start
77 }
78
79 pub fn end(&self) -> f64 {
81 self.end
82 }
83
84 pub fn span(&self) -> f64 {
86 self.end - self.start
87 }
88
89 pub fn kind(&self) -> AxisKind {
91 self.kind
92 }
93
94 #[must_use]
97 pub fn padded(self, fraction: f64) -> Self {
98 let pad = self.span() * fraction.max(0.0);
99 Self::new(self.start - pad, self.end + pad, self.kind)
100 }
101
102 #[must_use]
104 pub fn shifted(self, delta: f64) -> Self {
105 Self::new(self.start + delta, self.end + delta, self.kind)
106 }
107
108 #[must_use]
111 pub fn zoomed(self, factor: f64, anchor: f64) -> Self {
112 if !(factor > 0.0 && factor.is_finite() && anchor.is_finite()) {
113 return self;
114 }
115 Self::new(
116 anchor - (anchor - self.start) * factor,
117 anchor + (self.end - anchor) * factor,
118 self.kind,
119 )
120 }
121
122 pub fn fraction(&self, value: f64) -> f64 {
124 (value - self.start) / self.span()
125 }
126
127 pub fn value_at(&self, fraction: f64) -> f64 {
129 self.start + fraction * self.span()
130 }
131
132 pub fn ticks(&self, max: usize) -> Ticks {
135 let raw = self.span() / max.max(1) as f64;
137 let step = match self.kind {
138 AxisKind::Number => nice_step(raw),
139 AxisKind::Time if raw < 1.0 => nice_step(raw),
140 AxisKind::Time if raw > DAY => nice_step(raw / DAY) * DAY,
141 AxisKind::Time => TIME_STEPS
142 .iter()
143 .copied()
144 .find(|&step| step >= raw)
145 .unwrap_or(DAY),
146 };
147 let first = (self.start / step).ceil();
148 let last = (self.end / step).floor();
149 let usable = step.is_finite() && step > 0.0;
150 let (next, last) = if usable && first.abs() < MAX_TICK_INDEX && last.abs() < MAX_TICK_INDEX
151 {
152 (first as i64, last as i64)
154 } else {
155 (1, 0)
156 };
157 Ticks {
158 next,
159 last,
160 step,
161 kind: self.kind,
162 }
163 }
164}
165
166fn nice_step(raw: f64) -> f64 {
168 let magnitude = 10f64.powf(raw.log10().floor());
169 let norm = raw / magnitude;
170 let multiple = if norm <= 1.0 {
171 1.0
172 } else if norm <= 2.0 {
173 2.0
174 } else if norm <= 5.0 {
175 5.0
176 } else {
177 10.0
178 };
179 multiple * magnitude
180}
181
182#[derive(Debug, Clone)]
184pub struct Ticks {
185 next: i64,
186 last: i64,
187 step: f64,
188 kind: AxisKind,
189}
190
191impl Ticks {
192 pub fn step(&self) -> f64 {
194 self.step
195 }
196
197 pub fn write_label<const N: usize>(
205 &self,
206 value: f64,
207 text: &mut FmtBuf<N>,
208 ) -> Result<(), FmtError> {
209 if !value.is_finite() {
210 return Err(FmtError::NotFinite);
211 }
212 match self.kind {
213 AxisKind::Number => {
214 let digits = decimals(self.step);
215 let scale = 10f64.powi(digits as i32);
217 let value = if (value * scale).round() == 0.0 {
219 0.0
220 } else {
221 value
222 };
223 write_fixed(text, value, digits)
224 }
225 AxisKind::Time => {
226 let mark = text.len();
227 write_time(text, value, self.step).map_err(|_| {
228 text.truncate(mark);
229 FmtError::Overflow
230 })
231 }
232 }
233 }
234}
235
236impl Iterator for Ticks {
237 type Item = f64;
238
239 fn next(&mut self) -> Option<f64> {
240 if self.next > self.last {
241 return None;
242 }
243 let value = self.next as f64 * self.step;
245 self.next += 1;
246 Some(value)
247 }
248
249 fn size_hint(&self) -> (usize, Option<usize>) {
250 let left = usize::try_from(self.last - self.next + 1).unwrap_or(0);
251 (left, Some(left))
252 }
253}
254
255impl ExactSizeIterator for Ticks {}
256
257fn decimals(step: f64) -> usize {
259 let digits = (-step.log10() - 1e-9).ceil();
261 digits.clamp(0.0, 15.0) as usize
263}
264
265fn write_time<const N: usize>(text: &mut FmtBuf<N>, value: f64, step: f64) -> std::fmt::Result {
268 if step >= DAY {
269 let days = (value / DAY).floor() as i64;
271 let (year, month, day) = civil_from_days(days);
272 let sign = if year < 0 { "-" } else { "" };
273 let digits = year.unsigned_abs();
274 return write!(text, "{sign}{digits:04}-{month:02}-{day:02}");
275 }
276 let digits = if step < 1.0 { decimals(step).min(9) } else { 0 };
277 let scale = 10i64.pow(digits as u32);
279 let units = (value * scale as f64).round();
281 if units.abs() >= 9.0e18 {
282 return Err(std::fmt::Error);
283 }
284 let units = units as i64;
285 let seconds = units.div_euclid(scale).rem_euclid(86_400);
286 let fraction = units.rem_euclid(scale);
287 let (hours, minutes, secs) = (seconds / 3_600, seconds / 60 % 60, seconds % 60);
288 write!(text, "{hours:02}:{minutes:02}")?;
289 if step < 60.0 {
290 write!(text, ":{secs:02}")?;
291 }
292 if digits > 0 {
293 write!(text, ".{fraction:0digits$}")?;
294 }
295 Ok(())
296}