Skip to main content

gpui_component/plot/scale/
point.rs

1// @reference: https://d3js.org/d3-scale/point
2
3use itertools::Itertools;
4
5use super::Scale;
6
7/// Point scale maps discrete domain values to continuous range positions.
8///
9/// Points are evenly distributed across the range, with the first and last points
10/// aligned to the range boundaries.
11#[derive(Clone)]
12pub struct ScalePoint<T> {
13    domain: Vec<T>,
14    range_start: f32,
15    range_tick: f32,
16}
17
18impl<T> ScalePoint<T>
19where
20    T: PartialEq,
21{
22    /// Creates a new point scale with the given domain and range.
23    ///
24    /// # Examples
25    ///
26    /// ```ignore
27    /// let scale = ScalePoint::new(vec![1, 2, 3], vec![0., 100.]);
28    /// assert_eq!(scale.tick(&1), Some(0.));
29    /// assert_eq!(scale.tick(&2), Some(50.));
30    /// assert_eq!(scale.tick(&3), Some(100.));
31    /// ```
32    pub fn new(domain: Vec<T>, range: Vec<f32>) -> Self {
33        let len = domain.len();
34        let (range_start, range_tick) = if len == 0 {
35            (0., 0.)
36        } else {
37            let (min, max) = range
38                .iter()
39                .minmax()
40                .into_option()
41                .map_or((0., 0.), |(min, max)| (*min, *max));
42
43            let range_diff = max - min;
44
45            if len == 1 {
46                (min, range_diff)
47            } else {
48                (min, range_diff / (len - 1) as f32)
49            }
50        };
51
52        Self {
53            domain,
54            range_start,
55            range_tick,
56        }
57    }
58}
59
60impl<T> Scale<T> for ScalePoint<T>
61where
62    T: PartialEq,
63{
64    fn tick(&self, value: &T) -> Option<f32> {
65        let index = self.domain.iter().position(|v| v == value)?;
66
67        if self.domain.len() == 1 {
68            Some(self.range_start + self.range_tick / 2.)
69        } else {
70            Some(self.range_start + index as f32 * self.range_tick)
71        }
72    }
73
74    fn least_index(&self, tick: f32) -> usize {
75        if self.domain.is_empty() {
76            return 0;
77        }
78
79        if self.range_tick == 0. {
80            return 0;
81        }
82
83        let normalized_tick = tick - self.range_start;
84        let index = (normalized_tick / self.range_tick).round() as usize;
85        index.min(self.domain.len() - 1)
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn test_scale_point() {
95        let scale = ScalePoint::new(vec![1, 2, 3], vec![0., 100.]);
96        assert_eq!(scale.tick(&1), Some(0.));
97        assert_eq!(scale.tick(&2), Some(50.));
98        assert_eq!(scale.tick(&3), Some(100.));
99    }
100
101    #[test]
102    fn test_scale_point_range() {
103        let scale = ScalePoint::new(vec![1, 2, 3], vec![40., 80.]);
104        assert_eq!(scale.tick(&1), Some(40.));
105        assert_eq!(scale.tick(&2), Some(60.));
106        assert_eq!(scale.tick(&3), Some(80.));
107    }
108
109    #[test]
110    fn test_scale_point_empty() {
111        let scale = ScalePoint::new(vec![], vec![0., 100.]);
112        assert_eq!(scale.tick(&1), None);
113        assert_eq!(scale.tick(&2), None);
114        assert_eq!(scale.tick(&3), None);
115
116        let scale = ScalePoint::new(vec![1, 2, 3], vec![]);
117        assert_eq!(scale.tick(&1), Some(0.));
118        assert_eq!(scale.tick(&2), Some(0.));
119        assert_eq!(scale.tick(&3), Some(0.));
120    }
121
122    #[test]
123    fn test_scale_point_single() {
124        let scale = ScalePoint::new(vec![1], vec![0., 100.]);
125        assert_eq!(scale.tick(&1), Some(50.));
126    }
127
128    #[test]
129    fn test_least_index_basic() {
130        let scale = ScalePoint::new(vec![1, 2, 3], vec![0., 100.]);
131
132        // Exact positions
133        assert_eq!(scale.least_index(0.), 0);
134        assert_eq!(scale.least_index(50.), 1);
135        assert_eq!(scale.least_index(100.), 2);
136
137        // Between positions (should round to nearest)
138        assert_eq!(scale.least_index(24.), 0); // closer to 0
139        assert_eq!(scale.least_index(25.), 1); // equidistant, rounds to 1
140        assert_eq!(scale.least_index(26.), 1); // closer to 50
141        assert_eq!(scale.least_index(74.), 1); // closer to 50
142        assert_eq!(scale.least_index(75.), 2); // equidistant, rounds to 2
143        assert_eq!(scale.least_index(76.), 2); // closer to 100
144
145        // Outside range
146        assert_eq!(scale.least_index(-10.), 0); // below min
147        assert_eq!(scale.least_index(150.), 2); // above max
148    }
149
150    #[test]
151    fn test_least_index_with_offset() {
152        let scale = ScalePoint::new(vec![1, 2, 3], vec![40., 80.]);
153
154        // Exact positions: 40, 60, 80
155        assert_eq!(scale.least_index(40.), 0);
156        assert_eq!(scale.least_index(60.), 1);
157        assert_eq!(scale.least_index(80.), 2);
158
159        // Between positions
160        assert_eq!(scale.least_index(49.), 0); // closer to 40
161        assert_eq!(scale.least_index(50.), 1); // equidistant, rounds to 1
162        assert_eq!(scale.least_index(51.), 1); // closer to 60
163        assert_eq!(scale.least_index(69.), 1); // closer to 60
164        assert_eq!(scale.least_index(70.), 2); // equidistant, rounds to 2
165        assert_eq!(scale.least_index(71.), 2); // closer to 80
166
167        // Outside range
168        assert_eq!(scale.least_index(30.), 0); // below min
169        assert_eq!(scale.least_index(100.), 2); // above max
170    }
171
172    #[test]
173    fn test_least_index_empty() {
174        let scale = ScalePoint::new(Vec::<i32>::new(), vec![0., 100.]);
175        assert_eq!(scale.least_index(0.), 0);
176        assert_eq!(scale.least_index(50.), 0);
177        assert_eq!(scale.least_index(100.), 0);
178    }
179
180    #[test]
181    fn test_least_index_single() {
182        let scale = ScalePoint::new(vec![1], vec![0., 100.]);
183        assert_eq!(scale.least_index(0.), 0);
184        assert_eq!(scale.least_index(50.), 0);
185        assert_eq!(scale.least_index(100.), 0);
186    }
187
188    #[test]
189    fn test_least_index_empty_range() {
190        let scale = ScalePoint::new(vec![1, 2, 3], vec![]);
191        assert_eq!(scale.least_index(0.), 0);
192        assert_eq!(scale.least_index(50.), 0);
193        assert_eq!(scale.least_index(100.), 0);
194    }
195}