chartml_core/scales/
time.rs1use chrono::NaiveDateTime;
2use super::tick_step;
3
4pub struct ScaleTime {
7 domain: (NaiveDateTime, NaiveDateTime),
8 range: (f64, f64),
9}
10
11impl ScaleTime {
12 pub fn new(domain: (NaiveDateTime, NaiveDateTime), range: (f64, f64)) -> Self {
14 Self { domain, range }
15 }
16
17 pub fn map(&self, value: &NaiveDateTime) -> f64 {
19 let t = self.to_timestamp(value);
20 let (t0, t1) = self.domain_timestamps();
21 let (r0, r1) = self.range;
22 let domain_span = t1 - t0;
23 if domain_span == 0.0 {
24 return (r0 + r1) / 2.0;
25 }
26 r0 + (t - t0) / domain_span * (r1 - r0)
27 }
28
29 pub fn invert(&self, value: f64) -> NaiveDateTime {
31 let (t0, t1) = self.domain_timestamps();
32 let (r0, r1) = self.range;
33 let range_span = r1 - r0;
34 let t = if range_span == 0.0 {
35 (t0 + t1) / 2.0
36 } else {
37 t0 + (value - r0) / range_span * (t1 - t0)
38 };
39 self.secs_to_datetime(t)
40 }
41
42 pub fn ticks(&self, count: usize) -> Vec<NaiveDateTime> {
46 if count == 0 {
47 return vec![];
48 }
49 let (t0, t1) = self.domain_timestamps();
50 let min = t0.min(t1);
51 let max = t0.max(t1);
52 if min == max {
53 return vec![self.secs_to_datetime(min)];
54 }
55
56 let step = tick_step(min, max, count);
57 if step == 0.0 || !step.is_finite() {
58 return vec![];
59 }
60
61 let mut ticks = Vec::new();
62 let start = (min / step).ceil();
63 let stop = (max / step).floor();
64
65 let mut i = start;
66 while i <= stop {
67 let tick = i * step;
68 ticks.push(self.secs_to_datetime(tick));
69 i += 1.0;
70 }
71
72 if t0 > t1 {
73 ticks.reverse();
74 }
75
76 ticks
77 }
78
79 pub fn domain(&self) -> (NaiveDateTime, NaiveDateTime) {
81 self.domain
82 }
83
84 pub fn range(&self) -> (f64, f64) {
86 self.range
87 }
88
89 fn to_timestamp(&self, dt: &NaiveDateTime) -> f64 {
90 dt.and_utc().timestamp() as f64
91 }
92
93 fn domain_timestamps(&self) -> (f64, f64) {
94 (
95 self.to_timestamp(&self.domain.0),
96 self.to_timestamp(&self.domain.1),
97 )
98 }
99
100 fn secs_to_datetime(&self, secs: f64) -> NaiveDateTime {
101 let secs = (secs as i64).clamp(-8_334_632_851_200, 8_210_298_412_799);
103 chrono::DateTime::from_timestamp(secs, 0)
104 .unwrap_or_else(|| {
105 chrono::DateTime::from_timestamp(0, 0).unwrap()
107 })
108 .naive_utc()
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use chrono::NaiveDate;
116
117 fn make_dt(year: i32, month: u32, day: u32) -> NaiveDateTime {
118 NaiveDate::from_ymd_opt(year, month, day)
119 .unwrap()
120 .and_hms_opt(0, 0, 0)
121 .unwrap()
122 }
123
124 #[test]
125 fn time_scale_maps_midpoint() {
126 let start = make_dt(2024, 1, 1);
127 let end = make_dt(2024, 1, 31);
128 let mid = make_dt(2024, 1, 16);
129 let scale = ScaleTime::new((start, end), (0.0, 100.0));
130 let result = scale.map(&mid);
131 assert!(
132 (result - 50.0).abs() < 2.0,
133 "midpoint should map close to 50, got {}",
134 result
135 );
136 }
137
138 #[test]
139 fn time_scale_maps_endpoints() {
140 let start = make_dt(2024, 1, 1);
141 let end = make_dt(2024, 1, 31);
142 let scale = ScaleTime::new((start, end), (0.0, 100.0));
143 assert!(
144 (scale.map(&start) - 0.0).abs() < 1e-10,
145 "start should map to 0"
146 );
147 assert!(
148 (scale.map(&end) - 100.0).abs() < 1e-10,
149 "end should map to 100"
150 );
151 }
152
153 #[test]
154 fn time_scale_inverts() {
155 let start = make_dt(2024, 1, 1);
156 let end = make_dt(2024, 1, 31);
157 let scale = ScaleTime::new((start, end), (0.0, 100.0));
158 let mid_value = 50.0;
159 let inverted = scale.invert(mid_value);
160 let mid = make_dt(2024, 1, 16);
162 let diff_secs = (inverted.and_utc().timestamp() - mid.and_utc().timestamp()).abs();
163 assert!(
164 diff_secs < 86400, "inverted date should be close to midpoint, got {:?}",
166 inverted
167 );
168 }
169
170 #[test]
171 fn time_scale_ticks() {
172 let start = make_dt(2024, 1, 1);
173 let end = make_dt(2024, 12, 31);
174 let scale = ScaleTime::new((start, end), (0.0, 100.0));
175 let ticks = scale.ticks(5);
176 assert!(
177 !ticks.is_empty(),
178 "should generate at least one tick"
179 );
180 assert!(
181 ticks.len() <= 15,
182 "should not generate too many ticks, got {}",
183 ticks.len()
184 );
185 for tick in &ticks {
187 assert!(*tick >= start, "tick {:?} should be >= start", tick);
188 assert!(*tick <= end, "tick {:?} should be <= end", tick);
189 }
190 }
191}