Skip to main content

gpui_component/plot/scale/
band.rs

1// @reference: https://d3js.org/d3-scale/band
2
3use std::{collections::HashMap, hash::Hash};
4
5use itertools::Itertools;
6use num_traits::Zero;
7
8use super::Scale;
9
10#[derive(Clone)]
11pub struct ScaleBand<T> {
12    /// Each distinct domain value paired with its band index.
13    ///
14    /// D3 keys its band domain through an `InternMap`, so a repeated value
15    /// keeps the index of its first occurrence and the band count follows the
16    /// distinct values, not the entry count.
17    indices: HashMap<T, usize>,
18    range_diff: f32,
19    avg_width: f32,
20    padding_inner: f32,
21    padding_outer: f32,
22}
23
24impl<T> ScaleBand<T> {
25    pub fn new(domain: Vec<T>, range: Vec<f32>) -> Self
26    where
27        T: Eq + Hash,
28    {
29        let mut indices = HashMap::with_capacity(domain.len());
30        for value in domain {
31            let next = indices.len();
32            indices.entry(value).or_insert(next);
33        }
34
35        let len = indices.len() as f32;
36        let range_diff = range
37            .iter()
38            .minmax()
39            .into_option()
40            .map_or(0., |(min, max)| max - min);
41
42        Self {
43            indices,
44            range_diff,
45            avg_width: if len.is_zero() { 0. } else { range_diff / len },
46            padding_inner: 0.,
47            padding_outer: 0.,
48        }
49    }
50
51    /// Get the width of the band.
52    pub fn band_width(&self) -> f32 {
53        (self.avg_width * (1. - self.padding_inner)).min(30.)
54    }
55
56    /// Set the padding inner of the band.
57    pub fn padding_inner(mut self, padding_inner: f32) -> Self {
58        self.padding_inner = padding_inner;
59        self
60    }
61
62    /// Set the padding outer of the band.
63    pub fn padding_outer(mut self, padding_outer: f32) -> Self {
64        self.padding_outer = padding_outer;
65        self
66    }
67
68    /// The number of bands, one per distinct domain value.
69    fn len(&self) -> usize {
70        self.indices.len()
71    }
72
73    /// Get the ratio of the band.
74    fn ratio(&self) -> f32 {
75        1. + self.padding_inner / (self.len() - 1) as f32
76    }
77
78    /// Get the average width of the band for display.
79    fn display_avg_width(&self) -> f32 {
80        let padding_outer_width = self.avg_width * self.padding_outer;
81        (self.range_diff - padding_outer_width * 2.) / self.len() as f32
82    }
83}
84
85impl<T> Scale<T> for ScaleBand<T>
86where
87    T: Eq + Hash,
88{
89    fn tick(&self, value: &T) -> Option<f32> {
90        let index = *self.indices.get(value)?;
91        let domain_len = self.len();
92
93        // When there's only one element, place it in the center.
94        if domain_len == 1 {
95            return Some((self.range_diff - self.band_width()) / 2.);
96        }
97
98        let avg_width = self.display_avg_width();
99        let padding_outer_width = self.avg_width * self.padding_outer;
100        Some(index as f32 * avg_width * self.ratio() + padding_outer_width)
101    }
102
103    fn least_index(&self, tick: f32) -> usize {
104        let domain_len = self.len();
105        if domain_len == 0 {
106            return 0;
107        }
108
109        // Handle single element case
110        if domain_len == 1 {
111            return 0;
112        }
113
114        let avg_width = self.display_avg_width();
115        let padding_outer_width = self.avg_width * self.padding_outer;
116        let adjusted_tick = tick - padding_outer_width;
117        let index = (adjusted_tick / (avg_width * self.ratio())).round() as i32;
118
119        (index.max(0) as usize).min(domain_len.saturating_sub(1))
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_scale_band() {
129        let scale = ScaleBand::new(vec![1, 2, 3], vec![0., 90.]);
130        assert_eq!(scale.tick(&1), Some(0.));
131        assert_eq!(scale.tick(&2), Some(30.));
132        assert_eq!(scale.tick(&3), Some(60.));
133        assert_eq!(scale.band_width(), 30.);
134    }
135
136    #[test]
137    fn test_scale_band_dedup() {
138        // Simulates grouped bar chart: 2 series × 3 categories = 6 entries, 3 unique.
139        let scale = ScaleBand::new(vec![1, 2, 3, 1, 2, 3], vec![0., 90.]);
140        assert_eq!(scale.len(), 3);
141        assert_eq!(scale.tick(&1), Some(0.));
142        assert_eq!(scale.tick(&2), Some(30.));
143        assert_eq!(scale.tick(&3), Some(60.));
144        assert_eq!(scale.band_width(), 30.);
145    }
146
147    #[test]
148    fn test_scale_band_zero() {
149        let scale = ScaleBand::new(vec![], vec![0., 90.]);
150        assert_eq!(scale.tick(&1), None);
151        assert_eq!(scale.tick(&2), None);
152        assert_eq!(scale.tick(&3), None);
153        assert_eq!(scale.band_width(), 0.);
154
155        let scale = ScaleBand::new(vec![1, 2, 3], vec![]);
156        assert_eq!(scale.tick(&1), Some(0.));
157        assert_eq!(scale.tick(&2), Some(0.));
158        assert_eq!(scale.tick(&3), Some(0.));
159        assert_eq!(scale.band_width(), 0.);
160    }
161}