1use chrono::NaiveDate;
4
5#[derive(Clone, Debug, PartialEq)]
7pub struct BandScale {
8 domain: Vec<String>,
9 range_start: f64,
10 range_end: f64,
11 step: f64,
12 bandwidth: f64,
13 padding_inner: f64,
14 padding_outer: f64,
15}
16
17impl BandScale {
18 pub fn new(domain: Vec<String>, range: (f64, f64), padding_inner: f64) -> Self {
23 Self::with_padding(domain, range, padding_inner, padding_inner)
24 }
25
26 pub fn with_padding(
28 domain: Vec<String>,
29 range: (f64, f64),
30 padding_inner: f64,
31 padding_outer: f64,
32 ) -> Self {
33 let n = domain.len().max(1) as f64;
34 let range_start = range.0.min(range.1);
35 let range_end = range.0.max(range.1);
36 let padding_inner = padding_inner.clamp(0.0, 1.0);
37 let padding_outer = padding_outer.clamp(0.0, 1.0);
38 let inner_gaps = padding_inner * (n - 1.0).max(0.0);
39 let step = (range_end - range_start) / (n + inner_gaps + padding_outer * 2.0).max(1.0);
40 let bandwidth = step * (1.0 - padding_inner);
41
42 Self {
43 domain,
44 range_start,
45 range_end,
46 step,
47 bandwidth,
48 padding_inner,
49 padding_outer,
50 }
51 }
52
53 pub fn bandwidth(&self) -> f64 {
55 self.bandwidth
56 }
57
58 pub fn step(&self) -> f64 {
60 self.step
61 }
62
63 pub fn scale(&self, category: &str) -> Option<f64> {
65 let index = self.domain.iter().position(|c| c == category)?;
66 self.scale_by_index(index)
67 }
68
69 pub fn scale_by_index(&self, index: usize) -> Option<f64> {
71 if index >= self.domain.len() {
72 return None;
73 }
74 let band_start =
75 self.range_start + self.padding_outer * self.step + self.step * index as f64;
76 Some(band_start + self.bandwidth / 2.0)
77 }
78
79 pub fn domain(&self) -> &[String] {
81 &self.domain
82 }
83
84 pub fn index_at(&self, position: f64) -> Option<usize> {
86 if self.domain.is_empty() {
87 return None;
88 }
89 let pos = position.clamp(self.range_start, self.range_end);
90 let relative = pos - self.range_start - self.padding_outer * self.step;
91 if relative < 0.0 {
92 return None;
93 }
94 let idx = (relative / self.step).floor() as usize;
95 if idx >= self.domain.len() {
96 None
97 } else {
98 Some(idx)
99 }
100 }
101
102 pub fn position_to_fraction(&self, position: f64) -> Option<f64> {
104 let idx = self.index_at(position)?;
105 Some((idx as f64 + 0.5) / self.domain.len() as f64)
106 }
107
108 pub fn band_rect(&self, index: usize) -> Option<(f64, f64)> {
110 if index >= self.domain.len() {
111 return None;
112 }
113 let x = self.range_start + self.padding_outer * self.step + self.step * index as f64;
114 Some((x, self.bandwidth))
115 }
116}
117
118#[derive(Clone, Debug, PartialEq)]
120pub struct LinearScale {
121 domain_min: f64,
122 domain_max: f64,
123 range_start: f64,
124 range_end: f64,
125}
126
127impl LinearScale {
128 pub fn new(domain: (f64, f64), range: (f64, f64)) -> Self {
130 Self {
131 domain_min: domain.0,
132 domain_max: domain.1,
133 range_start: range.0,
134 range_end: range.1,
135 }
136 }
137
138 pub fn scale(&self, value: f64) -> f64 {
140 let domain_span = self.domain_max - self.domain_min;
141 if domain_span == 0.0 {
142 return (self.range_start + self.range_end) / 2.0;
143 }
144 let t = (value - self.domain_min) / domain_span;
145 self.range_start + t * (self.range_end - self.range_start)
146 }
147
148 pub fn invert(&self, pixel: f64) -> f64 {
150 let range_span = self.range_end - self.range_start;
151 if range_span == 0.0 {
152 return (self.domain_min + self.domain_max) / 2.0;
153 }
154 let t = (pixel - self.range_start) / range_span;
155 self.domain_min + t * (self.domain_max - self.domain_min)
156 }
157}
158
159#[derive(Clone, Debug, PartialEq)]
161pub struct LogScale {
162 inner: LinearScale,
163}
164
165impl LogScale {
166 pub fn new(domain: (f64, f64), range: (f64, f64)) -> Option<Self> {
168 if domain.0 <= 0.0 || domain.1 <= 0.0 {
169 return None;
170 }
171 Some(Self {
172 inner: LinearScale::new((domain.0.log10(), domain.1.log10()), range),
173 })
174 }
175
176 pub fn scale(&self, value: f64) -> Option<f64> {
178 if value <= 0.0 {
179 return None;
180 }
181 Some(self.inner.scale(value.log10()))
182 }
183}
184
185#[derive(Clone, Debug, PartialEq)]
187pub struct TimeScale {
188 domain: Vec<NaiveDate>,
189 inner: LinearScale,
190}
191
192impl TimeScale {
193 pub fn new(dates: Vec<NaiveDate>, range: (f64, f64)) -> Self {
195 let max_index = dates.len().saturating_sub(1).max(1) as f64;
196 Self {
197 domain: dates,
198 inner: LinearScale::new((0.0, max_index), range),
199 }
200 }
201
202 pub fn scale(&self, date: NaiveDate) -> Option<f64> {
204 let index = self.domain.iter().position(|d| *d == date)?;
205 Some(self.inner.scale(index as f64))
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use chrono::NaiveDate;
213
214 #[test]
215 fn band_scale_maps_categories() {
216 let scale = BandScale::new(vec!["A".into(), "B".into(), "C".into()], (0.0, 300.0), 0.1);
217 assert!(scale.bandwidth() > 0.0);
218 let b = scale.scale("B").unwrap();
219 assert!((b - 136.76470588235292).abs() < 1e-9);
220 assert!(scale.scale("missing").is_none());
221 }
222
223 #[test]
224 fn band_scale_offsets_first_band_from_range_start() {
225 let scale = BandScale::new(vec!["A".into(), "B".into()], (0.0, 200.0), 0.1);
226 let center = scale.scale("A").unwrap();
227 let band_start = center - scale.bandwidth() / 2.0;
228 assert!(band_start > 0.0);
229 }
230
231 #[test]
232 fn linear_scale_interpolates() {
233 let scale = LinearScale::new((0.0, 100.0), (0.0, 200.0));
234 assert_eq!(scale.scale(50.0), 100.0);
235 assert_eq!(scale.scale(0.0), 0.0);
236 }
237
238 #[test]
239 fn log_scale_requires_positive_domain() {
240 assert!(LogScale::new((0.0, 100.0), (0.0, 100.0)).is_none());
241 let scale = LogScale::new((1.0, 1000.0), (0.0, 100.0)).unwrap();
242 assert!((scale.scale(10.0).unwrap() - 33.33333333333333).abs() < f64::EPSILON);
243 assert!(scale.scale(0.0).is_none());
244 }
245
246 #[test]
247 fn time_scale_maps_by_index() {
248 let dates = vec![
249 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
250 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
251 NaiveDate::from_ymd_opt(2024, 3, 1).unwrap(),
252 ];
253 let scale = TimeScale::new(dates.clone(), (0.0, 100.0));
254 assert_eq!(scale.scale(dates[1]).unwrap(), 50.0);
255 }
256}