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 Category,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq)]
26pub struct Axis {
27 start: f64,
28 end: f64,
29 kind: AxisKind,
30}
31
32const DAY: f64 = 86_400.0;
34
35const TIME_STEPS: [f64; 18] = [
38 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,
39 10_800.0, 21_600.0, 43_200.0, DAY,
40];
41
42const MAX_TICK_INDEX: f64 = 9_007_199_254_740_992.0;
44
45impl Axis {
46 pub fn number(start: f64, end: f64) -> Self {
48 Self::new(start, end, AxisKind::Number)
49 }
50
51 pub fn time(start: f64, end: f64) -> Self {
53 Self::new(start, end, AxisKind::Time)
54 }
55
56 pub fn category(start: f64, end: f64) -> Self {
58 Self::new(start, end, AxisKind::Category)
59 }
60
61 pub fn new(start: f64, end: f64, kind: AxisKind) -> Self {
63 let (start, end) = if start.is_finite() && end.is_finite() {
64 let limit = f64::MAX / 2.0;
66 let (low, high) = (start.min(end), start.max(end));
67 (low.clamp(-limit, limit), high.clamp(-limit, limit))
68 } else {
69 (0.0, 1.0)
70 };
71 let (start, end) = if end > start {
72 (start, end)
73 } else {
74 let half = if start == 0.0 {
75 1.0
76 } else {
77 (start.abs() * 0.01).max(f64::MIN_POSITIVE)
78 };
79 (start - half, start + half)
80 };
81 Self { start, end, kind }
82 }
83
84 pub fn start(&self) -> f64 {
86 self.start
87 }
88
89 pub fn end(&self) -> f64 {
91 self.end
92 }
93
94 pub fn span(&self) -> f64 {
96 self.end - self.start
97 }
98
99 pub fn kind(&self) -> AxisKind {
101 self.kind
102 }
103
104 #[must_use]
107 pub fn padded(self, fraction: f64) -> Self {
108 let pad = self.span() * fraction.max(0.0);
109 Self::new(self.start - pad, self.end + pad, self.kind)
110 }
111
112 #[must_use]
114 pub fn shifted(self, delta: f64) -> Self {
115 Self::new(self.start + delta, self.end + delta, self.kind)
116 }
117
118 #[must_use]
121 pub fn zoomed(self, factor: f64, anchor: f64) -> Self {
122 if !(factor > 0.0 && factor.is_finite() && anchor.is_finite()) {
123 return self;
124 }
125 Self::new(
126 anchor - (anchor - self.start) * factor,
127 anchor + (self.end - anchor) * factor,
128 self.kind,
129 )
130 }
131
132 pub fn fraction(&self, value: f64) -> f64 {
134 (value - self.start) / self.span()
135 }
136
137 pub fn value_at(&self, fraction: f64) -> f64 {
139 self.start + fraction * self.span()
140 }
141
142 pub fn ticks(&self, max: usize) -> Ticks {
145 let raw = self.span() / max.max(1) as f64;
147 let step = match self.kind {
148 AxisKind::Number => nice_step(raw),
149 AxisKind::Category => nice_step(raw).max(1.0),
151 AxisKind::Time if raw < 1.0 => nice_step(raw),
152 AxisKind::Time if raw > DAY => nice_step(raw / DAY) * DAY,
153 AxisKind::Time => TIME_STEPS
154 .iter()
155 .copied()
156 .find(|&step| step >= raw)
157 .unwrap_or(DAY),
158 };
159 let first = (self.start / step).ceil();
160 let last = (self.end / step).floor();
161 let usable = step.is_finite() && step > 0.0;
162 let (next, last) = if usable && first.abs() < MAX_TICK_INDEX && last.abs() < MAX_TICK_INDEX
163 {
164 (first as i64, last as i64)
166 } else {
167 (1, 0)
168 };
169 Ticks {
170 next,
171 last,
172 step,
173 kind: self.kind,
174 }
175 }
176}
177
178fn nice_step(raw: f64) -> f64 {
180 let magnitude = 10f64.powf(raw.log10().floor());
181 let norm = raw / magnitude;
182 let multiple = if norm <= 1.0 {
183 1.0
184 } else if norm <= 2.0 {
185 2.0
186 } else if norm <= 5.0 {
187 5.0
188 } else {
189 10.0
190 };
191 multiple * magnitude
192}
193
194#[derive(Debug, Clone)]
196pub struct Ticks {
197 next: i64,
198 last: i64,
199 step: f64,
200 kind: AxisKind,
201}
202
203impl Ticks {
204 pub fn step(&self) -> f64 {
206 self.step
207 }
208
209 pub fn write_label<const N: usize>(
218 &self,
219 value: f64,
220 text: &mut FmtBuf<N>,
221 ) -> Result<(), FmtError> {
222 if !value.is_finite() {
223 return Err(FmtError::NotFinite);
224 }
225 match self.kind {
226 AxisKind::Number | AxisKind::Category => {
227 let digits = decimals(self.step);
228 let scale = 10f64.powi(digits as i32);
230 let value = if (value * scale).round() == 0.0 {
232 0.0
233 } else {
234 value
235 };
236 write_fixed(text, value, digits)
237 }
238 AxisKind::Time => {
239 let mark = text.len();
240 write_time(text, value, self.step).map_err(|_| {
241 text.truncate(mark);
242 FmtError::Overflow
243 })
244 }
245 }
246 }
247}
248
249impl Iterator for Ticks {
250 type Item = f64;
251
252 fn next(&mut self) -> Option<f64> {
253 if self.next > self.last {
254 return None;
255 }
256 let value = self.next as f64 * self.step;
258 self.next += 1;
259 Some(value)
260 }
261
262 fn size_hint(&self) -> (usize, Option<usize>) {
263 let left = usize::try_from(self.last - self.next + 1).unwrap_or(0);
264 (left, Some(left))
265 }
266}
267
268impl ExactSizeIterator for Ticks {}
269
270fn decimals(step: f64) -> usize {
272 let digits = (-step.log10() - 1e-9).ceil();
274 digits.clamp(0.0, 15.0) as usize
276}
277
278fn write_time<const N: usize>(text: &mut FmtBuf<N>, value: f64, step: f64) -> std::fmt::Result {
281 if step >= DAY {
282 let days = (value / DAY).floor() as i64;
284 let (year, month, day) = civil_from_days(days);
285 let sign = if year < 0 { "-" } else { "" };
286 let digits = year.unsigned_abs();
287 return write!(text, "{sign}{digits:04}-{month:02}-{day:02}");
288 }
289 let digits = if step < 1.0 { decimals(step).min(9) } else { 0 };
290 let scale = 10i64.pow(digits as u32);
292 let units = (value * scale as f64).round();
294 if units.abs() >= 9.0e18 {
295 return Err(std::fmt::Error);
296 }
297 let units = units as i64;
298 let seconds = units.div_euclid(scale).rem_euclid(86_400);
299 let fraction = units.rem_euclid(scale);
300 let (hours, minutes, secs) = (seconds / 3_600, seconds / 60 % 60, seconds % 60);
301 write!(text, "{hours:02}:{minutes:02}")?;
302 if step < 60.0 {
303 write!(text, ":{secs:02}")?;
304 }
305 if digits > 0 {
306 write!(text, ".{fraction:0digits$}")?;
307 }
308 Ok(())
309}