Skip to main content

liora_components/
chart_scale.rs

1use gpui::SharedString;
2
3#[derive(Clone, Copy, Debug, PartialEq)]
4pub struct ScaleLinear {
5    domain: (f64, f64),
6    range: (f32, f32),
7}
8
9impl ScaleLinear {
10    pub fn new(domain: (f64, f64), range: (f32, f32)) -> Self {
11        Self { domain, range }
12    }
13
14    pub fn tick(&self, value: f64) -> f32 {
15        let span = self.domain.1 - self.domain.0;
16        if !value.is_finite() || span.abs() < f64::EPSILON {
17            return self.range.0;
18        }
19        let t = ((value - self.domain.0) / span) as f32;
20        self.range.0 + (self.range.1 - self.range.0) * t
21    }
22
23    pub fn ticks(&self, count: usize) -> Vec<(f64, f32)> {
24        let count = count.max(2);
25        let step = (self.domain.1 - self.domain.0) / (count - 1) as f64;
26        (0..count)
27            .map(|index| {
28                let value = self.domain.0 + step * index as f64;
29                (value, self.tick(value))
30            })
31            .collect()
32    }
33}
34
35#[derive(Clone, Debug, PartialEq)]
36pub struct ScalePoint {
37    domain: Vec<SharedString>,
38    domain_len: usize,
39    range: (f32, f32),
40}
41
42impl ScalePoint {
43    pub fn new(domain: Vec<SharedString>, range: (f32, f32)) -> Self {
44        let domain_len = domain.len();
45        Self {
46            domain,
47            domain_len,
48            range,
49        }
50    }
51
52    /// Build an index-based point scale without cloning/storing every label.
53    /// This is useful for dense native charts where label text is painted from
54    /// a separate sparse axis-label list, while data points still need stable
55    /// original-index positioning.
56    pub fn from_len(domain_len: usize, range: (f32, f32)) -> Self {
57        Self {
58            domain: Vec::new(),
59            domain_len,
60            range,
61        }
62    }
63
64    pub fn tick_index(&self, index: usize) -> Option<f32> {
65        if self.domain_len == 0 || index >= self.domain_len {
66            return None;
67        }
68        if self.domain_len == 1 {
69            return Some((self.range.0 + self.range.1) / 2.0);
70        }
71        let step = (self.range.1 - self.range.0) / (self.domain_len - 1) as f32;
72        Some(self.range.0 + step * index as f32)
73    }
74
75    pub fn tick(&self, value: &SharedString) -> Option<f32> {
76        self.domain
77            .iter()
78            .position(|label| label == value)
79            .and_then(|index| self.tick_index(index))
80    }
81}
82
83#[derive(Clone, Debug, PartialEq)]
84pub struct ScaleBand {
85    domain: Vec<SharedString>,
86    range: (f32, f32),
87    padding_inner: f32,
88    padding_outer: f32,
89}
90
91impl ScaleBand {
92    pub fn new(domain: Vec<SharedString>, range: (f32, f32)) -> Self {
93        Self {
94            domain,
95            range,
96            padding_inner: 0.2,
97            padding_outer: 0.1,
98        }
99    }
100
101    pub fn padding_inner(mut self, padding: f32) -> Self {
102        self.padding_inner = padding.clamp(0.0, 0.95);
103        self
104    }
105
106    pub fn padding_outer(mut self, padding: f32) -> Self {
107        self.padding_outer = padding.max(0.0);
108        self
109    }
110
111    pub fn step(&self) -> f32 {
112        if self.domain.is_empty() {
113            return 0.0;
114        }
115        let slots = self.domain.len() as f32 - self.padding_inner + self.padding_outer * 2.0;
116        if slots <= 0.0 {
117            0.0
118        } else {
119            (self.range.1 - self.range.0) / slots
120        }
121    }
122
123    pub fn band_width(&self) -> f32 {
124        self.step() * (1.0 - self.padding_inner)
125    }
126
127    pub fn tick_index(&self, index: usize) -> Option<f32> {
128        if self.domain.is_empty() || index >= self.domain.len() {
129            return None;
130        }
131        Some(self.range.0 + self.step() * (self.padding_outer + index as f32))
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn linear_scale_maps_domain_to_range() {
141        let scale = ScaleLinear::new((0.0, 100.0), (200.0, 0.0));
142        assert_eq!(scale.tick(0.0), 200.0);
143        assert_eq!(scale.tick(50.0), 100.0);
144        assert_eq!(scale.tick(100.0), 0.0);
145    }
146
147    #[test]
148    fn point_scale_handles_single_and_multiple_labels() {
149        let one = ScalePoint::new(vec!["A".into()], (0.0, 100.0));
150        assert_eq!(one.tick_index(0), Some(50.0));
151
152        let many = ScalePoint::new(vec!["A".into(), "B".into(), "C".into()], (0.0, 100.0));
153        assert_eq!(many.tick_index(1), Some(50.0));
154        assert_eq!(many.tick(&"C".into()), Some(100.0));
155
156        let index_only = ScalePoint::from_len(3, (0.0, 100.0));
157        assert_eq!(index_only.tick_index(2), Some(100.0));
158        assert_eq!(index_only.tick(&"C".into()), None);
159    }
160
161    #[test]
162    fn band_scale_allocates_padded_bands() {
163        let scale = ScaleBand::new(vec!["A".into(), "B".into()], (0.0, 120.0))
164            .padding_inner(0.2)
165            .padding_outer(0.1);
166        assert!(scale.band_width() > 0.0);
167        assert!(scale.tick_index(1).unwrap() > scale.tick_index(0).unwrap());
168    }
169}