liora_components/
chart_scale.rs1use 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 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}