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
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
24pub enum NaKey {
25 #[default]
28 Auto,
29 Always,
34 Never,
37}
38
39fn signature(values: &[Value]) -> u64 {
43 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
44 let mut mix = |x: u64| {
45 h ^= x;
46 h = h.wrapping_mul(0x0000_0100_0000_01b3);
47 };
48 mix(values.len() as u64);
49 for v in values {
50 match v {
51 Value::Na => mix(1),
52 _ => match v.as_f64() {
53 Some(f) => {
54 mix(2);
55 mix(f.to_bits());
56 }
57 None => mix(3),
58 },
59 }
60 }
61 h
62}
63
64#[derive(Clone, Debug)]
68pub struct ScaleFillBinned {
69 aesthetic: Aesthetic,
70 breaks: Vec<f64>,
72 colors: Vec<(u8, u8, u8)>,
73 counts: Vec<usize>,
75 na_count: usize,
77 seen: HashSet<u64>,
89 na_color: (u8, u8, u8),
93 na_pattern: Option<FillPattern>,
100 na_label: String,
101 na_key: NaKey,
103 count_reserve: Option<usize>,
108 show_counts: bool,
111 name: String,
112 corrected: bool,
113}
114
115fn sorted_edges(breaks: Vec<f64>) -> (Vec<f64>, bool) {
127 let mut v: Vec<f64> = breaks.into_iter().filter(|f| f.is_finite()).collect();
128 v.sort_by(f64::total_cmp);
129 if v.len() >= 2 {
130 (v, false)
131 } else {
132 (vec![0.0, 1.0], true)
133 }
134}
135
136fn lerp_color(low: (u8, u8, u8), high: (u8, u8, u8), t: f64) -> (u8, u8, u8) {
138 let t = t.clamp(0.0, 1.0);
139 let lerp = |a: u8, b: u8| (a as f64 + (b as f64 - a as f64) * t).round() as u8;
140 (
141 lerp(low.0, high.0),
142 lerp(low.1, high.1),
143 lerp(low.2, high.2),
144 )
145}
146
147fn default_colors(n: usize, low: (u8, u8, u8), high: (u8, u8, u8)) -> Vec<(u8, u8, u8)> {
150 (0..n)
151 .map(|i| {
152 let t = if n <= 1 {
153 0.5
154 } else {
155 i as f64 / (n - 1) as f64
156 };
157 lerp_color(low, high, t)
158 })
159 .collect()
160}
161
162const DEFAULT_LOW: (u8, u8, u8) = (50, 50, 200);
164const DEFAULT_HIGH: (u8, u8, u8) = (200, 50, 50);
165
166impl ScaleFillBinned {
167 pub fn new(aesthetic: Aesthetic, breaks: Vec<f64>, colors: Vec<(u8, u8, u8)>) -> Self {
172 let (breaks, edges_corrected) = sorted_edges(breaks);
173 let n = breaks.len() - 1;
174 let (colors, colors_corrected) = if colors.len() == n {
175 (colors, false)
176 } else {
177 (default_colors(n, DEFAULT_LOW, DEFAULT_HIGH), true)
178 };
179 ScaleFillBinned {
180 aesthetic,
181 breaks,
182 colors,
183 counts: vec![0; n],
184 na_count: 0,
185 seen: HashSet::new(),
186 na_color: (127, 127, 127),
187 na_pattern: None,
188 na_label: "no data".to_string(),
189 na_key: NaKey::Auto,
190 count_reserve: None,
191 show_counts: true,
192 name: String::new(),
193 corrected: edges_corrected || colors_corrected,
194 }
195 }
196
197 pub fn two(
200 aesthetic: Aesthetic,
201 breaks: Vec<f64>,
202 low: (u8, u8, u8),
203 high: (u8, u8, u8),
204 ) -> Self {
205 let (breaks, corrected) = sorted_edges(breaks);
206 let n = breaks.len() - 1;
207 ScaleFillBinned {
208 aesthetic,
209 colors: default_colors(n, low, high),
210 counts: vec![0; n],
211 breaks,
212 na_count: 0,
213 seen: HashSet::new(),
214 na_color: (127, 127, 127),
215 na_pattern: None,
216 na_label: "no data".to_string(),
217 na_key: NaKey::Auto,
218 count_reserve: None,
219 show_counts: true,
220 name: String::new(),
221 corrected,
222 }
223 }
224
225 pub fn na_color(mut self, color: (u8, u8, u8)) -> Self {
230 self.na_color = color;
231 self
232 }
233
234 pub fn na_pattern(mut self, pattern: FillPattern) -> Self {
241 self.na_pattern = Some(pattern);
242 self
243 }
244
245 pub fn na_label(mut self, label: &str) -> Self {
247 self.na_label = label.to_string();
248 self
249 }
250
251 pub fn na_key(mut self, show: NaKey) -> Self {
260 self.na_key = show;
261 self
262 }
263
264 pub fn count_reserve(mut self, max_count: usize) -> Self {
275 self.count_reserve = Some(max_count);
276 self
277 }
278
279 pub fn show_counts(mut self, on: bool) -> Self {
284 self.show_counts = on;
285 self
286 }
287
288 fn pad_trailing_count(label: &str, min_digits: usize) -> String {
296 if label.ends_with(')') {
297 if let Some(open) = label.rfind('(') {
298 let inner = &label[open + 1..label.len() - 1];
299 if !inner.is_empty() && inner.chars().all(|c| c.is_ascii_digit()) {
300 let digits = min_digits.max(inner.len());
301 return format!("{}({inner:0>digits$})", &label[..open]);
302 }
303 }
304 }
305 label.to_string()
306 }
307
308 fn bin_index(&self, v: f64) -> usize {
324 let n = self.colors.len();
325 let v = v.clamp(self.breaks[0], self.breaks[n]);
326 for i in 0..n {
327 let (lo, hi) = (self.breaks[i], self.breaks[i + 1]);
328 let hit = if i == n - 1 {
329 v >= lo && v <= hi } else if lo == hi {
331 v == lo } else {
333 v >= lo && v < hi };
335 if hit {
336 return i;
337 }
338 }
339 n - 1
340 }
341
342 fn class_label(&self, i: usize) -> String {
347 let (lo, hi) = (self.breaks[i], self.breaks[i + 1]);
348 if !self.show_counts {
349 return if lo == hi {
350 format_value(&Value::Float(lo))
351 } else {
352 format!(
353 "{}–{}",
354 format_value(&Value::Float(lo)),
355 format_value(&Value::Float(hi))
356 )
357 };
358 }
359 if lo == hi {
360 format!("{} ({})", format_value(&Value::Float(lo)), self.counts[i])
361 } else {
362 format!(
363 "{}–{} ({})",
364 format_value(&Value::Float(lo)),
365 format_value(&Value::Float(hi)),
366 self.counts[i]
367 )
368 }
369 }
370
371 fn na_label_with_count(&self) -> String {
373 if !self.show_counts {
374 return self.na_label.clone();
375 }
376 format!("{} ({})", self.na_label, self.na_count)
377 }
378
379 fn na_key_shown(&self) -> bool {
381 match self.na_key {
382 NaKey::Auto => self.na_count > 0,
383 NaKey::Always => true,
384 NaKey::Never => false,
385 }
386 }
387}
388
389impl Scale for ScaleFillBinned {
390 fn aesthetic(&self) -> Aesthetic {
391 self.aesthetic.clone()
392 }
393
394 fn train(&mut self, values: &[Value]) {
395 if !self.seen.insert(signature(values)) {
399 return;
400 }
401 for v in values {
402 match v {
403 Value::Na => self.na_count += 1,
404 _ => {
405 if let Some(f) = v.as_f64() {
406 if f.is_finite() {
407 let i = self.bin_index(f);
408 self.counts[i] += 1;
409 }
410 }
411 }
412 }
413 }
414 }
415
416 fn map(&self, _value: &Value) -> f64 {
419 0.0
420 }
421
422 fn breaks(&self) -> Vec<(f64, String)> {
423 let n = self.colors.len();
424 let (lo0, hi_n) = (self.breaks[0], self.breaks[n]);
425 let span = (hi_n - lo0).max(f64::EPSILON);
426 let mut out: Vec<(f64, String)> = (0..n)
427 .map(|i| {
428 let center = (self.breaks[i] + self.breaks[i + 1]) / 2.0;
429 ((center - lo0) / span, self.class_label(i))
430 })
431 .collect();
432 if self.na_key_shown() {
433 out.push((1.0, self.na_label_with_count()));
434 }
435 out
436 }
437
438 fn breaks_for_layout(&self) -> Vec<(f64, String)> {
443 match self.count_reserve.filter(|_| self.show_counts) {
444 None => self.breaks(),
445 Some(reserve) => {
446 let min_digits = reserve.to_string().len();
447 self.breaks()
448 .into_iter()
449 .map(|(pos, label)| (pos, Self::pad_trailing_count(&label, min_digits)))
450 .collect()
451 }
452 }
453 }
454
455 fn name(&self) -> &str {
456 &self.name
457 }
458
459 fn set_name(&mut self, name: &str) {
460 self.name = name.to_string();
461 }
462
463 fn is_discrete(&self) -> bool {
464 true
465 }
466
467 fn map_to_color(&self, value: &Value) -> Option<(u8, u8, u8)> {
468 if matches!(value, Value::Na) {
469 return Some(self.na_color);
470 }
471 if let Some(f) = value.as_f64() {
472 if f.is_finite() {
473 return Some(self.colors[self.bin_index(f)]);
474 }
475 }
476 if let Value::Str(s) = value {
477 if s == &self.na_label_with_count() {
478 return Some(self.na_color);
479 }
480 for i in 0..self.colors.len() {
481 if self.class_label(i) == *s {
482 return Some(self.colors[i]);
483 }
484 }
485 }
486 None
487 }
488
489 fn domain(&self) -> Option<(f64, f64)> {
490 let n = self.colors.len();
491 Some((self.breaks[0], self.breaks[n]))
492 }
493
494 fn na_fill_pattern(&self) -> Option<FillPattern> {
500 self.na_pattern
501 }
502
503 fn is_na_break(&self, label: &str) -> bool {
504 self.na_key_shown() && label == self.na_label_with_count()
505 }
506
507 fn validate(&self, warnings: &mut Vec<String>) {
508 if self.corrected {
509 warnings.push(
510 "scale_fill_binned: breaks need >= 2 increasing finite edges, with one colour \
511 per bin — falling back to a default range/palette"
512 .to_string(),
513 );
514 }
515 }
516
517 fn clone_box(&self) -> Box<dyn Scale> {
518 Box::new(self.clone())
519 }
520
521 fn reset_training(&mut self) {
522 self.counts.iter_mut().for_each(|c| *c = 0);
523 self.na_count = 0;
524 self.seen.clear();
525 }
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531
532 fn scale() -> ScaleFillBinned {
533 ScaleFillBinned::new(
534 Aesthetic::Fill,
535 vec![0.0, 10.0, 50.0, 100.0],
536 vec![(0, 0, 255), (0, 255, 0), (255, 0, 0)],
537 )
538 }
539
540 #[test]
541 fn bins_are_half_open_except_the_last() {
542 let s = scale();
543 assert_eq!(s.bin_index(0.0), 0);
544 assert_eq!(s.bin_index(9.999), 0);
545 assert_eq!(s.bin_index(10.0), 1); assert_eq!(s.bin_index(49.999), 1);
547 assert_eq!(s.bin_index(50.0), 2);
548 assert_eq!(s.bin_index(100.0), 2); assert_eq!(
550 s.bin_index(1000.0),
551 2,
552 "out-of-range clamps to the last bin"
553 );
554 assert_eq!(
555 s.bin_index(-50.0),
556 0,
557 "out-of-range clamps to the first bin"
558 );
559 }
560
561 #[test]
562 fn train_tallies_counts_and_na_separately() {
563 let mut s = scale();
564 s.train(&[
565 Value::Float(1.0),
566 Value::Float(5.0),
567 Value::Float(20.0),
568 Value::Float(99.0),
569 Value::Float(100.0),
570 Value::Na,
571 Value::Na,
572 ]);
573 assert_eq!(s.counts, vec![2, 1, 2]);
574 assert_eq!(s.na_count, 2);
575 }
576
577 #[test]
583 fn training_the_same_batch_twice_does_not_double_count() {
584 let batch = [Value::Float(1.0), Value::Float(20.0), Value::Na];
585 let mut s = scale();
586 s.train(&batch);
587 s.train(&batch); assert_eq!(s.counts, vec![1, 1, 0]);
589 assert_eq!(s.na_count, 1);
590 s.train(&[Value::Float(60.0)]);
592 assert_eq!(s.counts, vec![1, 1, 1]);
593 }
594
595 #[test]
596 fn breaks_label_each_class_with_its_count_and_na_with_its_own() {
597 let mut s = scale();
598 s.train(&[Value::Float(1.0), Value::Float(20.0), Value::Na]);
599 let breaks = s.breaks();
600 let labels: Vec<&str> = breaks.iter().map(|(_, l)| l.as_str()).collect();
601 assert_eq!(
602 labels,
603 vec!["0–10 (1)", "10–50 (1)", "50–100 (0)", "no data (1)"]
604 );
605 }
606
607 #[test]
608 fn no_na_key_when_nothing_is_missing() {
609 let mut s = scale();
610 s.train(&[Value::Float(1.0)]);
611 assert_eq!(s.breaks().len(), 3, "no trailing no-data entry");
612 }
613
614 #[test]
615 fn map_to_color_matches_the_bin_and_roundtrips_legend_labels() {
616 let mut s = scale();
617 s.train(&[Value::Float(1.0), Value::Float(20.0), Value::Na]);
618 assert_eq!(s.map_to_color(&Value::Float(5.0)), Some((0, 0, 255)));
619 assert_eq!(s.map_to_color(&Value::Float(20.0)), Some((0, 255, 0)));
620 assert_eq!(s.map_to_color(&Value::Float(75.0)), Some((255, 0, 0)));
621 assert_eq!(s.map_to_color(&Value::Na), Some((127, 127, 127)));
622 for (_, label) in s.breaks() {
623 let via_label = s.map_to_color(&Value::Str(label.clone()));
624 assert!(via_label.is_some(), "{label} should round-trip to a colour");
625 }
626 }
627
628 #[test]
629 fn na_color_and_label_are_configurable() {
630 let mut s = scale().na_color((10, 20, 30)).na_label("missing");
631 s.train(&[Value::Na]);
632 assert_eq!(s.map_to_color(&Value::Na), Some((10, 20, 30)));
633 assert!(s.breaks().iter().any(|(_, l)| l == "missing (1)"));
634 assert_eq!(
635 s.map_to_color(&Value::Str("missing (1)".to_string())),
636 Some((10, 20, 30))
637 );
638 }
639
640 #[test]
641 fn na_pattern_is_none_until_set() {
642 let s = scale();
643 assert_eq!(s.na_fill_pattern(), None);
644 }
645
646 #[test]
647 fn na_pattern_is_reported_only_for_the_no_data_break_label() {
648 let pattern = FillPattern::stripe((10, 20, 30), (200, 210, 220));
649 let mut s = scale().na_pattern(pattern);
650 s.train(&[Value::Na, Value::Float(5.0)]);
651 assert_eq!(s.na_fill_pattern(), Some(pattern));
652 assert!(s.is_na_break(&s.na_label_with_count()));
653 let ordinary = s.class_label(0);
655 assert!(!s.is_na_break(&ordinary));
656 }
657
658 #[test]
659 fn no_na_break_when_nothing_trained_as_na() {
660 let s = scale().na_pattern(FillPattern::stripe((1, 2, 3), (4, 5, 6)));
663 assert!(!s.is_na_break(&s.na_label_with_count()));
664 }
665
666 #[test]
667 fn two_interpolates_colors_across_bins() {
668 let s = ScaleFillBinned::two(
669 Aesthetic::Fill,
670 vec![0.0, 25.0, 50.0, 75.0, 100.0],
671 (0, 0, 0),
672 (255, 255, 255),
673 );
674 assert_eq!(s.colors[0], (0, 0, 0));
675 assert_eq!(s.colors[3], (255, 255, 255));
676 assert!(s.colors[1].0 < s.colors[2].0);
677 }
678
679 #[test]
680 fn mismatched_colors_fall_back_and_warn() {
681 let s = ScaleFillBinned::new(
682 Aesthetic::Fill,
683 vec![0.0, 10.0, 20.0],
684 vec![(0, 0, 0)], );
686 assert_eq!(s.colors.len(), 2);
687 let mut warnings = Vec::new();
688 s.validate(&mut warnings);
689 assert_eq!(warnings.len(), 1);
690 }
691
692 #[test]
693 fn too_few_breaks_falls_back_to_a_single_bin_rather_than_panicking() {
694 let s = ScaleFillBinned::new(Aesthetic::Fill, vec![5.0], vec![]);
695 assert_eq!(s.breaks.len(), 2);
696 assert_eq!(s.colors.len(), 1);
697 let mut warnings = Vec::new();
698 s.validate(&mut warnings);
699 assert_eq!(warnings.len(), 1);
700 }
701
702 #[test]
703 fn domain_spans_the_outer_edges() {
704 let s = scale();
705 assert_eq!(s.domain(), Some((0.0, 100.0)));
706 }
707
708 #[test]
709 fn reset_training_clears_counts_but_keeps_breaks_and_colors() {
710 let mut s = scale();
711 s.train(&[Value::Float(1.0), Value::Na]);
712 s.reset_training();
713 assert_eq!(s.counts, vec![0, 0, 0]);
714 assert_eq!(s.na_count, 0);
715 assert_eq!(s.breaks, vec![0.0, 10.0, 50.0, 100.0]);
716 }
717
718 #[test]
724 fn duplicate_edge_at_the_max_keeps_both_classes_and_colours() {
725 let mut s = ScaleFillBinned::new(
726 Aesthetic::Fill,
727 vec![35.0, 150.0, 150.0],
728 vec![(0, 0, 255), (255, 0, 0)],
729 );
730 assert!(
731 !s.corrected,
732 "a legitimate zero-width bin is not a fallback"
733 );
734 assert_eq!(s.colors.len(), 2, "caller's requested class count is kept");
735 assert_eq!(
736 s.colors,
737 vec![(0, 0, 255), (255, 0, 0)],
738 "distinct colours per class"
739 );
740
741 s.train(&[Value::Float(35.0), Value::Float(150.0)]);
742 assert_eq!(s.counts, vec![1, 1], "each cell counted into its own class");
743 assert_eq!(s.bin_index(35.0), 0);
744 assert_eq!(
745 s.bin_index(150.0),
746 1,
747 "the zero-width bin claims its own edge value"
748 );
749 assert_eq!(s.map_to_color(&Value::Float(150.0)), Some((255, 0, 0)));
750
751 let breaks = s.breaks();
752 let labels: Vec<&str> = breaks.iter().map(|(_, l)| l.as_str()).collect();
753 assert_eq!(
754 labels,
755 vec!["35–150 (1)", "150 (1)"],
756 "the zero-width class's label/count is a single value, not a 150–150 range"
757 );
758 }
759
760 #[test]
763 fn duplicate_edge_at_the_min_keeps_both_classes_and_colours() {
764 let mut s = ScaleFillBinned::new(
765 Aesthetic::Fill,
766 vec![0.0, 0.0, 10.0],
767 vec![(0, 0, 255), (255, 0, 0)],
768 );
769 assert!(!s.corrected);
770 assert_eq!(s.colors.len(), 2);
771
772 s.train(&[Value::Float(0.0), Value::Float(0.0), Value::Float(5.0)]);
773 assert_eq!(s.counts, vec![2, 1]);
774 assert_eq!(s.bin_index(0.0), 0, "the zero-width bin claims the minimum");
775 assert_eq!(s.bin_index(5.0), 1);
776 assert_eq!(s.map_to_color(&Value::Float(0.0)), Some((0, 0, 255)));
777
778 let breaks = s.breaks();
779 let labels: Vec<&str> = breaks.iter().map(|(_, l)| l.as_str()).collect();
780 assert_eq!(labels, vec!["0 (2)", "0–10 (1)"]);
781 }
782
783 #[test]
786 fn duplicate_edge_in_the_middle_keeps_all_classes_and_colours() {
787 let mut s = ScaleFillBinned::new(
788 Aesthetic::Fill,
789 vec![0.0, 50.0, 50.0, 100.0],
790 vec![(0, 0, 255), (0, 255, 0), (255, 0, 0)],
791 );
792 assert!(!s.corrected);
793 assert_eq!(s.colors.len(), 3, "all three requested classes survive");
794 assert_eq!(s.colors, vec![(0, 0, 255), (0, 255, 0), (255, 0, 0)]);
795
796 s.train(&[Value::Float(25.0), Value::Float(50.0), Value::Float(75.0)]);
797 assert_eq!(
798 s.counts,
799 vec![1, 1, 1],
800 "the shared edge value goes to the middle (zero-width) bin"
801 );
802 assert_eq!(s.bin_index(25.0), 0);
803 assert_eq!(
804 s.bin_index(50.0),
805 1,
806 "the zero-width middle bin wins the tie over its neighbours"
807 );
808 assert_eq!(s.bin_index(75.0), 2);
809 assert_eq!(s.map_to_color(&Value::Float(50.0)), Some((0, 255, 0)));
810
811 let breaks = s.breaks();
812 let labels: Vec<&str> = breaks.iter().map(|(_, l)| l.as_str()).collect();
813 assert_eq!(labels, vec!["0–50 (1)", "50 (1)", "50–100 (1)"]);
814 }
815
816 #[test]
820 fn non_degenerate_edges_are_unaffected() {
821 let s = scale();
822 assert_eq!(s.breaks, vec![0.0, 10.0, 50.0, 100.0]);
823 assert_eq!(s.colors, vec![(0, 0, 255), (0, 255, 0), (255, 0, 0)]);
824 assert_eq!(s.bin_index(0.0), 0);
825 assert_eq!(s.bin_index(9.999), 0);
826 assert_eq!(s.bin_index(10.0), 1);
827 assert_eq!(s.bin_index(49.999), 1);
828 assert_eq!(s.bin_index(50.0), 2);
829 assert_eq!(s.bin_index(100.0), 2);
830 }
831
832 #[test]
835 fn na_key_always_lists_the_key_with_zero_count_when_nothing_is_missing() {
836 let mut s = scale().na_key(NaKey::Always);
837 s.train(&[Value::Float(1.0)]);
838 let breaks = s.breaks();
839 assert_eq!(breaks.len(), 4, "the no-data key is kept even at 0");
840 let (_, label) = breaks.last().unwrap();
841 assert_eq!(label, "no data (0)");
842 assert_eq!(
844 s.map_to_color(&Value::Str(label.clone())),
845 Some((127, 127, 127))
846 );
847 assert!(s.is_na_break(label));
848 }
849
850 #[test]
851 fn na_key_never_hides_it_even_with_na_present() {
852 let mut s = scale().na_key(NaKey::Never);
853 s.train(&[Value::Float(1.0), Value::Na, Value::Na]);
854 let breaks = s.breaks();
855 assert_eq!(breaks.len(), 3, "no trailing no-data entry despite 2 NAs");
856 assert!(!s.is_na_break(&s.na_label_with_count()));
857 }
858
859 #[test]
860 fn na_key_auto_is_unchanged() {
861 let mut without_na = scale();
863 without_na.train(&[Value::Float(1.0)]);
864 assert_eq!(without_na.breaks().len(), 3);
865
866 let mut with_na = scale();
867 with_na.train(&[Value::Float(1.0), Value::Na]);
868 assert_eq!(with_na.breaks().len(), 4);
869 }
870
871 #[test]
874 fn count_reserve_pads_measured_labels_but_not_drawn_ones() {
875 let mut few = scale().count_reserve(9999);
876 few.train(&[Value::Float(1.0), Value::Float(20.0), Value::Float(75.0)]);
877 let drawn = few.breaks();
878 let labels: Vec<&str> = drawn.iter().map(|(_, l)| l.as_str()).collect();
879 assert_eq!(
880 labels,
881 vec!["0–10 (1)", "10–50 (1)", "50–100 (1)"],
882 "drawn labels unaffected"
883 );
884
885 let layout = few.breaks_for_layout();
886 let layout_labels: Vec<&str> = layout.iter().map(|(_, l)| l.as_str()).collect();
887 assert_eq!(
888 layout_labels,
889 vec!["0–10 (0001)", "10–50 (0001)", "50–100 (0001)"],
890 "layout labels reserve the 4-digit width"
891 );
892 }
893
894 #[test]
901 fn count_reserve_gives_equal_length_layout_labels_across_differing_counts() {
902 let mut few = scale().count_reserve(9999);
903 few.train(&[Value::Float(1.0), Value::Float(20.0), Value::Float(75.0)]);
904
905 let mut many = scale().count_reserve(9999);
906 let batch: Vec<Value> = (0..1234)
907 .flat_map(|_| [Value::Float(1.0), Value::Float(20.0), Value::Float(75.0)])
908 .collect();
909 many.train(&batch);
910
911 let few_layout = few.breaks_for_layout();
912 let many_layout = many.breaks_for_layout();
913 for (f, m) in few_layout.iter().zip(many_layout.iter()) {
914 assert_eq!(
915 f.1.len(),
916 m.1.len(),
917 "layout labels must measure the same width: {f:?} vs {m:?}"
918 );
919 }
920 assert_ne!(few.breaks(), many.breaks());
922 }
923
924 #[test]
925 fn count_reserve_is_none_by_default_and_layout_matches_breaks() {
926 let mut s = scale();
927 s.train(&[Value::Float(1.0), Value::Na]);
928 assert_eq!(s.breaks_for_layout(), s.breaks());
929 }
930
931 #[test]
932 fn count_reserve_never_shrinks_below_the_actual_count() {
933 let mut s = scale().count_reserve(1);
936 let batch: Vec<Value> = (0..1234).map(|_| Value::Float(1.0)).collect();
937 s.train(&batch);
938 let layout = s.breaks_for_layout();
939 assert!(layout[0].1.contains("(1234)"), "{layout:?}");
940 }
941}