1use crate::aes::Aesthetic;
2use crate::data::Value;
3
4use super::Scale;
5
6#[derive(Clone)]
14pub struct ScaleColorSteps {
15 aesthetic: Aesthetic,
16 stops: Vec<(u8, u8, u8)>,
17 n_bins: usize,
18 name: String,
19 min: f64,
20 max: f64,
21 trained: bool,
22}
23
24impl ScaleColorSteps {
25 pub fn new(aesthetic: Aesthetic, stops: Vec<(u8, u8, u8)>, n_bins: usize) -> Self {
27 ScaleColorSteps {
28 aesthetic,
29 stops: if stops.is_empty() {
30 vec![(50, 50, 200), (200, 50, 50)]
31 } else {
32 stops
33 },
34 n_bins: n_bins.max(1),
35 name: String::new(),
36 min: f64::INFINITY,
37 max: f64::NEG_INFINITY,
38 trained: false,
39 }
40 }
41
42 pub fn two(aesthetic: Aesthetic, low: (u8, u8, u8), high: (u8, u8, u8), n_bins: usize) -> Self {
44 Self::new(aesthetic, vec![low, high], n_bins)
45 }
46
47 fn interp(&self, f: f64) -> (u8, u8, u8) {
48 let f = f.clamp(0.0, 1.0);
49 if self.stops.len() == 1 {
50 return self.stops[0];
51 }
52 let segs = self.stops.len() - 1;
53 let pos = f * segs as f64;
54 let i = (pos.floor() as usize).min(segs - 1);
55 let t = pos - i as f64;
56 let (r0, g0, b0) = self.stops[i];
57 let (r1, g1, b1) = self.stops[i + 1];
58 let lerp = |a: u8, b: u8| (a as f64 + (b as f64 - a as f64) * t).round() as u8;
59 (lerp(r0, r1), lerp(g0, g1), lerp(b0, b1))
60 }
61
62 fn bin_index(&self, v: f64) -> usize {
63 if !self.trained || self.max <= self.min {
64 return 0;
65 }
66 let frac = (v - self.min) / (self.max - self.min);
67 ((frac * self.n_bins as f64).floor() as isize).clamp(0, self.n_bins as isize - 1) as usize
68 }
69
70 fn bin_color(&self, i: usize) -> (u8, u8, u8) {
71 self.interp((i as f64 + 0.5) / self.n_bins as f64)
73 }
74
75 fn bin_label(&self, i: usize) -> String {
76 if !self.trained || self.max <= self.min {
77 return format!("bin {i}");
78 }
79 let w = (self.max - self.min) / self.n_bins as f64;
80 let lo = self.min + i as f64 * w;
81 format!("[{:.2}, {:.2})", lo, lo + w)
82 }
83}
84
85impl Scale for ScaleColorSteps {
86 fn aesthetic(&self) -> Aesthetic {
87 self.aesthetic.clone()
88 }
89
90 fn train(&mut self, values: &[Value]) {
91 for v in values {
92 if let Some(f) = v.as_f64() {
93 if f.is_finite() {
94 self.min = self.min.min(f);
95 self.max = self.max.max(f);
96 self.trained = true;
97 }
98 }
99 }
100 }
101
102 fn map(&self, _value: &Value) -> f64 {
103 0.0
104 }
105
106 fn breaks(&self) -> Vec<(f64, String)> {
107 (0..self.n_bins)
108 .map(|i| ((i as f64 + 0.5) / self.n_bins as f64, self.bin_label(i)))
109 .collect()
110 }
111
112 fn name(&self) -> &str {
113 &self.name
114 }
115
116 fn set_name(&mut self, name: &str) {
117 self.name = name.to_string();
118 }
119
120 fn is_discrete(&self) -> bool {
121 true
122 }
123
124 fn map_to_color(&self, value: &Value) -> Option<(u8, u8, u8)> {
125 if let Some(f) = value.as_f64() {
126 return Some(self.bin_color(self.bin_index(f)));
127 }
128 if let Value::Str(s) = value {
130 for i in 0..self.n_bins {
131 if &self.bin_label(i) == s {
132 return Some(self.bin_color(i));
133 }
134 }
135 }
136 None
137 }
138
139 fn domain(&self) -> Option<(f64, f64)> {
140 if self.trained {
141 Some((self.min, self.max))
142 } else {
143 None
144 }
145 }
146
147 fn clone_box(&self) -> Box<dyn Scale> {
148 Box::new(self.clone())
149 }
150
151 fn reset_training(&mut self) {
152 self.min = f64::INFINITY;
153 self.max = f64::NEG_INFINITY;
154 self.trained = false;
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 fn trained(n: usize) -> ScaleColorSteps {
163 let mut s = ScaleColorSteps::two(Aesthetic::Color, (0, 0, 0), (255, 255, 255), n);
164 s.train(&[Value::Float(0.0), Value::Float(100.0)]);
165 s
166 }
167
168 #[test]
169 fn bins_map_to_distinct_colors() {
170 let s = trained(4);
171 let low = s.map_to_color(&Value::Float(5.0)).unwrap();
172 let high = s.map_to_color(&Value::Float(95.0)).unwrap();
173 assert_ne!(low, high);
174 assert_eq!(
176 s.map_to_color(&Value::Float(-10.0)),
177 s.map_to_color(&Value::Float(0.0))
178 );
179 }
180
181 #[test]
182 fn same_bin_same_color() {
183 let s = trained(4); let a = s.map_to_color(&Value::Float(1.0)).unwrap();
185 let b = s.map_to_color(&Value::Float(24.0)).unwrap();
186 assert_eq!(a, b, "values in the same bin share a colour");
187 }
188
189 #[test]
190 fn legend_label_roundtrips_to_bin_color() {
191 let s = trained(5);
192 let breaks = s.breaks();
193 assert_eq!(breaks.len(), 5);
194 for (i, (_, label)) in breaks.iter().enumerate() {
196 let via_label = s.map_to_color(&Value::Str(label.clone())).unwrap();
197 assert_eq!(via_label, s.bin_color(i));
198 }
199 }
200}