1use std::collections::HashSet;
13
14use crate::aes::Aesthetic;
15use crate::data::Value;
16use crate::format::format_value;
17use crate::render::backend::FillPattern;
18
19use super::Scale;
20
21fn signature(values: &[Value]) -> u64 {
25 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
26 let mut mix = |x: u64| {
27 h ^= x;
28 h = h.wrapping_mul(0x0000_0100_0000_01b3);
29 };
30 mix(values.len() as u64);
31 for v in values {
32 match v {
33 Value::Na => mix(1),
34 _ => match v.as_f64() {
35 Some(f) => {
36 mix(2);
37 mix(f.to_bits());
38 }
39 None => mix(3),
40 },
41 }
42 }
43 h
44}
45
46#[derive(Clone, Debug)]
50pub struct ScaleFillBinned {
51 aesthetic: Aesthetic,
52 breaks: Vec<f64>,
54 colors: Vec<(u8, u8, u8)>,
55 counts: Vec<usize>,
57 na_count: usize,
59 seen: HashSet<u64>,
71 na_color: (u8, u8, u8),
75 na_pattern: Option<FillPattern>,
82 na_label: String,
83 name: String,
84 corrected: bool,
85}
86
87fn sorted_edges(breaks: Vec<f64>) -> (Vec<f64>, bool) {
99 let mut v: Vec<f64> = breaks.into_iter().filter(|f| f.is_finite()).collect();
100 v.sort_by(f64::total_cmp);
101 if v.len() >= 2 {
102 (v, false)
103 } else {
104 (vec![0.0, 1.0], true)
105 }
106}
107
108fn lerp_color(low: (u8, u8, u8), high: (u8, u8, u8), t: f64) -> (u8, u8, u8) {
110 let t = t.clamp(0.0, 1.0);
111 let lerp = |a: u8, b: u8| (a as f64 + (b as f64 - a as f64) * t).round() as u8;
112 (
113 lerp(low.0, high.0),
114 lerp(low.1, high.1),
115 lerp(low.2, high.2),
116 )
117}
118
119fn default_colors(n: usize, low: (u8, u8, u8), high: (u8, u8, u8)) -> Vec<(u8, u8, u8)> {
122 (0..n)
123 .map(|i| {
124 let t = if n <= 1 {
125 0.5
126 } else {
127 i as f64 / (n - 1) as f64
128 };
129 lerp_color(low, high, t)
130 })
131 .collect()
132}
133
134const DEFAULT_LOW: (u8, u8, u8) = (50, 50, 200);
136const DEFAULT_HIGH: (u8, u8, u8) = (200, 50, 50);
137
138impl ScaleFillBinned {
139 pub fn new(aesthetic: Aesthetic, breaks: Vec<f64>, colors: Vec<(u8, u8, u8)>) -> Self {
144 let (breaks, edges_corrected) = sorted_edges(breaks);
145 let n = breaks.len() - 1;
146 let (colors, colors_corrected) = if colors.len() == n {
147 (colors, false)
148 } else {
149 (default_colors(n, DEFAULT_LOW, DEFAULT_HIGH), true)
150 };
151 ScaleFillBinned {
152 aesthetic,
153 breaks,
154 colors,
155 counts: vec![0; n],
156 na_count: 0,
157 seen: HashSet::new(),
158 na_color: (127, 127, 127),
159 na_pattern: None,
160 na_label: "no data".to_string(),
161 name: String::new(),
162 corrected: edges_corrected || colors_corrected,
163 }
164 }
165
166 pub fn two(
169 aesthetic: Aesthetic,
170 breaks: Vec<f64>,
171 low: (u8, u8, u8),
172 high: (u8, u8, u8),
173 ) -> Self {
174 let (breaks, corrected) = sorted_edges(breaks);
175 let n = breaks.len() - 1;
176 ScaleFillBinned {
177 aesthetic,
178 colors: default_colors(n, low, high),
179 counts: vec![0; n],
180 breaks,
181 na_count: 0,
182 seen: HashSet::new(),
183 na_color: (127, 127, 127),
184 na_pattern: None,
185 na_label: "no data".to_string(),
186 name: String::new(),
187 corrected,
188 }
189 }
190
191 pub fn na_color(mut self, color: (u8, u8, u8)) -> Self {
196 self.na_color = color;
197 self
198 }
199
200 pub fn na_pattern(mut self, pattern: FillPattern) -> Self {
207 self.na_pattern = Some(pattern);
208 self
209 }
210
211 pub fn na_label(mut self, label: &str) -> Self {
213 self.na_label = label.to_string();
214 self
215 }
216
217 fn bin_index(&self, v: f64) -> usize {
233 let n = self.colors.len();
234 let v = v.clamp(self.breaks[0], self.breaks[n]);
235 for i in 0..n {
236 let (lo, hi) = (self.breaks[i], self.breaks[i + 1]);
237 let hit = if i == n - 1 {
238 v >= lo && v <= hi } else if lo == hi {
240 v == lo } else {
242 v >= lo && v < hi };
244 if hit {
245 return i;
246 }
247 }
248 n - 1
249 }
250
251 fn class_label(&self, i: usize) -> String {
256 let (lo, hi) = (self.breaks[i], self.breaks[i + 1]);
257 if lo == hi {
258 format!("{} ({})", format_value(&Value::Float(lo)), self.counts[i])
259 } else {
260 format!(
261 "{}–{} ({})",
262 format_value(&Value::Float(lo)),
263 format_value(&Value::Float(hi)),
264 self.counts[i]
265 )
266 }
267 }
268
269 fn na_label_with_count(&self) -> String {
271 format!("{} ({})", self.na_label, self.na_count)
272 }
273}
274
275impl Scale for ScaleFillBinned {
276 fn aesthetic(&self) -> Aesthetic {
277 self.aesthetic.clone()
278 }
279
280 fn train(&mut self, values: &[Value]) {
281 if !self.seen.insert(signature(values)) {
285 return;
286 }
287 for v in values {
288 match v {
289 Value::Na => self.na_count += 1,
290 _ => {
291 if let Some(f) = v.as_f64() {
292 if f.is_finite() {
293 let i = self.bin_index(f);
294 self.counts[i] += 1;
295 }
296 }
297 }
298 }
299 }
300 }
301
302 fn map(&self, _value: &Value) -> f64 {
305 0.0
306 }
307
308 fn breaks(&self) -> Vec<(f64, String)> {
309 let n = self.colors.len();
310 let (lo0, hi_n) = (self.breaks[0], self.breaks[n]);
311 let span = (hi_n - lo0).max(f64::EPSILON);
312 let mut out: Vec<(f64, String)> = (0..n)
313 .map(|i| {
314 let center = (self.breaks[i] + self.breaks[i + 1]) / 2.0;
315 ((center - lo0) / span, self.class_label(i))
316 })
317 .collect();
318 if self.na_count > 0 {
319 out.push((1.0, self.na_label_with_count()));
320 }
321 out
322 }
323
324 fn name(&self) -> &str {
325 &self.name
326 }
327
328 fn set_name(&mut self, name: &str) {
329 self.name = name.to_string();
330 }
331
332 fn is_discrete(&self) -> bool {
333 true
334 }
335
336 fn map_to_color(&self, value: &Value) -> Option<(u8, u8, u8)> {
337 if matches!(value, Value::Na) {
338 return Some(self.na_color);
339 }
340 if let Some(f) = value.as_f64() {
341 if f.is_finite() {
342 return Some(self.colors[self.bin_index(f)]);
343 }
344 }
345 if let Value::Str(s) = value {
346 if s == &self.na_label_with_count() {
347 return Some(self.na_color);
348 }
349 for i in 0..self.colors.len() {
350 if self.class_label(i) == *s {
351 return Some(self.colors[i]);
352 }
353 }
354 }
355 None
356 }
357
358 fn domain(&self) -> Option<(f64, f64)> {
359 let n = self.colors.len();
360 Some((self.breaks[0], self.breaks[n]))
361 }
362
363 fn na_fill_pattern(&self) -> Option<FillPattern> {
369 self.na_pattern
370 }
371
372 fn is_na_break(&self, label: &str) -> bool {
373 self.na_count > 0 && label == self.na_label_with_count()
374 }
375
376 fn validate(&self, warnings: &mut Vec<String>) {
377 if self.corrected {
378 warnings.push(
379 "scale_fill_binned: breaks need >= 2 increasing finite edges, with one colour \
380 per bin — falling back to a default range/palette"
381 .to_string(),
382 );
383 }
384 }
385
386 fn clone_box(&self) -> Box<dyn Scale> {
387 Box::new(self.clone())
388 }
389
390 fn reset_training(&mut self) {
391 self.counts.iter_mut().for_each(|c| *c = 0);
392 self.na_count = 0;
393 self.seen.clear();
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 fn scale() -> ScaleFillBinned {
402 ScaleFillBinned::new(
403 Aesthetic::Fill,
404 vec![0.0, 10.0, 50.0, 100.0],
405 vec![(0, 0, 255), (0, 255, 0), (255, 0, 0)],
406 )
407 }
408
409 #[test]
410 fn bins_are_half_open_except_the_last() {
411 let s = scale();
412 assert_eq!(s.bin_index(0.0), 0);
413 assert_eq!(s.bin_index(9.999), 0);
414 assert_eq!(s.bin_index(10.0), 1); assert_eq!(s.bin_index(49.999), 1);
416 assert_eq!(s.bin_index(50.0), 2);
417 assert_eq!(s.bin_index(100.0), 2); assert_eq!(
419 s.bin_index(1000.0),
420 2,
421 "out-of-range clamps to the last bin"
422 );
423 assert_eq!(
424 s.bin_index(-50.0),
425 0,
426 "out-of-range clamps to the first bin"
427 );
428 }
429
430 #[test]
431 fn train_tallies_counts_and_na_separately() {
432 let mut s = scale();
433 s.train(&[
434 Value::Float(1.0),
435 Value::Float(5.0),
436 Value::Float(20.0),
437 Value::Float(99.0),
438 Value::Float(100.0),
439 Value::Na,
440 Value::Na,
441 ]);
442 assert_eq!(s.counts, vec![2, 1, 2]);
443 assert_eq!(s.na_count, 2);
444 }
445
446 #[test]
452 fn training_the_same_batch_twice_does_not_double_count() {
453 let batch = [Value::Float(1.0), Value::Float(20.0), Value::Na];
454 let mut s = scale();
455 s.train(&batch);
456 s.train(&batch); assert_eq!(s.counts, vec![1, 1, 0]);
458 assert_eq!(s.na_count, 1);
459 s.train(&[Value::Float(60.0)]);
461 assert_eq!(s.counts, vec![1, 1, 1]);
462 }
463
464 #[test]
465 fn breaks_label_each_class_with_its_count_and_na_with_its_own() {
466 let mut s = scale();
467 s.train(&[Value::Float(1.0), Value::Float(20.0), Value::Na]);
468 let breaks = s.breaks();
469 let labels: Vec<&str> = breaks.iter().map(|(_, l)| l.as_str()).collect();
470 assert_eq!(
471 labels,
472 vec!["0–10 (1)", "10–50 (1)", "50–100 (0)", "no data (1)"]
473 );
474 }
475
476 #[test]
477 fn no_na_key_when_nothing_is_missing() {
478 let mut s = scale();
479 s.train(&[Value::Float(1.0)]);
480 assert_eq!(s.breaks().len(), 3, "no trailing no-data entry");
481 }
482
483 #[test]
484 fn map_to_color_matches_the_bin_and_roundtrips_legend_labels() {
485 let mut s = scale();
486 s.train(&[Value::Float(1.0), Value::Float(20.0), Value::Na]);
487 assert_eq!(s.map_to_color(&Value::Float(5.0)), Some((0, 0, 255)));
488 assert_eq!(s.map_to_color(&Value::Float(20.0)), Some((0, 255, 0)));
489 assert_eq!(s.map_to_color(&Value::Float(75.0)), Some((255, 0, 0)));
490 assert_eq!(s.map_to_color(&Value::Na), Some((127, 127, 127)));
491 for (_, label) in s.breaks() {
492 let via_label = s.map_to_color(&Value::Str(label.clone()));
493 assert!(via_label.is_some(), "{label} should round-trip to a colour");
494 }
495 }
496
497 #[test]
498 fn na_color_and_label_are_configurable() {
499 let mut s = scale().na_color((10, 20, 30)).na_label("missing");
500 s.train(&[Value::Na]);
501 assert_eq!(s.map_to_color(&Value::Na), Some((10, 20, 30)));
502 assert!(s.breaks().iter().any(|(_, l)| l == "missing (1)"));
503 assert_eq!(
504 s.map_to_color(&Value::Str("missing (1)".to_string())),
505 Some((10, 20, 30))
506 );
507 }
508
509 #[test]
510 fn na_pattern_is_none_until_set() {
511 let s = scale();
512 assert_eq!(s.na_fill_pattern(), None);
513 }
514
515 #[test]
516 fn na_pattern_is_reported_only_for_the_no_data_break_label() {
517 let pattern = FillPattern::stripe((10, 20, 30), (200, 210, 220));
518 let mut s = scale().na_pattern(pattern);
519 s.train(&[Value::Na, Value::Float(5.0)]);
520 assert_eq!(s.na_fill_pattern(), Some(pattern));
521 assert!(s.is_na_break(&s.na_label_with_count()));
522 let ordinary = s.class_label(0);
524 assert!(!s.is_na_break(&ordinary));
525 }
526
527 #[test]
528 fn no_na_break_when_nothing_trained_as_na() {
529 let s = scale().na_pattern(FillPattern::stripe((1, 2, 3), (4, 5, 6)));
532 assert!(!s.is_na_break(&s.na_label_with_count()));
533 }
534
535 #[test]
536 fn two_interpolates_colors_across_bins() {
537 let s = ScaleFillBinned::two(
538 Aesthetic::Fill,
539 vec![0.0, 25.0, 50.0, 75.0, 100.0],
540 (0, 0, 0),
541 (255, 255, 255),
542 );
543 assert_eq!(s.colors[0], (0, 0, 0));
544 assert_eq!(s.colors[3], (255, 255, 255));
545 assert!(s.colors[1].0 < s.colors[2].0);
546 }
547
548 #[test]
549 fn mismatched_colors_fall_back_and_warn() {
550 let s = ScaleFillBinned::new(
551 Aesthetic::Fill,
552 vec![0.0, 10.0, 20.0],
553 vec![(0, 0, 0)], );
555 assert_eq!(s.colors.len(), 2);
556 let mut warnings = Vec::new();
557 s.validate(&mut warnings);
558 assert_eq!(warnings.len(), 1);
559 }
560
561 #[test]
562 fn too_few_breaks_falls_back_to_a_single_bin_rather_than_panicking() {
563 let s = ScaleFillBinned::new(Aesthetic::Fill, vec![5.0], vec![]);
564 assert_eq!(s.breaks.len(), 2);
565 assert_eq!(s.colors.len(), 1);
566 let mut warnings = Vec::new();
567 s.validate(&mut warnings);
568 assert_eq!(warnings.len(), 1);
569 }
570
571 #[test]
572 fn domain_spans_the_outer_edges() {
573 let s = scale();
574 assert_eq!(s.domain(), Some((0.0, 100.0)));
575 }
576
577 #[test]
578 fn reset_training_clears_counts_but_keeps_breaks_and_colors() {
579 let mut s = scale();
580 s.train(&[Value::Float(1.0), Value::Na]);
581 s.reset_training();
582 assert_eq!(s.counts, vec![0, 0, 0]);
583 assert_eq!(s.na_count, 0);
584 assert_eq!(s.breaks, vec![0.0, 10.0, 50.0, 100.0]);
585 }
586
587 #[test]
593 fn duplicate_edge_at_the_max_keeps_both_classes_and_colours() {
594 let mut s = ScaleFillBinned::new(
595 Aesthetic::Fill,
596 vec![35.0, 150.0, 150.0],
597 vec![(0, 0, 255), (255, 0, 0)],
598 );
599 assert!(
600 !s.corrected,
601 "a legitimate zero-width bin is not a fallback"
602 );
603 assert_eq!(s.colors.len(), 2, "caller's requested class count is kept");
604 assert_eq!(
605 s.colors,
606 vec![(0, 0, 255), (255, 0, 0)],
607 "distinct colours per class"
608 );
609
610 s.train(&[Value::Float(35.0), Value::Float(150.0)]);
611 assert_eq!(s.counts, vec![1, 1], "each cell counted into its own class");
612 assert_eq!(s.bin_index(35.0), 0);
613 assert_eq!(
614 s.bin_index(150.0),
615 1,
616 "the zero-width bin claims its own edge value"
617 );
618 assert_eq!(s.map_to_color(&Value::Float(150.0)), Some((255, 0, 0)));
619
620 let breaks = s.breaks();
621 let labels: Vec<&str> = breaks.iter().map(|(_, l)| l.as_str()).collect();
622 assert_eq!(
623 labels,
624 vec!["35–150 (1)", "150 (1)"],
625 "the zero-width class's label/count is a single value, not a 150–150 range"
626 );
627 }
628
629 #[test]
632 fn duplicate_edge_at_the_min_keeps_both_classes_and_colours() {
633 let mut s = ScaleFillBinned::new(
634 Aesthetic::Fill,
635 vec![0.0, 0.0, 10.0],
636 vec![(0, 0, 255), (255, 0, 0)],
637 );
638 assert!(!s.corrected);
639 assert_eq!(s.colors.len(), 2);
640
641 s.train(&[Value::Float(0.0), Value::Float(0.0), Value::Float(5.0)]);
642 assert_eq!(s.counts, vec![2, 1]);
643 assert_eq!(s.bin_index(0.0), 0, "the zero-width bin claims the minimum");
644 assert_eq!(s.bin_index(5.0), 1);
645 assert_eq!(s.map_to_color(&Value::Float(0.0)), Some((0, 0, 255)));
646
647 let breaks = s.breaks();
648 let labels: Vec<&str> = breaks.iter().map(|(_, l)| l.as_str()).collect();
649 assert_eq!(labels, vec!["0 (2)", "0–10 (1)"]);
650 }
651
652 #[test]
655 fn duplicate_edge_in_the_middle_keeps_all_classes_and_colours() {
656 let mut s = ScaleFillBinned::new(
657 Aesthetic::Fill,
658 vec![0.0, 50.0, 50.0, 100.0],
659 vec![(0, 0, 255), (0, 255, 0), (255, 0, 0)],
660 );
661 assert!(!s.corrected);
662 assert_eq!(s.colors.len(), 3, "all three requested classes survive");
663 assert_eq!(s.colors, vec![(0, 0, 255), (0, 255, 0), (255, 0, 0)]);
664
665 s.train(&[Value::Float(25.0), Value::Float(50.0), Value::Float(75.0)]);
666 assert_eq!(
667 s.counts,
668 vec![1, 1, 1],
669 "the shared edge value goes to the middle (zero-width) bin"
670 );
671 assert_eq!(s.bin_index(25.0), 0);
672 assert_eq!(
673 s.bin_index(50.0),
674 1,
675 "the zero-width middle bin wins the tie over its neighbours"
676 );
677 assert_eq!(s.bin_index(75.0), 2);
678 assert_eq!(s.map_to_color(&Value::Float(50.0)), Some((0, 255, 0)));
679
680 let breaks = s.breaks();
681 let labels: Vec<&str> = breaks.iter().map(|(_, l)| l.as_str()).collect();
682 assert_eq!(labels, vec!["0–50 (1)", "50 (1)", "50–100 (1)"]);
683 }
684
685 #[test]
689 fn non_degenerate_edges_are_unaffected() {
690 let s = scale();
691 assert_eq!(s.breaks, vec![0.0, 10.0, 50.0, 100.0]);
692 assert_eq!(s.colors, vec![(0, 0, 255), (0, 255, 0), (255, 0, 0)]);
693 assert_eq!(s.bin_index(0.0), 0);
694 assert_eq!(s.bin_index(9.999), 0);
695 assert_eq!(s.bin_index(10.0), 1);
696 assert_eq!(s.bin_index(49.999), 1);
697 assert_eq!(s.bin_index(50.0), 2);
698 assert_eq!(s.bin_index(100.0), 2);
699 }
700}