1use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta, Timelike};
6
7use crate::chart::chart_data::{XAxisTemporalKind, x_datetime, x_time};
8
9const MOST_TICKS: f64 = 4096.0;
11
12pub fn nice_steps(lo: f64, hi: f64, finest: f64, whole: bool) -> Vec<f64> {
17 let span = hi - lo;
18 if !(span > 0.0 && span.is_finite()) {
19 return Vec::new();
20 }
21 let finest = finest.max(span / MOST_TICKS);
22 let mut exp = finest.log10().floor() as i32;
23 let mut steps = Vec::new();
24 loop {
25 for m in [1.0, 2.0, 2.5, 5.0] {
26 let step = m * 10f64.powi(exp);
27 if step < finest * (1.0 - 1e-9) || (whole && step < 1.0) || (m == 2.5 && exp < 1) {
28 continue;
29 }
30 steps.push(step);
31 if step >= span {
32 return steps;
33 }
34 }
35 exp += 1;
36 }
37}
38
39fn clean(v: f64, step: f64) -> f64 {
41 let places = (-(step.log10() + 1e-9).floor()).max(0.0) as i32 + 1;
42 let scale = 10f64.powi(places.min(15));
43 let rounded = (v * scale).round() / scale;
44 if rounded == 0.0 { 0.0 } else { rounded }
46}
47
48pub fn multiples(lo: f64, hi: f64, step: f64) -> Vec<f64> {
50 if step.partial_cmp(&0.0) != Some(std::cmp::Ordering::Greater) || (hi - lo) / step > MOST_TICKS
51 {
52 return Vec::new();
53 }
54 let first = (lo / step - 1e-9).ceil() as i64;
55 let last = (hi / step + 1e-9).floor() as i64;
56 (first..=last)
57 .map(|k| clean(k as f64 * step, step))
58 .collect()
59}
60
61pub fn widen(lo: f64, hi: f64, step: f64) -> [f64; 2] {
63 let wlo = clean((lo / step + 1e-9).floor() * step, step);
64 let mut whi = clean((hi / step - 1e-9).ceil() * step, step);
65 if whi <= wlo {
66 whi = clean(wlo + step, step);
67 }
68 [wlo, whi]
69}
70
71pub fn widen_snug(lo: f64, hi: f64, step: f64, whole: bool) -> [f64; 2] {
75 let [wlo, whi] = widen(lo, hi, step);
76 let Some(&minor) = minor_steps(step, whole).last() else {
77 return [wlo, whi];
78 };
79 let [mlo, mhi] = widen(lo, hi, minor);
80 let most = step * 0.75;
81 [
82 if lo - wlo > most { mlo } else { wlo },
83 if whi - hi > most { mhi } else { whi },
84 ]
85}
86
87pub fn minor_steps(step: f64, whole: bool) -> Vec<f64> {
91 let mantissa = step / 10f64.powf((step.log10() + 1e-9).floor());
92 let divisors: &[f64] = if (mantissa - 1.0).abs() < 1e-6 {
93 &[5.0, 2.0]
94 } else if (mantissa - 2.0).abs() < 1e-6 {
95 &[4.0, 2.0]
96 } else {
97 &[5.0]
99 };
100 divisors
101 .iter()
102 .map(|d| step / d)
103 .filter(|s| !whole || *s >= 1.0 - 1e-9)
104 .collect()
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
109pub enum Unit {
110 Second,
111 Minute,
112 Hour,
113 Day,
114 Month,
115 Year,
116}
117
118impl Unit {
119 fn seconds(self) -> f64 {
121 match self {
122 Unit::Second => 1.0,
123 Unit::Minute => 60.0,
124 Unit::Hour => 3600.0,
125 Unit::Day => 86_400.0,
126 Unit::Month => 2_629_746.0,
127 Unit::Year => 31_556_952.0,
128 }
129 }
130}
131
132#[derive(Clone, Copy, Debug, PartialEq)]
135pub struct CalendarStep {
136 pub unit: Unit,
137 pub n: u32,
138}
139
140const CALENDAR_STEPS: &[(Unit, &[u32])] = &[
141 (Unit::Second, &[1, 2, 5, 10, 15, 30]),
142 (Unit::Minute, &[1, 2, 5, 10, 15, 30]),
143 (Unit::Hour, &[1, 2, 3, 6, 12]),
144 (Unit::Day, &[1, 2, 7, 14]),
146 (Unit::Month, &[1, 2, 3, 6]),
147 (Unit::Year, &[1, 2, 5, 10, 20, 50, 100, 200, 500, 1000]),
148];
149
150pub fn calendar_steps(kind: XAxisTemporalKind) -> impl Iterator<Item = CalendarStep> {
153 CALENDAR_STEPS
154 .iter()
155 .flat_map(|(unit, ns)| ns.iter().map(move |&n| CalendarStep { unit: *unit, n }))
156 .filter(move |s| match kind {
157 XAxisTemporalKind::Date => s.unit >= Unit::Day,
158 XAxisTemporalKind::Time => s.unit <= Unit::Hour,
159 _ => true,
160 })
161}
162
163pub fn to_datetime(v: f64, kind: XAxisTemporalKind) -> Option<NaiveDateTime> {
166 match kind {
167 XAxisTemporalKind::Numeric => None,
168 XAxisTemporalKind::Time => Some(epoch_day().and_time(x_time(v)?)),
169 _ => x_datetime(v, kind),
170 }
171}
172
173fn epoch_day() -> NaiveDate {
174 NaiveDate::from_ymd_opt(1970, 1, 1).unwrap_or_default()
175}
176
177pub fn from_datetime(at: NaiveDateTime, kind: XAxisTemporalKind) -> Option<f64> {
179 let utc = at.and_utc();
180 Some(match kind {
181 XAxisTemporalKind::Numeric => return None,
182 XAxisTemporalKind::Date => (at.date() - epoch_day()).num_days() as f64,
183 XAxisTemporalKind::DatetimeUs => utc.timestamp_micros() as f64,
184 XAxisTemporalKind::DatetimeMs => utc.timestamp_millis() as f64,
185 XAxisTemporalKind::DatetimeNs => utc.timestamp_nanos_opt()? as f64,
186 XAxisTemporalKind::Time => {
187 let t = at.time();
188 f64::from(t.num_seconds_from_midnight()) * 1e9 + f64::from(t.nanosecond())
189 }
190 })
191}
192
193pub fn calendar_ticks(
196 lo: NaiveDateTime,
197 hi: NaiveDateTime,
198 step: CalendarStep,
199) -> Vec<NaiveDateTime> {
200 let span = (hi - lo).as_seconds_f64();
201 let n = step.n.max(1);
202 if span < 0.0 || span / (step.unit.seconds() * f64::from(n)) > MOST_TICKS {
203 return Vec::new();
204 }
205 let midnight = |d: NaiveDate| d.and_time(NaiveTime::MIN);
206 let mut ticks = Vec::new();
207 let mut push = |at: NaiveDateTime| {
208 if at >= lo && at <= hi {
209 ticks.push(at);
210 }
211 at <= hi
212 };
213 match step.unit {
214 Unit::Year => {
215 let n = n as i32;
216 let mut year = lo.year().div_euclid(n) * n;
217 while let Some(d) = NaiveDate::from_ymd_opt(year, 1, 1) {
218 if !push(midnight(d)) {
219 break;
220 }
221 year += n;
222 }
223 }
224 Unit::Month => {
225 let n = n as i32;
226 let first = lo.year() * 12 + lo.month0() as i32;
227 let mut month = first - first.rem_euclid(n);
228 while let Some(d) =
229 NaiveDate::from_ymd_opt(month.div_euclid(12), month.rem_euclid(12) as u32 + 1, 1)
230 {
231 if !push(midnight(d)) {
232 break;
233 }
234 month += n;
235 }
236 }
237 Unit::Day if n == 1 => {
238 let mut day = lo.date();
239 while push(midnight(day)) {
240 let Some(next) = day.succ_opt() else { break };
241 day = next;
242 }
243 }
244 Unit::Day => {
245 let last = if n == 2 { 29 } else { 28 };
248 let (mut year, mut month) = (lo.year(), lo.month());
249 'months: while let Some(first) = NaiveDate::from_ymd_opt(year, month, 1) {
250 for day in (1..=last).step_by(n as usize) {
251 if let Some(d) = first.with_day(day)
252 && !push(midnight(d))
253 {
254 break 'months;
255 }
256 }
257 (year, month) = if month == 12 {
258 (year + 1, 1)
259 } else {
260 (year, month + 1)
261 };
262 }
263 }
264 Unit::Hour | Unit::Minute | Unit::Second => {
265 let every = step.unit.seconds() as i64 * i64::from(n);
268 let day = midnight(lo.date());
269 let into = (lo - day).num_seconds();
270 let mut at = day + TimeDelta::seconds(into.div_euclid(every) * every);
271 while push(at) {
272 at += TimeDelta::seconds(every);
273 }
274 }
275 }
276 ticks
277}
278
279pub fn calendar_labels(
285 ticks: &[NaiveDateTime],
286 unit: Unit,
287 kind: XAxisTemporalKind,
288 context: bool,
289) -> Vec<String> {
290 ticks
291 .iter()
292 .enumerate()
293 .map(|(i, at)| {
294 let pattern = if kind == XAxisTemporalKind::Time {
295 if unit == Unit::Second {
296 "%H:%M:%S"
297 } else {
298 "%H:%M"
299 }
300 } else {
301 calendar_pattern(at, unit, context && i == 0)
302 };
303 at.format(pattern).to_string()
304 })
305 .collect()
306}
307
308fn calendar_pattern(at: &NaiveDateTime, unit: Unit, first: bool) -> &'static str {
309 let day_start = at.time() == NaiveTime::MIN;
310 let month_start = day_start && at.day() == 1;
311 let year_start = month_start && at.month() == 1;
312 if year_start || unit == Unit::Year {
313 return "%Y";
314 }
315 match unit {
316 Unit::Month if first => "%b %Y",
317 Unit::Month => "%b",
318 Unit::Day if first => "%b %-d %Y",
319 _ if month_start && !first => "%b",
320 Unit::Day => "%-d",
321 _ if day_start && first => "%b %-d %Y",
322 _ if day_start => "%b %-d",
323 Unit::Second if first => "%b %-d %Y %H:%M:%S",
324 Unit::Second => "%H:%M:%S",
325 _ if first => "%b %-d %Y %H:%M",
326 _ => "%H:%M",
327 }
328}
329
330#[cfg(test)]
331mod tests;