lc_render/scale/
band.rs

1use crate::math::linear::range;
2use crate::{Scale, ScaleKind};
3use itertools::Itertools;
4use std::collections::HashMap;
5
6const DEFAULT_PADDING: f32 = 0.1_f32;
7const DEFAULT_ALIGN: f32 = 0.5_f32;
8const DEFAULT_STEP: f32 = 1_f32;
9const DEFAULT_BANDWIDTH: f32 = 1_f32;
10
11/// BandScale represents axis scale with categories.
12#[derive(Clone)]
13pub struct BandScale {
14    /// Scale categories.
15    domain: Vec<String>,
16
17    /// Start of the scale range.
18    range_start: i32,
19
20    /// End of the scale range.
21    range_end: i32,
22
23    /// Step value between categories.
24    step: f32,
25
26    /// Bandwidth of each category.
27    bandwidth: f32,
28
29    /// Inner padding of each category.
30    padding_inner: f32,
31
32    /// Outer padding of each category.
33    padding_outer: f32,
34
35    /// Scale align value.
36    align: f32,
37
38    /// Index of each category on a scale.
39    index: HashMap<String, usize>,
40
41    /// Offsets for each category on a scale.
42    offsets: Vec<f32>,
43
44    /// Does this scale needs an offset from the start and end of an axis.
45    /// This is usually need for an area or line views.
46    no_boundaries_offset: bool,
47}
48
49impl BandScale {
50    /// Create a new BandScale.
51    pub fn new(domain: Vec<String>, range_start: i32, range_end: i32) -> Self {
52        let mut band = BandScale {
53            domain: domain.iter().unique().map(|s| s.to_string()).collect(),
54            range_start,
55            range_end,
56            step: DEFAULT_STEP,
57            bandwidth: DEFAULT_BANDWIDTH,
58            padding_inner: DEFAULT_PADDING,
59            padding_outer: DEFAULT_PADDING,
60            align: DEFAULT_ALIGN,
61            index: HashMap::new(),
62            offsets: Vec::new(),
63            no_boundaries_offset: false,
64        };
65        band.rescale();
66        band
67    }
68
69    /// Get scale range start.
70    pub fn range_start(&self) -> i32 {
71        self.range_start
72    }
73
74    /// Get scale range end.
75    pub fn range_end(&self) -> i32 {
76        self.range_end
77    }
78
79    /// Set scale inner padding.
80    pub fn set_inner_padding(mut self, padding: f32) -> Self {
81        self.padding_inner = padding;
82        self.rescale();
83        self
84    }
85
86    /// Set scale inner padding.
87    pub fn set_outer_padding(mut self, padding: f32) -> Self {
88        self.padding_outer = padding;
89        self.rescale();
90        self
91    }
92
93    /// Change scale no boundaries offset parameter.
94    pub fn set_no_boundaries_offset(mut self, no_boundaries_offset: bool) -> Self {
95        self.no_boundaries_offset = no_boundaries_offset;
96        self
97    }
98
99    // Calculate the step, bandwidth, offsets and index for the scale.
100    fn rescale(&mut self) {
101        // We need an additional category after the axis in case no_boundaries_offset is set to true.
102        let (domain_len, offsets_count) = if self.no_boundaries_offset {
103            (self.domain.len() - 1, self.domain.len())
104        } else {
105            (self.domain.len(), self.domain.len() - 1)
106        };
107
108        let padded_domain_len = domain_len as f32 - self.padding_inner;
109        let mut start = self.range_start as f32;
110        let mut end = self.range_end as f32;
111        let reverse = end < start;
112        if reverse {
113            std::mem::swap(&mut start, &mut end)
114        }
115
116        // Calculate scale step.
117        let computed_step = padded_domain_len + self.padding_outer * 2_f32;
118        let mut step_denominator = 1_f32;
119        if computed_step > 1_f32 {
120            step_denominator = computed_step;
121        }
122        let range = range(start, end);
123        self.step = range / step_denominator;
124
125        // Calculate start and bandwidth with the updated step value.
126        start += (range - self.step * padded_domain_len) * self.align;
127        self.bandwidth = self.step * (1_f32 - self.padding_inner);
128
129        // Calculate offset value for each category.
130        self.offsets.clear();
131        for i in 0..=offsets_count {
132            self.offsets.push(start + self.step * i as f32);
133        }
134        if reverse {
135            self.offsets.reverse();
136        }
137
138        // Add categories to the index.
139        self.index.clear();
140        let mut processed_domains = Vec::new();
141        for domain in self.domain.iter() {
142            if !self.index.contains_key(domain) {
143                self.index.insert(domain.clone(), processed_domains.len());
144                processed_domains.push(domain.clone());
145            }
146        }
147
148        // Update the domain with the new domain that does not contains duplicates.
149        self.domain.clear();
150        self.domain = processed_domains;
151    }
152}
153
154impl Scale<String> for BandScale {
155    fn scale(&self, domain: &String) -> f32 {
156        return match self.index.get(domain) {
157            Some(offset_idx) => self.offsets[*offset_idx],
158            None => 0_f32,
159        };
160    }
161
162    fn ticks(&self) -> Vec<String> {
163        self.domain.clone()
164    }
165
166    fn kind(&self) -> ScaleKind {
167        ScaleKind::Band
168    }
169
170    fn bandwidth(&self) -> f32 {
171        self.bandwidth
172    }
173
174    fn is_range_reversed(&self) -> bool {
175        self.range_start > self.range_end
176    }
177
178    fn tick_offset(&self) -> f32 {
179        if self.no_boundaries_offset {
180            return 0_f32;
181        }
182
183        // Views with boundaries offset will have a tick in the middle of a category.
184        self.bandwidth() / 2_f32
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn band_scale_with_boundaries_offset() {
194        let band_scale = BandScale::new(
195            vec![
196                "A".to_string(),
197                "B".to_string(),
198                "C".to_string(),
199                "C".to_string(),
200                "D".to_string(),
201                "A".to_string(),
202            ],
203            10,
204            200,
205        );
206
207        assert_eq!(band_scale.range_start(), 10);
208        assert_eq!(band_scale.range_end(), 200);
209        assert_eq!(
210            *band_scale.ticks(),
211            vec![
212                "A".to_string(),
213                "B".to_string(),
214                "C".to_string(),
215                "D".to_string()
216            ]
217        );
218        assert!((band_scale.scale(&"D".to_string()) - 153.65854).abs() < f32::EPSILON);
219        assert_eq!(band_scale.kind(), ScaleKind::Band);
220        assert!((band_scale.bandwidth() - 41.707317_f32).abs() < f32::EPSILON);
221        assert!(!band_scale.is_range_reversed());
222        assert!((band_scale.tick_offset() - 20.853659_f32).abs() < f32::EPSILON);
223    }
224
225    #[test]
226    fn band_scale_without_boundaries_offset() {
227        let band_scale = BandScale::new(
228            vec![
229                "a1".to_string(),
230                "a1".to_string(),
231                "a2".to_string(),
232                "a3".to_string(),
233                "a4".to_string(),
234                "a2".to_string(),
235            ],
236            0,
237            100,
238        )
239        .set_no_boundaries_offset(true)
240        .set_inner_padding(0_f32)
241        .set_outer_padding(0_f32);
242
243        assert_eq!(band_scale.range_start(), 0);
244        assert_eq!(band_scale.range_end(), 100);
245        assert_eq!(
246            *band_scale.ticks(),
247            vec![
248                "a1".to_string(),
249                "a2".to_string(),
250                "a3".to_string(),
251                "a4".to_string()
252            ]
253        );
254        assert!((band_scale.scale(&"a2".to_string()) - 33.333332_f32).abs() < f32::EPSILON);
255        assert_eq!(band_scale.kind(), ScaleKind::Band);
256        assert!((band_scale.bandwidth() - 33.333332_f32).abs() < f32::EPSILON);
257        assert!(!band_scale.is_range_reversed());
258        assert!((band_scale.tick_offset() - 0_f32).abs() < f32::EPSILON);
259    }
260}