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 name: String,
109 corrected: bool,
110}
111
112fn sorted_edges(breaks: Vec<f64>) -> (Vec<f64>, bool) {
124 let mut v: Vec<f64> = breaks.into_iter().filter(|f| f.is_finite()).collect();
125 v.sort_by(f64::total_cmp);
126 if v.len() >= 2 {
127 (v, false)
128 } else {
129 (vec![0.0, 1.0], true)
130 }
131}
132
133fn lerp_color(low: (u8, u8, u8), high: (u8, u8, u8), t: f64) -> (u8, u8, u8) {
135 let t = t.clamp(0.0, 1.0);
136 let lerp = |a: u8, b: u8| (a as f64 + (b as f64 - a as f64) * t).round() as u8;
137 (
138 lerp(low.0, high.0),
139 lerp(low.1, high.1),
140 lerp(low.2, high.2),
141 )
142}
143
144fn default_colors(n: usize, low: (u8, u8, u8), high: (u8, u8, u8)) -> Vec<(u8, u8, u8)> {
147 (0..n)
148 .map(|i| {
149 let t = if n <= 1 {
150 0.5
151 } else {
152 i as f64 / (n - 1) as f64
153 };
154 lerp_color(low, high, t)
155 })
156 .collect()
157}
158
159const DEFAULT_LOW: (u8, u8, u8) = (50, 50, 200);
161const DEFAULT_HIGH: (u8, u8, u8) = (200, 50, 50);
162
163impl ScaleFillBinned {
164 pub fn new(aesthetic: Aesthetic, breaks: Vec<f64>, colors: Vec<(u8, u8, u8)>) -> Self {
169 let (breaks, edges_corrected) = sorted_edges(breaks);
170 let n = breaks.len() - 1;
171 let (colors, colors_corrected) = if colors.len() == n {
172 (colors, false)
173 } else {
174 (default_colors(n, DEFAULT_LOW, DEFAULT_HIGH), true)
175 };
176 ScaleFillBinned {
177 aesthetic,
178 breaks,
179 colors,
180 counts: vec![0; n],
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 na_key: NaKey::Auto,
187 count_reserve: None,
188 name: String::new(),
189 corrected: edges_corrected || colors_corrected,
190 }
191 }
192
193 pub fn two(
196 aesthetic: Aesthetic,
197 breaks: Vec<f64>,
198 low: (u8, u8, u8),
199 high: (u8, u8, u8),
200 ) -> Self {
201 let (breaks, corrected) = sorted_edges(breaks);
202 let n = breaks.len() - 1;
203 ScaleFillBinned {
204 aesthetic,
205 colors: default_colors(n, low, high),
206 counts: vec![0; n],
207 breaks,
208 na_count: 0,
209 seen: HashSet::new(),
210 na_color: (127, 127, 127),
211 na_pattern: None,
212 na_label: "no data".to_string(),
213 na_key: NaKey::Auto,
214 count_reserve: None,
215 name: String::new(),
216 corrected,
217 }
218 }
219
220 pub fn na_color(mut self, color: (u8, u8, u8)) -> Self {
225 self.na_color = color;
226 self
227 }
228
229 pub fn na_pattern(mut self, pattern: FillPattern) -> Self {
236 self.na_pattern = Some(pattern);
237 self
238 }
239
240 pub fn na_label(mut self, label: &str) -> Self {
242 self.na_label = label.to_string();
243 self
244 }
245
246 pub fn na_key(mut self, show: NaKey) -> Self {
255 self.na_key = show;
256 self
257 }
258
259 pub fn count_reserve(mut self, max_count: usize) -> Self {
270 self.count_reserve = Some(max_count);
271 self
272 }
273
274 fn pad_trailing_count(label: &str, min_digits: usize) -> String {
282 if label.ends_with(')') {
283 if let Some(open) = label.rfind('(') {
284 let inner = &label[open + 1..label.len() - 1];
285 if !inner.is_empty() && inner.chars().all(|c| c.is_ascii_digit()) {
286 let digits = min_digits.max(inner.len());
287 return format!("{}({inner:0>digits$})", &label[..open]);
288 }
289 }
290 }
291 label.to_string()
292 }
293
294 fn bin_index(&self, v: f64) -> usize {
310 let n = self.colors.len();
311 let v = v.clamp(self.breaks[0], self.breaks[n]);
312 for i in 0..n {
313 let (lo, hi) = (self.breaks[i], self.breaks[i + 1]);
314 let hit = if i == n - 1 {
315 v >= lo && v <= hi } else if lo == hi {
317 v == lo } else {
319 v >= lo && v < hi };
321 if hit {
322 return i;
323 }
324 }
325 n - 1
326 }
327
328 fn class_label(&self, i: usize) -> String {
333 let (lo, hi) = (self.breaks[i], self.breaks[i + 1]);
334 if lo == hi {
335 format!("{} ({})", format_value(&Value::Float(lo)), self.counts[i])
336 } else {
337 format!(
338 "{}–{} ({})",
339 format_value(&Value::Float(lo)),
340 format_value(&Value::Float(hi)),
341 self.counts[i]
342 )
343 }
344 }
345
346 fn na_label_with_count(&self) -> String {
348 format!("{} ({})", self.na_label, self.na_count)
349 }
350
351 fn na_key_shown(&self) -> bool {
353 match self.na_key {
354 NaKey::Auto => self.na_count > 0,
355 NaKey::Always => true,
356 NaKey::Never => false,
357 }
358 }
359}
360
361impl Scale for ScaleFillBinned {
362 fn aesthetic(&self) -> Aesthetic {
363 self.aesthetic.clone()
364 }
365
366 fn train(&mut self, values: &[Value]) {
367 if !self.seen.insert(signature(values)) {
371 return;
372 }
373 for v in values {
374 match v {
375 Value::Na => self.na_count += 1,
376 _ => {
377 if let Some(f) = v.as_f64() {
378 if f.is_finite() {
379 let i = self.bin_index(f);
380 self.counts[i] += 1;
381 }
382 }
383 }
384 }
385 }
386 }
387
388 fn map(&self, _value: &Value) -> f64 {
391 0.0
392 }
393
394 fn breaks(&self) -> Vec<(f64, String)> {
395 let n = self.colors.len();
396 let (lo0, hi_n) = (self.breaks[0], self.breaks[n]);
397 let span = (hi_n - lo0).max(f64::EPSILON);
398 let mut out: Vec<(f64, String)> = (0..n)
399 .map(|i| {
400 let center = (self.breaks[i] + self.breaks[i + 1]) / 2.0;
401 ((center - lo0) / span, self.class_label(i))
402 })
403 .collect();
404 if self.na_key_shown() {
405 out.push((1.0, self.na_label_with_count()));
406 }
407 out
408 }
409
410 fn breaks_for_layout(&self) -> Vec<(f64, String)> {
415 match self.count_reserve {
416 None => self.breaks(),
417 Some(reserve) => {
418 let min_digits = reserve.to_string().len();
419 self.breaks()
420 .into_iter()
421 .map(|(pos, label)| (pos, Self::pad_trailing_count(&label, min_digits)))
422 .collect()
423 }
424 }
425 }
426
427 fn name(&self) -> &str {
428 &self.name
429 }
430
431 fn set_name(&mut self, name: &str) {
432 self.name = name.to_string();
433 }
434
435 fn is_discrete(&self) -> bool {
436 true
437 }
438
439 fn map_to_color(&self, value: &Value) -> Option<(u8, u8, u8)> {
440 if matches!(value, Value::Na) {
441 return Some(self.na_color);
442 }
443 if let Some(f) = value.as_f64() {
444 if f.is_finite() {
445 return Some(self.colors[self.bin_index(f)]);
446 }
447 }
448 if let Value::Str(s) = value {
449 if s == &self.na_label_with_count() {
450 return Some(self.na_color);
451 }
452 for i in 0..self.colors.len() {
453 if self.class_label(i) == *s {
454 return Some(self.colors[i]);
455 }
456 }
457 }
458 None
459 }
460
461 fn domain(&self) -> Option<(f64, f64)> {
462 let n = self.colors.len();
463 Some((self.breaks[0], self.breaks[n]))
464 }
465
466 fn na_fill_pattern(&self) -> Option<FillPattern> {
472 self.na_pattern
473 }
474
475 fn is_na_break(&self, label: &str) -> bool {
476 self.na_key_shown() && label == self.na_label_with_count()
477 }
478
479 fn validate(&self, warnings: &mut Vec<String>) {
480 if self.corrected {
481 warnings.push(
482 "scale_fill_binned: breaks need >= 2 increasing finite edges, with one colour \
483 per bin — falling back to a default range/palette"
484 .to_string(),
485 );
486 }
487 }
488
489 fn clone_box(&self) -> Box<dyn Scale> {
490 Box::new(self.clone())
491 }
492
493 fn reset_training(&mut self) {
494 self.counts.iter_mut().for_each(|c| *c = 0);
495 self.na_count = 0;
496 self.seen.clear();
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 fn scale() -> ScaleFillBinned {
505 ScaleFillBinned::new(
506 Aesthetic::Fill,
507 vec![0.0, 10.0, 50.0, 100.0],
508 vec![(0, 0, 255), (0, 255, 0), (255, 0, 0)],
509 )
510 }
511
512 #[test]
513 fn bins_are_half_open_except_the_last() {
514 let s = scale();
515 assert_eq!(s.bin_index(0.0), 0);
516 assert_eq!(s.bin_index(9.999), 0);
517 assert_eq!(s.bin_index(10.0), 1); assert_eq!(s.bin_index(49.999), 1);
519 assert_eq!(s.bin_index(50.0), 2);
520 assert_eq!(s.bin_index(100.0), 2); assert_eq!(
522 s.bin_index(1000.0),
523 2,
524 "out-of-range clamps to the last bin"
525 );
526 assert_eq!(
527 s.bin_index(-50.0),
528 0,
529 "out-of-range clamps to the first bin"
530 );
531 }
532
533 #[test]
534 fn train_tallies_counts_and_na_separately() {
535 let mut s = scale();
536 s.train(&[
537 Value::Float(1.0),
538 Value::Float(5.0),
539 Value::Float(20.0),
540 Value::Float(99.0),
541 Value::Float(100.0),
542 Value::Na,
543 Value::Na,
544 ]);
545 assert_eq!(s.counts, vec![2, 1, 2]);
546 assert_eq!(s.na_count, 2);
547 }
548
549 #[test]
555 fn training_the_same_batch_twice_does_not_double_count() {
556 let batch = [Value::Float(1.0), Value::Float(20.0), Value::Na];
557 let mut s = scale();
558 s.train(&batch);
559 s.train(&batch); assert_eq!(s.counts, vec![1, 1, 0]);
561 assert_eq!(s.na_count, 1);
562 s.train(&[Value::Float(60.0)]);
564 assert_eq!(s.counts, vec![1, 1, 1]);
565 }
566
567 #[test]
568 fn breaks_label_each_class_with_its_count_and_na_with_its_own() {
569 let mut s = scale();
570 s.train(&[Value::Float(1.0), Value::Float(20.0), Value::Na]);
571 let breaks = s.breaks();
572 let labels: Vec<&str> = breaks.iter().map(|(_, l)| l.as_str()).collect();
573 assert_eq!(
574 labels,
575 vec!["0–10 (1)", "10–50 (1)", "50–100 (0)", "no data (1)"]
576 );
577 }
578
579 #[test]
580 fn no_na_key_when_nothing_is_missing() {
581 let mut s = scale();
582 s.train(&[Value::Float(1.0)]);
583 assert_eq!(s.breaks().len(), 3, "no trailing no-data entry");
584 }
585
586 #[test]
587 fn map_to_color_matches_the_bin_and_roundtrips_legend_labels() {
588 let mut s = scale();
589 s.train(&[Value::Float(1.0), Value::Float(20.0), Value::Na]);
590 assert_eq!(s.map_to_color(&Value::Float(5.0)), Some((0, 0, 255)));
591 assert_eq!(s.map_to_color(&Value::Float(20.0)), Some((0, 255, 0)));
592 assert_eq!(s.map_to_color(&Value::Float(75.0)), Some((255, 0, 0)));
593 assert_eq!(s.map_to_color(&Value::Na), Some((127, 127, 127)));
594 for (_, label) in s.breaks() {
595 let via_label = s.map_to_color(&Value::Str(label.clone()));
596 assert!(via_label.is_some(), "{label} should round-trip to a colour");
597 }
598 }
599
600 #[test]
601 fn na_color_and_label_are_configurable() {
602 let mut s = scale().na_color((10, 20, 30)).na_label("missing");
603 s.train(&[Value::Na]);
604 assert_eq!(s.map_to_color(&Value::Na), Some((10, 20, 30)));
605 assert!(s.breaks().iter().any(|(_, l)| l == "missing (1)"));
606 assert_eq!(
607 s.map_to_color(&Value::Str("missing (1)".to_string())),
608 Some((10, 20, 30))
609 );
610 }
611
612 #[test]
613 fn na_pattern_is_none_until_set() {
614 let s = scale();
615 assert_eq!(s.na_fill_pattern(), None);
616 }
617
618 #[test]
619 fn na_pattern_is_reported_only_for_the_no_data_break_label() {
620 let pattern = FillPattern::stripe((10, 20, 30), (200, 210, 220));
621 let mut s = scale().na_pattern(pattern);
622 s.train(&[Value::Na, Value::Float(5.0)]);
623 assert_eq!(s.na_fill_pattern(), Some(pattern));
624 assert!(s.is_na_break(&s.na_label_with_count()));
625 let ordinary = s.class_label(0);
627 assert!(!s.is_na_break(&ordinary));
628 }
629
630 #[test]
631 fn no_na_break_when_nothing_trained_as_na() {
632 let s = scale().na_pattern(FillPattern::stripe((1, 2, 3), (4, 5, 6)));
635 assert!(!s.is_na_break(&s.na_label_with_count()));
636 }
637
638 #[test]
639 fn two_interpolates_colors_across_bins() {
640 let s = ScaleFillBinned::two(
641 Aesthetic::Fill,
642 vec![0.0, 25.0, 50.0, 75.0, 100.0],
643 (0, 0, 0),
644 (255, 255, 255),
645 );
646 assert_eq!(s.colors[0], (0, 0, 0));
647 assert_eq!(s.colors[3], (255, 255, 255));
648 assert!(s.colors[1].0 < s.colors[2].0);
649 }
650
651 #[test]
652 fn mismatched_colors_fall_back_and_warn() {
653 let s = ScaleFillBinned::new(
654 Aesthetic::Fill,
655 vec![0.0, 10.0, 20.0],
656 vec![(0, 0, 0)], );
658 assert_eq!(s.colors.len(), 2);
659 let mut warnings = Vec::new();
660 s.validate(&mut warnings);
661 assert_eq!(warnings.len(), 1);
662 }
663
664 #[test]
665 fn too_few_breaks_falls_back_to_a_single_bin_rather_than_panicking() {
666 let s = ScaleFillBinned::new(Aesthetic::Fill, vec![5.0], vec![]);
667 assert_eq!(s.breaks.len(), 2);
668 assert_eq!(s.colors.len(), 1);
669 let mut warnings = Vec::new();
670 s.validate(&mut warnings);
671 assert_eq!(warnings.len(), 1);
672 }
673
674 #[test]
675 fn domain_spans_the_outer_edges() {
676 let s = scale();
677 assert_eq!(s.domain(), Some((0.0, 100.0)));
678 }
679
680 #[test]
681 fn reset_training_clears_counts_but_keeps_breaks_and_colors() {
682 let mut s = scale();
683 s.train(&[Value::Float(1.0), Value::Na]);
684 s.reset_training();
685 assert_eq!(s.counts, vec![0, 0, 0]);
686 assert_eq!(s.na_count, 0);
687 assert_eq!(s.breaks, vec![0.0, 10.0, 50.0, 100.0]);
688 }
689
690 #[test]
696 fn duplicate_edge_at_the_max_keeps_both_classes_and_colours() {
697 let mut s = ScaleFillBinned::new(
698 Aesthetic::Fill,
699 vec![35.0, 150.0, 150.0],
700 vec![(0, 0, 255), (255, 0, 0)],
701 );
702 assert!(
703 !s.corrected,
704 "a legitimate zero-width bin is not a fallback"
705 );
706 assert_eq!(s.colors.len(), 2, "caller's requested class count is kept");
707 assert_eq!(
708 s.colors,
709 vec![(0, 0, 255), (255, 0, 0)],
710 "distinct colours per class"
711 );
712
713 s.train(&[Value::Float(35.0), Value::Float(150.0)]);
714 assert_eq!(s.counts, vec![1, 1], "each cell counted into its own class");
715 assert_eq!(s.bin_index(35.0), 0);
716 assert_eq!(
717 s.bin_index(150.0),
718 1,
719 "the zero-width bin claims its own edge value"
720 );
721 assert_eq!(s.map_to_color(&Value::Float(150.0)), Some((255, 0, 0)));
722
723 let breaks = s.breaks();
724 let labels: Vec<&str> = breaks.iter().map(|(_, l)| l.as_str()).collect();
725 assert_eq!(
726 labels,
727 vec!["35–150 (1)", "150 (1)"],
728 "the zero-width class's label/count is a single value, not a 150–150 range"
729 );
730 }
731
732 #[test]
735 fn duplicate_edge_at_the_min_keeps_both_classes_and_colours() {
736 let mut s = ScaleFillBinned::new(
737 Aesthetic::Fill,
738 vec![0.0, 0.0, 10.0],
739 vec![(0, 0, 255), (255, 0, 0)],
740 );
741 assert!(!s.corrected);
742 assert_eq!(s.colors.len(), 2);
743
744 s.train(&[Value::Float(0.0), Value::Float(0.0), Value::Float(5.0)]);
745 assert_eq!(s.counts, vec![2, 1]);
746 assert_eq!(s.bin_index(0.0), 0, "the zero-width bin claims the minimum");
747 assert_eq!(s.bin_index(5.0), 1);
748 assert_eq!(s.map_to_color(&Value::Float(0.0)), Some((0, 0, 255)));
749
750 let breaks = s.breaks();
751 let labels: Vec<&str> = breaks.iter().map(|(_, l)| l.as_str()).collect();
752 assert_eq!(labels, vec!["0 (2)", "0–10 (1)"]);
753 }
754
755 #[test]
758 fn duplicate_edge_in_the_middle_keeps_all_classes_and_colours() {
759 let mut s = ScaleFillBinned::new(
760 Aesthetic::Fill,
761 vec![0.0, 50.0, 50.0, 100.0],
762 vec![(0, 0, 255), (0, 255, 0), (255, 0, 0)],
763 );
764 assert!(!s.corrected);
765 assert_eq!(s.colors.len(), 3, "all three requested classes survive");
766 assert_eq!(s.colors, vec![(0, 0, 255), (0, 255, 0), (255, 0, 0)]);
767
768 s.train(&[Value::Float(25.0), Value::Float(50.0), Value::Float(75.0)]);
769 assert_eq!(
770 s.counts,
771 vec![1, 1, 1],
772 "the shared edge value goes to the middle (zero-width) bin"
773 );
774 assert_eq!(s.bin_index(25.0), 0);
775 assert_eq!(
776 s.bin_index(50.0),
777 1,
778 "the zero-width middle bin wins the tie over its neighbours"
779 );
780 assert_eq!(s.bin_index(75.0), 2);
781 assert_eq!(s.map_to_color(&Value::Float(50.0)), Some((0, 255, 0)));
782
783 let breaks = s.breaks();
784 let labels: Vec<&str> = breaks.iter().map(|(_, l)| l.as_str()).collect();
785 assert_eq!(labels, vec!["0–50 (1)", "50 (1)", "50–100 (1)"]);
786 }
787
788 #[test]
792 fn non_degenerate_edges_are_unaffected() {
793 let s = scale();
794 assert_eq!(s.breaks, vec![0.0, 10.0, 50.0, 100.0]);
795 assert_eq!(s.colors, vec![(0, 0, 255), (0, 255, 0), (255, 0, 0)]);
796 assert_eq!(s.bin_index(0.0), 0);
797 assert_eq!(s.bin_index(9.999), 0);
798 assert_eq!(s.bin_index(10.0), 1);
799 assert_eq!(s.bin_index(49.999), 1);
800 assert_eq!(s.bin_index(50.0), 2);
801 assert_eq!(s.bin_index(100.0), 2);
802 }
803
804 #[test]
807 fn na_key_always_lists_the_key_with_zero_count_when_nothing_is_missing() {
808 let mut s = scale().na_key(NaKey::Always);
809 s.train(&[Value::Float(1.0)]);
810 let breaks = s.breaks();
811 assert_eq!(breaks.len(), 4, "the no-data key is kept even at 0");
812 let (_, label) = breaks.last().unwrap();
813 assert_eq!(label, "no data (0)");
814 assert_eq!(
816 s.map_to_color(&Value::Str(label.clone())),
817 Some((127, 127, 127))
818 );
819 assert!(s.is_na_break(label));
820 }
821
822 #[test]
823 fn na_key_never_hides_it_even_with_na_present() {
824 let mut s = scale().na_key(NaKey::Never);
825 s.train(&[Value::Float(1.0), Value::Na, Value::Na]);
826 let breaks = s.breaks();
827 assert_eq!(breaks.len(), 3, "no trailing no-data entry despite 2 NAs");
828 assert!(!s.is_na_break(&s.na_label_with_count()));
829 }
830
831 #[test]
832 fn na_key_auto_is_unchanged() {
833 let mut without_na = scale();
835 without_na.train(&[Value::Float(1.0)]);
836 assert_eq!(without_na.breaks().len(), 3);
837
838 let mut with_na = scale();
839 with_na.train(&[Value::Float(1.0), Value::Na]);
840 assert_eq!(with_na.breaks().len(), 4);
841 }
842
843 #[test]
846 fn count_reserve_pads_measured_labels_but_not_drawn_ones() {
847 let mut few = scale().count_reserve(9999);
848 few.train(&[Value::Float(1.0), Value::Float(20.0), Value::Float(75.0)]);
849 let drawn = few.breaks();
850 let labels: Vec<&str> = drawn.iter().map(|(_, l)| l.as_str()).collect();
851 assert_eq!(
852 labels,
853 vec!["0–10 (1)", "10–50 (1)", "50–100 (1)"],
854 "drawn labels unaffected"
855 );
856
857 let layout = few.breaks_for_layout();
858 let layout_labels: Vec<&str> = layout.iter().map(|(_, l)| l.as_str()).collect();
859 assert_eq!(
860 layout_labels,
861 vec!["0–10 (0001)", "10–50 (0001)", "50–100 (0001)"],
862 "layout labels reserve the 4-digit width"
863 );
864 }
865
866 #[test]
873 fn count_reserve_gives_equal_length_layout_labels_across_differing_counts() {
874 let mut few = scale().count_reserve(9999);
875 few.train(&[Value::Float(1.0), Value::Float(20.0), Value::Float(75.0)]);
876
877 let mut many = scale().count_reserve(9999);
878 let batch: Vec<Value> = (0..1234)
879 .flat_map(|_| [Value::Float(1.0), Value::Float(20.0), Value::Float(75.0)])
880 .collect();
881 many.train(&batch);
882
883 let few_layout = few.breaks_for_layout();
884 let many_layout = many.breaks_for_layout();
885 for (f, m) in few_layout.iter().zip(many_layout.iter()) {
886 assert_eq!(
887 f.1.len(),
888 m.1.len(),
889 "layout labels must measure the same width: {f:?} vs {m:?}"
890 );
891 }
892 assert_ne!(few.breaks(), many.breaks());
894 }
895
896 #[test]
897 fn count_reserve_is_none_by_default_and_layout_matches_breaks() {
898 let mut s = scale();
899 s.train(&[Value::Float(1.0), Value::Na]);
900 assert_eq!(s.breaks_for_layout(), s.breaks());
901 }
902
903 #[test]
904 fn count_reserve_never_shrinks_below_the_actual_count() {
905 let mut s = scale().count_reserve(1);
908 let batch: Vec<Value> = (0..1234).map(|_| Value::Float(1.0)).collect();
909 s.train(&batch);
910 let layout = s.breaks_for_layout();
911 assert!(layout[0].1.contains("(1234)"), "{layout:?}");
912 }
913}