Skip to main content

gpui_base/plot/scale/
point.rs

1// @reference: https://d3js.org/d3-scale/point
2
3use super::Scale;
4
5/// Point scale maps discrete domain values to continuous range positions.
6///
7/// Points are evenly distributed across the range, with the first and last points
8/// aligned to the range boundaries.
9#[derive(Clone)]
10pub struct ScalePoint<T> {
11    domain: Vec<T>,
12    range_start: f32,
13    range_tick: f32,
14}
15
16impl<T> ScalePoint<T>
17where
18    T: PartialEq,
19{
20    /// Place `domain` evenly from `range[0]` to `range[1]`; a single value sits
21    /// in the middle of the range.
22    ///
23    /// # Examples
24    ///
25    /// ```ignore
26    /// let scale = ScalePoint::new([1, 2, 3], [0., 100.]);
27    /// assert_eq!(scale.tick(&1), Some(0.));
28    /// assert_eq!(scale.tick(&2), Some(50.));
29    /// assert_eq!(scale.tick(&3), Some(100.));
30    /// ```
31    pub fn new(domain: impl IntoIterator<Item = T>, range: [f32; 2]) -> Self {
32        let domain: Vec<T> = domain.into_iter().collect();
33        let len = domain.len();
34        let range_diff = range[1] - range[0];
35        let (range_start, range_tick) = match len {
36            0 => (0., 0.),
37            1 => (range[0], range_diff),
38            _ => (range[0], range_diff / (len - 1) as f32),
39        };
40
41        Self {
42            domain,
43            range_start,
44            range_tick,
45        }
46    }
47
48    /// Returns the position of the domain value at `index`.
49    ///
50    /// Equivalent to [`Scale::tick`] on `domain[index]` for a domain of unique
51    /// values, without searching the domain. Charts whose domain is built one
52    /// entry per datum use this to project a series in linear time.
53    pub fn tick_at(&self, index: usize) -> Option<f32> {
54        let len = self.domain.len();
55        if index >= len {
56            return None;
57        }
58
59        if len == 1 {
60            Some(self.range_start + self.range_tick / 2.)
61        } else {
62            Some(self.range_start + index as f32 * self.range_tick)
63        }
64    }
65}
66
67impl<T> Scale<T> for ScalePoint<T>
68where
69    T: PartialEq,
70{
71    fn tick(&self, value: &T) -> Option<f32> {
72        let index = self.domain.iter().position(|v| v == value)?;
73        self.tick_at(index)
74    }
75
76    fn nearest_index(&self, tick: f32) -> usize {
77        if self.domain.is_empty() {
78            return 0;
79        }
80
81        if self.range_tick == 0. {
82            return 0;
83        }
84
85        let normalized_tick = tick - self.range_start;
86        let index = (normalized_tick / self.range_tick).round() as usize;
87        index.min(self.domain.len() - 1)
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn test_scale_point() {
97        let scale = ScalePoint::new(vec![1, 2, 3], [0., 100.]);
98        assert_eq!(scale.tick(&1), Some(0.));
99        assert_eq!(scale.tick(&2), Some(50.));
100        assert_eq!(scale.tick(&3), Some(100.));
101    }
102
103    #[test]
104    fn test_scale_point_range() {
105        let scale = ScalePoint::new(vec![1, 2, 3], [40., 80.]);
106        assert_eq!(scale.tick(&1), Some(40.));
107        assert_eq!(scale.tick(&2), Some(60.));
108        assert_eq!(scale.tick(&3), Some(80.));
109    }
110
111    #[test]
112    fn test_scale_point_empty() {
113        let scale = ScalePoint::new(vec![], [0., 100.]);
114        assert_eq!(scale.tick(&1), None);
115        assert_eq!(scale.tick(&2), None);
116        assert_eq!(scale.tick(&3), None);
117
118        let scale = ScalePoint::new(vec![1, 2, 3], [0., 0.]);
119        assert_eq!(scale.tick(&1), Some(0.));
120        assert_eq!(scale.tick(&2), Some(0.));
121        assert_eq!(scale.tick(&3), Some(0.));
122    }
123
124    #[test]
125    fn test_scale_point_single() {
126        let scale = ScalePoint::new(vec![1], [0., 100.]);
127        assert_eq!(scale.tick(&1), Some(50.));
128    }
129
130    #[test]
131    fn test_tick_at_matches_tick() {
132        for domain in [vec![], vec![1], vec![1, 2, 3], vec![1, 2, 3, 4, 5]] {
133            let scale = ScalePoint::new(domain.clone(), [40., 80.]);
134            for (i, value) in domain.iter().enumerate() {
135                assert_eq!(scale.tick_at(i), scale.tick(value));
136            }
137            assert_eq!(scale.tick_at(domain.len()), None);
138        }
139    }
140
141    #[test]
142    fn test_nearest_index_basic() {
143        let scale = ScalePoint::new(vec![1, 2, 3], [0., 100.]);
144
145        // Exact positions
146        assert_eq!(scale.nearest_index(0.), 0);
147        assert_eq!(scale.nearest_index(50.), 1);
148        assert_eq!(scale.nearest_index(100.), 2);
149
150        // Between positions (should round to nearest)
151        assert_eq!(scale.nearest_index(24.), 0); // closer to 0
152        assert_eq!(scale.nearest_index(25.), 1); // equidistant, rounds to 1
153        assert_eq!(scale.nearest_index(26.), 1); // closer to 50
154        assert_eq!(scale.nearest_index(74.), 1); // closer to 50
155        assert_eq!(scale.nearest_index(75.), 2); // equidistant, rounds to 2
156        assert_eq!(scale.nearest_index(76.), 2); // closer to 100
157
158        // Outside range
159        assert_eq!(scale.nearest_index(-10.), 0); // below min
160        assert_eq!(scale.nearest_index(150.), 2); // above max
161    }
162
163    #[test]
164    fn test_nearest_index_with_offset() {
165        let scale = ScalePoint::new(vec![1, 2, 3], [40., 80.]);
166
167        // Exact positions: 40, 60, 80
168        assert_eq!(scale.nearest_index(40.), 0);
169        assert_eq!(scale.nearest_index(60.), 1);
170        assert_eq!(scale.nearest_index(80.), 2);
171
172        // Between positions
173        assert_eq!(scale.nearest_index(49.), 0); // closer to 40
174        assert_eq!(scale.nearest_index(50.), 1); // equidistant, rounds to 1
175        assert_eq!(scale.nearest_index(51.), 1); // closer to 60
176        assert_eq!(scale.nearest_index(69.), 1); // closer to 60
177        assert_eq!(scale.nearest_index(70.), 2); // equidistant, rounds to 2
178        assert_eq!(scale.nearest_index(71.), 2); // closer to 80
179
180        // Outside range
181        assert_eq!(scale.nearest_index(30.), 0); // below min
182        assert_eq!(scale.nearest_index(100.), 2); // above max
183    }
184
185    #[test]
186    fn test_nearest_index_empty() {
187        let scale = ScalePoint::new(Vec::<i32>::new(), [0., 100.]);
188        assert_eq!(scale.nearest_index(0.), 0);
189        assert_eq!(scale.nearest_index(50.), 0);
190        assert_eq!(scale.nearest_index(100.), 0);
191    }
192
193    #[test]
194    fn test_nearest_index_single() {
195        let scale = ScalePoint::new(vec![1], [0., 100.]);
196        assert_eq!(scale.nearest_index(0.), 0);
197        assert_eq!(scale.nearest_index(50.), 0);
198        assert_eq!(scale.nearest_index(100.), 0);
199    }
200
201    #[test]
202    fn test_nearest_index_empty_range() {
203        let scale = ScalePoint::new(vec![1, 2, 3], [0., 0.]);
204        assert_eq!(scale.nearest_index(0.), 0);
205        assert_eq!(scale.nearest_index(50.), 0);
206        assert_eq!(scale.nearest_index(100.), 0);
207    }
208
209    #[test]
210    fn test_reversed_range() {
211        let scale = ScalePoint::new([1, 2, 3], [100., 0.]);
212        assert_eq!(scale.tick(&1), Some(100.));
213        assert_eq!(scale.tick(&3), Some(0.));
214        assert_eq!(scale.nearest_index(90.), 0);
215        assert_eq!(scale.nearest_index(10.), 2);
216    }
217}