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 midpoint: Option<f64>,
25}
26
27impl ScaleColorSteps {
28 pub fn new(aesthetic: Aesthetic, stops: Vec<(u8, u8, u8)>, n_bins: usize) -> Self {
30 ScaleColorSteps {
31 aesthetic,
32 stops: if stops.is_empty() {
33 vec![(50, 50, 200), (200, 50, 50)]
34 } else {
35 stops
36 },
37 n_bins: n_bins.max(1),
38 name: String::new(),
39 min: f64::INFINITY,
40 max: f64::NEG_INFINITY,
41 trained: false,
42 midpoint: None,
43 }
44 }
45
46 pub fn diverging(
49 aesthetic: Aesthetic,
50 low: (u8, u8, u8),
51 mid: (u8, u8, u8),
52 high: (u8, u8, u8),
53 midpoint: f64,
54 n_bins: usize,
55 ) -> Self {
56 let mut s = Self::new(aesthetic, vec![low, mid, high], n_bins);
57 s.midpoint = Some(midpoint);
58 s
59 }
60
61 pub fn viridis(aesthetic: Aesthetic, option: char, n_bins: usize) -> Self {
66 let stops = super::color::GeneratedPalette::Viridis {
67 option,
68 begin: 0.0,
69 end: 1.0,
70 direction: 1,
71 }
72 .colors(16)
73 .into_iter()
74 .map(|c| (c.r, c.g, c.b))
75 .collect();
76 Self::new(aesthetic, stops, n_bins)
77 }
78
79 pub fn two(aesthetic: Aesthetic, low: (u8, u8, u8), high: (u8, u8, u8), n_bins: usize) -> Self {
81 Self::new(aesthetic, vec![low, high], n_bins)
82 }
83
84 fn interp(&self, f: f64) -> (u8, u8, u8) {
85 let f = f.clamp(0.0, 1.0);
86 if self.stops.len() == 1 {
87 return self.stops[0];
88 }
89 let segs = self.stops.len() - 1;
90 let pos = f * segs as f64;
91 let i = (pos.floor() as usize).min(segs - 1);
92 let t = pos - i as f64;
93 let (r0, g0, b0) = self.stops[i];
94 let (r1, g1, b1) = self.stops[i + 1];
95 let lerp = |a: u8, b: u8| (a as f64 + (b as f64 - a as f64) * t).round() as u8;
96 (lerp(r0, r1), lerp(g0, g1), lerp(b0, b1))
97 }
98
99 fn bin_index(&self, v: f64) -> usize {
100 if !self.trained || self.max <= self.min {
101 return 0;
102 }
103 let frac = (v - self.min) / (self.max - self.min);
104 ((frac * self.n_bins as f64).floor() as isize).clamp(0, self.n_bins as isize - 1) as usize
105 }
106
107 fn bin_color(&self, i: usize) -> (u8, u8, u8) {
108 let f = (i as f64 + 0.5) / self.n_bins as f64;
110 match self.midpoint {
111 Some(mid) if self.trained && self.max > self.min => {
112 let v = self.min + f * (self.max - self.min);
113 let half = (self.max - mid).abs().max((self.min - mid).abs());
114 self.interp(0.5 + (v - mid) / (2.0 * half))
115 }
116 _ => self.interp(f),
117 }
118 }
119
120 fn bin_label(&self, i: usize) -> String {
121 if !self.trained || self.max <= self.min {
122 return format!("bin {i}");
123 }
124 let w = (self.max - self.min) / self.n_bins as f64;
125 let lo = self.min + i as f64 * w;
126 format!("[{:.2}, {:.2})", lo, lo + w)
127 }
128}
129
130impl Scale for ScaleColorSteps {
131 fn aesthetic(&self) -> Aesthetic {
132 self.aesthetic.clone()
133 }
134
135 fn train(&mut self, values: &[Value]) {
136 for v in values {
137 if let Some(f) = v.as_f64() {
138 if f.is_finite() {
139 self.min = self.min.min(f);
140 self.max = self.max.max(f);
141 self.trained = true;
142 }
143 }
144 }
145 }
146
147 fn map(&self, _value: &Value) -> f64 {
148 0.0
149 }
150
151 fn breaks(&self) -> Vec<(f64, String)> {
152 (0..self.n_bins)
153 .map(|i| ((i as f64 + 0.5) / self.n_bins as f64, self.bin_label(i)))
154 .collect()
155 }
156
157 fn name(&self) -> &str {
158 &self.name
159 }
160
161 fn set_name(&mut self, name: &str) {
162 self.name = name.to_string();
163 }
164
165 fn is_discrete(&self) -> bool {
166 true
167 }
168
169 fn map_to_color(&self, value: &Value) -> Option<(u8, u8, u8)> {
170 if let Some(f) = value.as_f64() {
171 return Some(self.bin_color(self.bin_index(f)));
172 }
173 if let Value::Str(s) = value {
175 for i in 0..self.n_bins {
176 if &self.bin_label(i) == s {
177 return Some(self.bin_color(i));
178 }
179 }
180 }
181 None
182 }
183
184 fn domain(&self) -> Option<(f64, f64)> {
185 if self.trained {
186 Some((self.min, self.max))
187 } else {
188 None
189 }
190 }
191
192 fn clone_box(&self) -> Box<dyn Scale> {
193 Box::new(self.clone())
194 }
195
196 fn reset_training(&mut self) {
197 self.min = f64::INFINITY;
198 self.max = f64::NEG_INFINITY;
199 self.trained = false;
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 fn trained(n: usize) -> ScaleColorSteps {
208 let mut s = ScaleColorSteps::two(Aesthetic::Color, (0, 0, 0), (255, 255, 255), n);
209 s.train(&[Value::Float(0.0), Value::Float(100.0)]);
210 s
211 }
212
213 #[test]
214 fn steps2_puts_mid_at_the_midpoint() {
215 let mut s = ScaleColorSteps::diverging(
216 Aesthetic::Fill,
217 (0, 0, 255),
218 (255, 255, 255),
219 (255, 0, 0),
220 0.0,
221 4,
222 );
223 s.train(&[Value::Float(-10.0), Value::Float(30.0)]);
224 let lo = s.map_to_color(&Value::Float(-5.0)).unwrap();
226 let hi = s.map_to_color(&Value::Float(25.0)).unwrap();
227 assert!(lo.2 > lo.0, "{lo:?}");
228 assert!(hi.0 > hi.2, "{hi:?}");
229 let near = s.map_to_color(&Value::Float(5.0)).unwrap();
231 assert!(near.1 > 150, "{near:?}");
232 }
233
234 #[test]
235 fn viridis_b_runs_dark_to_yellow() {
236 let mut s = ScaleColorSteps::viridis(Aesthetic::Color, 'D', 5);
237 s.train(&[Value::Float(0.0), Value::Float(1.0)]);
238 let lo = s.map_to_color(&Value::Float(0.0)).unwrap();
239 let hi = s.map_to_color(&Value::Float(1.0)).unwrap();
240 assert!(hi.0 as u32 + hi.1 as u32 > lo.0 as u32 + lo.1 as u32);
241 }
242
243 #[test]
244 fn bins_map_to_distinct_colors() {
245 let s = trained(4);
246 let low = s.map_to_color(&Value::Float(5.0)).unwrap();
247 let high = s.map_to_color(&Value::Float(95.0)).unwrap();
248 assert_ne!(low, high);
249 assert_eq!(
251 s.map_to_color(&Value::Float(-10.0)),
252 s.map_to_color(&Value::Float(0.0))
253 );
254 }
255
256 #[test]
257 fn same_bin_same_color() {
258 let s = trained(4); let a = s.map_to_color(&Value::Float(1.0)).unwrap();
260 let b = s.map_to_color(&Value::Float(24.0)).unwrap();
261 assert_eq!(a, b, "values in the same bin share a colour");
262 }
263
264 #[test]
265 fn legend_label_roundtrips_to_bin_color() {
266 let s = trained(5);
267 let breaks = s.breaks();
268 assert_eq!(breaks.len(), 5);
269 for (i, (_, label)) in breaks.iter().enumerate() {
271 let via_label = s.map_to_color(&Value::Str(label.clone())).unwrap();
272 assert_eq!(via_label, s.bin_color(i));
273 }
274 }
275}