1use std::collections::HashSet;
13
14use crate::aes::Aesthetic;
15use crate::data::Value;
16use crate::format::format_value;
17
18use super::Scale;
19
20fn signature(values: &[Value]) -> u64 {
24 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
25 let mut mix = |x: u64| {
26 h ^= x;
27 h = h.wrapping_mul(0x0000_0100_0000_01b3);
28 };
29 mix(values.len() as u64);
30 for v in values {
31 match v {
32 Value::Na => mix(1),
33 _ => match v.as_f64() {
34 Some(f) => {
35 mix(2);
36 mix(f.to_bits());
37 }
38 None => mix(3),
39 },
40 }
41 }
42 h
43}
44
45#[derive(Clone, Debug)]
49pub struct ScaleFillBinned {
50 aesthetic: Aesthetic,
51 breaks: Vec<f64>,
53 colors: Vec<(u8, u8, u8)>,
54 counts: Vec<usize>,
56 na_count: usize,
58 seen: HashSet<u64>,
70 na_color: (u8, u8, u8),
73 na_label: String,
74 name: String,
75 corrected: bool,
76}
77
78fn sorted_edges(breaks: Vec<f64>) -> (Vec<f64>, bool) {
82 let mut v: Vec<f64> = breaks.into_iter().filter(|f| f.is_finite()).collect();
83 v.sort_by(f64::total_cmp);
84 v.dedup();
85 if v.len() >= 2 {
86 (v, false)
87 } else {
88 (vec![0.0, 1.0], true)
89 }
90}
91
92fn lerp_color(low: (u8, u8, u8), high: (u8, u8, u8), t: f64) -> (u8, u8, u8) {
94 let t = t.clamp(0.0, 1.0);
95 let lerp = |a: u8, b: u8| (a as f64 + (b as f64 - a as f64) * t).round() as u8;
96 (
97 lerp(low.0, high.0),
98 lerp(low.1, high.1),
99 lerp(low.2, high.2),
100 )
101}
102
103fn default_colors(n: usize, low: (u8, u8, u8), high: (u8, u8, u8)) -> Vec<(u8, u8, u8)> {
106 (0..n)
107 .map(|i| {
108 let t = if n <= 1 {
109 0.5
110 } else {
111 i as f64 / (n - 1) as f64
112 };
113 lerp_color(low, high, t)
114 })
115 .collect()
116}
117
118const DEFAULT_LOW: (u8, u8, u8) = (50, 50, 200);
120const DEFAULT_HIGH: (u8, u8, u8) = (200, 50, 50);
121
122impl ScaleFillBinned {
123 pub fn new(aesthetic: Aesthetic, breaks: Vec<f64>, colors: Vec<(u8, u8, u8)>) -> Self {
128 let (breaks, edges_corrected) = sorted_edges(breaks);
129 let n = breaks.len() - 1;
130 let (colors, colors_corrected) = if colors.len() == n {
131 (colors, false)
132 } else {
133 (default_colors(n, DEFAULT_LOW, DEFAULT_HIGH), true)
134 };
135 ScaleFillBinned {
136 aesthetic,
137 breaks,
138 colors,
139 counts: vec![0; n],
140 na_count: 0,
141 seen: HashSet::new(),
142 na_color: (127, 127, 127),
143 na_label: "no data".to_string(),
144 name: String::new(),
145 corrected: edges_corrected || colors_corrected,
146 }
147 }
148
149 pub fn two(
152 aesthetic: Aesthetic,
153 breaks: Vec<f64>,
154 low: (u8, u8, u8),
155 high: (u8, u8, u8),
156 ) -> Self {
157 let (breaks, corrected) = sorted_edges(breaks);
158 let n = breaks.len() - 1;
159 ScaleFillBinned {
160 aesthetic,
161 colors: default_colors(n, low, high),
162 counts: vec![0; n],
163 breaks,
164 na_count: 0,
165 seen: HashSet::new(),
166 na_color: (127, 127, 127),
167 na_label: "no data".to_string(),
168 name: String::new(),
169 corrected,
170 }
171 }
172
173 pub fn na_color(mut self, color: (u8, u8, u8)) -> Self {
176 self.na_color = color;
177 self
178 }
179
180 pub fn na_label(mut self, label: &str) -> Self {
182 self.na_label = label.to_string();
183 self
184 }
185
186 fn bin_index(&self, v: f64) -> usize {
191 let n = self.colors.len();
192 let v = v.clamp(self.breaks[0], self.breaks[n]);
193 for (i, w) in self.breaks[1..n].iter().enumerate() {
194 if v < *w {
195 return i;
196 }
197 }
198 n - 1
199 }
200
201 fn class_label(&self, i: usize) -> String {
203 format!(
204 "{}–{} ({})",
205 format_value(&Value::Float(self.breaks[i])),
206 format_value(&Value::Float(self.breaks[i + 1])),
207 self.counts[i]
208 )
209 }
210
211 fn na_label_with_count(&self) -> String {
213 format!("{} ({})", self.na_label, self.na_count)
214 }
215}
216
217impl Scale for ScaleFillBinned {
218 fn aesthetic(&self) -> Aesthetic {
219 self.aesthetic.clone()
220 }
221
222 fn train(&mut self, values: &[Value]) {
223 if !self.seen.insert(signature(values)) {
227 return;
228 }
229 for v in values {
230 match v {
231 Value::Na => self.na_count += 1,
232 _ => {
233 if let Some(f) = v.as_f64() {
234 if f.is_finite() {
235 let i = self.bin_index(f);
236 self.counts[i] += 1;
237 }
238 }
239 }
240 }
241 }
242 }
243
244 fn map(&self, _value: &Value) -> f64 {
247 0.0
248 }
249
250 fn breaks(&self) -> Vec<(f64, String)> {
251 let n = self.colors.len();
252 let (lo0, hi_n) = (self.breaks[0], self.breaks[n]);
253 let span = (hi_n - lo0).max(f64::EPSILON);
254 let mut out: Vec<(f64, String)> = (0..n)
255 .map(|i| {
256 let center = (self.breaks[i] + self.breaks[i + 1]) / 2.0;
257 ((center - lo0) / span, self.class_label(i))
258 })
259 .collect();
260 if self.na_count > 0 {
261 out.push((1.0, self.na_label_with_count()));
262 }
263 out
264 }
265
266 fn name(&self) -> &str {
267 &self.name
268 }
269
270 fn set_name(&mut self, name: &str) {
271 self.name = name.to_string();
272 }
273
274 fn is_discrete(&self) -> bool {
275 true
276 }
277
278 fn map_to_color(&self, value: &Value) -> Option<(u8, u8, u8)> {
279 if matches!(value, Value::Na) {
280 return Some(self.na_color);
281 }
282 if let Some(f) = value.as_f64() {
283 if f.is_finite() {
284 return Some(self.colors[self.bin_index(f)]);
285 }
286 }
287 if let Value::Str(s) = value {
288 if s == &self.na_label_with_count() {
289 return Some(self.na_color);
290 }
291 for i in 0..self.colors.len() {
292 if self.class_label(i) == *s {
293 return Some(self.colors[i]);
294 }
295 }
296 }
297 None
298 }
299
300 fn domain(&self) -> Option<(f64, f64)> {
301 let n = self.colors.len();
302 Some((self.breaks[0], self.breaks[n]))
303 }
304
305 fn validate(&self, warnings: &mut Vec<String>) {
306 if self.corrected {
307 warnings.push(
308 "scale_fill_binned: breaks need >= 2 increasing finite edges, with one colour \
309 per bin — falling back to a default range/palette"
310 .to_string(),
311 );
312 }
313 }
314
315 fn clone_box(&self) -> Box<dyn Scale> {
316 Box::new(self.clone())
317 }
318
319 fn reset_training(&mut self) {
320 self.counts.iter_mut().for_each(|c| *c = 0);
321 self.na_count = 0;
322 self.seen.clear();
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 fn scale() -> ScaleFillBinned {
331 ScaleFillBinned::new(
332 Aesthetic::Fill,
333 vec![0.0, 10.0, 50.0, 100.0],
334 vec![(0, 0, 255), (0, 255, 0), (255, 0, 0)],
335 )
336 }
337
338 #[test]
339 fn bins_are_half_open_except_the_last() {
340 let s = scale();
341 assert_eq!(s.bin_index(0.0), 0);
342 assert_eq!(s.bin_index(9.999), 0);
343 assert_eq!(s.bin_index(10.0), 1); assert_eq!(s.bin_index(49.999), 1);
345 assert_eq!(s.bin_index(50.0), 2);
346 assert_eq!(s.bin_index(100.0), 2); assert_eq!(
348 s.bin_index(1000.0),
349 2,
350 "out-of-range clamps to the last bin"
351 );
352 assert_eq!(
353 s.bin_index(-50.0),
354 0,
355 "out-of-range clamps to the first bin"
356 );
357 }
358
359 #[test]
360 fn train_tallies_counts_and_na_separately() {
361 let mut s = scale();
362 s.train(&[
363 Value::Float(1.0),
364 Value::Float(5.0),
365 Value::Float(20.0),
366 Value::Float(99.0),
367 Value::Float(100.0),
368 Value::Na,
369 Value::Na,
370 ]);
371 assert_eq!(s.counts, vec![2, 1, 2]);
372 assert_eq!(s.na_count, 2);
373 }
374
375 #[test]
381 fn training_the_same_batch_twice_does_not_double_count() {
382 let batch = [Value::Float(1.0), Value::Float(20.0), Value::Na];
383 let mut s = scale();
384 s.train(&batch);
385 s.train(&batch); assert_eq!(s.counts, vec![1, 1, 0]);
387 assert_eq!(s.na_count, 1);
388 s.train(&[Value::Float(60.0)]);
390 assert_eq!(s.counts, vec![1, 1, 1]);
391 }
392
393 #[test]
394 fn breaks_label_each_class_with_its_count_and_na_with_its_own() {
395 let mut s = scale();
396 s.train(&[Value::Float(1.0), Value::Float(20.0), Value::Na]);
397 let breaks = s.breaks();
398 let labels: Vec<&str> = breaks.iter().map(|(_, l)| l.as_str()).collect();
399 assert_eq!(
400 labels,
401 vec!["0–10 (1)", "10–50 (1)", "50–100 (0)", "no data (1)"]
402 );
403 }
404
405 #[test]
406 fn no_na_key_when_nothing_is_missing() {
407 let mut s = scale();
408 s.train(&[Value::Float(1.0)]);
409 assert_eq!(s.breaks().len(), 3, "no trailing no-data entry");
410 }
411
412 #[test]
413 fn map_to_color_matches_the_bin_and_roundtrips_legend_labels() {
414 let mut s = scale();
415 s.train(&[Value::Float(1.0), Value::Float(20.0), Value::Na]);
416 assert_eq!(s.map_to_color(&Value::Float(5.0)), Some((0, 0, 255)));
417 assert_eq!(s.map_to_color(&Value::Float(20.0)), Some((0, 255, 0)));
418 assert_eq!(s.map_to_color(&Value::Float(75.0)), Some((255, 0, 0)));
419 assert_eq!(s.map_to_color(&Value::Na), Some((127, 127, 127)));
420 for (_, label) in s.breaks() {
421 let via_label = s.map_to_color(&Value::Str(label.clone()));
422 assert!(via_label.is_some(), "{label} should round-trip to a colour");
423 }
424 }
425
426 #[test]
427 fn na_color_and_label_are_configurable() {
428 let mut s = scale().na_color((10, 20, 30)).na_label("missing");
429 s.train(&[Value::Na]);
430 assert_eq!(s.map_to_color(&Value::Na), Some((10, 20, 30)));
431 assert!(s.breaks().iter().any(|(_, l)| l == "missing (1)"));
432 assert_eq!(
433 s.map_to_color(&Value::Str("missing (1)".to_string())),
434 Some((10, 20, 30))
435 );
436 }
437
438 #[test]
439 fn two_interpolates_colors_across_bins() {
440 let s = ScaleFillBinned::two(
441 Aesthetic::Fill,
442 vec![0.0, 25.0, 50.0, 75.0, 100.0],
443 (0, 0, 0),
444 (255, 255, 255),
445 );
446 assert_eq!(s.colors[0], (0, 0, 0));
447 assert_eq!(s.colors[3], (255, 255, 255));
448 assert!(s.colors[1].0 < s.colors[2].0);
449 }
450
451 #[test]
452 fn mismatched_colors_fall_back_and_warn() {
453 let s = ScaleFillBinned::new(
454 Aesthetic::Fill,
455 vec![0.0, 10.0, 20.0],
456 vec![(0, 0, 0)], );
458 assert_eq!(s.colors.len(), 2);
459 let mut warnings = Vec::new();
460 s.validate(&mut warnings);
461 assert_eq!(warnings.len(), 1);
462 }
463
464 #[test]
465 fn too_few_breaks_falls_back_to_a_single_bin_rather_than_panicking() {
466 let s = ScaleFillBinned::new(Aesthetic::Fill, vec![5.0], vec![]);
467 assert_eq!(s.breaks.len(), 2);
468 assert_eq!(s.colors.len(), 1);
469 let mut warnings = Vec::new();
470 s.validate(&mut warnings);
471 assert_eq!(warnings.len(), 1);
472 }
473
474 #[test]
475 fn domain_spans_the_outer_edges() {
476 let s = scale();
477 assert_eq!(s.domain(), Some((0.0, 100.0)));
478 }
479
480 #[test]
481 fn reset_training_clears_counts_but_keeps_breaks_and_colors() {
482 let mut s = scale();
483 s.train(&[Value::Float(1.0), Value::Na]);
484 s.reset_training();
485 assert_eq!(s.counts, vec![0, 0, 0]);
486 assert_eq!(s.na_count, 0);
487 assert_eq!(s.breaks, vec![0.0, 10.0, 50.0, 100.0]);
488 }
489}