gpui_component/plot/scale/
band.rs

1// @reference: https://d3js.org/d3-scale/band
2
3use itertools::Itertools;
4use num_traits::Zero;
5
6use super::Scale;
7
8#[derive(Clone)]
9pub struct ScaleBand<T> {
10    domain: Vec<T>,
11    range_diff: f32,
12    avg_width: f32,
13    padding_inner: f32,
14    padding_outer: f32,
15}
16
17impl<T> ScaleBand<T> {
18    pub fn new(domain: Vec<T>, range: Vec<f32>) -> Self {
19        let len = domain.len() as f32;
20        let range_diff = range
21            .iter()
22            .minmax()
23            .into_option()
24            .map_or(0., |(min, max)| max - min);
25
26        Self {
27            domain,
28            range_diff,
29            avg_width: if len.is_zero() { 0. } else { range_diff / len },
30            padding_inner: 0.,
31            padding_outer: 0.,
32        }
33    }
34
35    /// Get the width of the band.
36    pub fn band_width(&self) -> f32 {
37        (self.avg_width * (1. - self.padding_inner)).min(30.)
38    }
39
40    /// Set the padding inner of the band.
41    pub fn padding_inner(mut self, padding_inner: f32) -> Self {
42        self.padding_inner = padding_inner;
43        self
44    }
45
46    /// Set the padding outer of the band.
47    pub fn padding_outer(mut self, padding_outer: f32) -> Self {
48        self.padding_outer = padding_outer;
49        self
50    }
51
52    /// Get the ratio of the band.
53    fn ratio(&self) -> f32 {
54        1. + self.padding_inner / (self.domain.len() - 1) as f32
55    }
56
57    /// Get the average width of the band for display.
58    fn display_avg_width(&self) -> f32 {
59        let padding_outer_width = self.avg_width * self.padding_outer;
60        (self.range_diff - padding_outer_width * 2.) / self.domain.len() as f32
61    }
62}
63
64impl<T> Scale<T> for ScaleBand<T>
65where
66    T: PartialEq,
67{
68    fn tick(&self, value: &T) -> Option<f32> {
69        let index = self.domain.iter().position(|v| v == value)?;
70        let domain_len = self.domain.len();
71
72        // When there's only one element, place it in the center.
73        if domain_len == 1 {
74            return Some((self.range_diff - self.band_width()) / 2.);
75        }
76
77        let avg_width = self.display_avg_width();
78        let padding_outer_width = self.avg_width * self.padding_outer;
79        Some(index as f32 * avg_width * self.ratio() + padding_outer_width)
80    }
81
82    fn least_index(&self, tick: f32) -> usize {
83        if self.domain.is_empty() {
84            return 0;
85        }
86
87        let domain_len = self.domain.len();
88
89        // Handle single element case
90        if domain_len == 1 {
91            return 0;
92        }
93
94        let avg_width = self.display_avg_width();
95        let padding_outer_width = self.avg_width * self.padding_outer;
96        let adjusted_tick = tick - padding_outer_width;
97        let index = (adjusted_tick / (avg_width * self.ratio())).round() as i32;
98
99        (index.max(0) as usize).min(domain_len.saturating_sub(1))
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn test_scale_band() {
109        let scale = ScaleBand::new(vec![1, 2, 3], vec![0., 90.]);
110        assert_eq!(scale.tick(&1), Some(0.));
111        assert_eq!(scale.tick(&2), Some(30.));
112        assert_eq!(scale.tick(&3), Some(60.));
113        assert_eq!(scale.band_width(), 30.);
114    }
115
116    #[test]
117    fn test_scale_band_zero() {
118        let scale = ScaleBand::new(vec![], vec![0., 90.]);
119        assert_eq!(scale.tick(&1), None);
120        assert_eq!(scale.tick(&2), None);
121        assert_eq!(scale.tick(&3), None);
122        assert_eq!(scale.band_width(), 0.);
123
124        let scale = ScaleBand::new(vec![1, 2, 3], vec![]);
125        assert_eq!(scale.tick(&1), Some(0.));
126        assert_eq!(scale.tick(&2), Some(0.));
127        assert_eq!(scale.tick(&3), Some(0.));
128        assert_eq!(scale.band_width(), 0.);
129    }
130}