Skip to main content

ggplot_rs/scale/
datetime.rs

1use crate::aes::Aesthetic;
2use crate::data::Value;
3
4use super::Scale;
5
6/// Date/time scale — maps epoch seconds to [0, 1] and formats axis labels as dates.
7#[derive(Clone, Debug)]
8pub struct ScaleDateTime {
9    aesthetic: Aesthetic,
10    name: String,
11    min: f64,
12    max: f64,
13    trained: bool,
14    expand: (f64, f64),
15}
16
17impl ScaleDateTime {
18    pub fn new() -> Self {
19        ScaleDateTime {
20            aesthetic: Aesthetic::X,
21            name: String::new(),
22            min: f64::INFINITY,
23            max: f64::NEG_INFINITY,
24            trained: false,
25            expand: (0.05, 0.0),
26        }
27    }
28
29    pub fn for_aesthetic(mut self, aes: Aesthetic) -> Self {
30        self.aesthetic = aes;
31        self
32    }
33
34    pub fn with_name(mut self, name: &str) -> Self {
35        self.name = name.to_string();
36        self
37    }
38
39    fn expanded_range(&self) -> (f64, f64) {
40        let range = self.max - self.min;
41        let mult = self.expand.0;
42        let add = self.expand.1;
43        (self.min - range * mult - add, self.max + range * mult + add)
44    }
45
46    /// Choose a "nice" step size in seconds for date/time breaks.
47    fn nice_datetime_step(range_secs: f64) -> f64 {
48        const MINUTE: f64 = 60.0;
49        const HOUR: f64 = 3600.0;
50        const DAY: f64 = 86400.0;
51        const WEEK: f64 = 7.0 * DAY;
52        const MONTH: f64 = 30.0 * DAY;
53        const YEAR: f64 = 365.25 * DAY;
54
55        let candidates = [
56            1.0,
57            5.0,
58            10.0,
59            30.0,
60            MINUTE,
61            5.0 * MINUTE,
62            10.0 * MINUTE,
63            30.0 * MINUTE,
64            HOUR,
65            3.0 * HOUR,
66            6.0 * HOUR,
67            12.0 * HOUR,
68            DAY,
69            2.0 * DAY,
70            WEEK,
71            2.0 * WEEK,
72            MONTH,
73            3.0 * MONTH,
74            6.0 * MONTH,
75            YEAR,
76            2.0 * YEAR,
77            5.0 * YEAR,
78            10.0 * YEAR,
79            20.0 * YEAR,
80            50.0 * YEAR,
81            100.0 * YEAR,
82        ];
83
84        let target = range_secs / 5.0;
85        for &c in &candidates {
86            if c >= target {
87                return c;
88            }
89        }
90        // For very large ranges, use multiples of 100 years
91        let n = (target / (100.0 * YEAR)).ceil();
92        n * 100.0 * YEAR
93    }
94
95    /// Format a timestamp (epoch seconds) as a human-readable label,
96    /// adapting precision to the break step size.
97    fn format_datetime(secs: f64, _step: f64) -> String {
98        let epoch_secs = secs as i64;
99        crate::data::format_epoch_secs(epoch_secs)
100    }
101}
102
103impl Default for ScaleDateTime {
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109impl Scale for ScaleDateTime {
110    fn aesthetic(&self) -> Aesthetic {
111        self.aesthetic.clone()
112    }
113
114    fn train(&mut self, values: &[Value]) {
115        for v in values {
116            if let Some(f) = v.as_f64() {
117                if f.is_finite() {
118                    if f < self.min {
119                        self.min = f;
120                    }
121                    if f > self.max {
122                        self.max = f;
123                    }
124                }
125            }
126        }
127        self.trained = true;
128    }
129
130    fn map(&self, value: &Value) -> f64 {
131        let f = match value.as_f64() {
132            Some(f) => f,
133            None => return 0.0,
134        };
135        let (emin, emax) = self.expanded_range();
136        let range = emax - emin;
137        if range.abs() < f64::EPSILON {
138            0.5
139        } else {
140            (f - emin) / range
141        }
142    }
143
144    fn breaks(&self) -> Vec<(f64, String)> {
145        if !self.trained || self.min > self.max {
146            return vec![];
147        }
148
149        let range = self.max - self.min;
150        if range.abs() < f64::EPSILON {
151            let label = Self::format_datetime(self.min, 1.0);
152            return vec![(0.5, label)];
153        }
154
155        let (emin, emax) = self.expanded_range();
156        let step = Self::nice_datetime_step(range);
157
158        let start = (emin / step).ceil() * step;
159        let mut breaks = Vec::new();
160        let mut v = start;
161        while v <= emax + step * 0.001 {
162            let pos = self.map(&Value::Float(v));
163            let label = Self::format_datetime(v, step);
164            breaks.push((pos, label));
165            v += step;
166        }
167        breaks
168    }
169
170    fn name(&self) -> &str {
171        &self.name
172    }
173
174    fn set_name(&mut self, name: &str) {
175        self.name = name.to_string();
176    }
177
178    fn set_limits(&mut self, min: f64, max: f64) {
179        self.min = min;
180        self.max = max;
181        self.trained = true;
182    }
183
184    fn clone_box(&self) -> Box<dyn Scale> {
185        Box::new(self.clone())
186    }
187
188    fn reset_training(&mut self) {
189        self.min = f64::INFINITY;
190        self.max = f64::NEG_INFINITY;
191        self.trained = false;
192    }
193}