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) {
91 let mut v: Vec<f64> = breaks.into_iter().filter(|f| f.is_finite()).collect();
92 v.sort_by(f64::total_cmp);
93 v.dedup();
94 if v.len() >= 2 {
95 (v, false)
96 } else {
97 (vec![0.0, 1.0], true)
98 }
99}
100
101fn lerp_color(low: (u8, u8, u8), high: (u8, u8, u8), t: f64) -> (u8, u8, u8) {
103 let t = t.clamp(0.0, 1.0);
104 let lerp = |a: u8, b: u8| (a as f64 + (b as f64 - a as f64) * t).round() as u8;
105 (
106 lerp(low.0, high.0),
107 lerp(low.1, high.1),
108 lerp(low.2, high.2),
109 )
110}
111
112fn default_colors(n: usize, low: (u8, u8, u8), high: (u8, u8, u8)) -> Vec<(u8, u8, u8)> {
115 (0..n)
116 .map(|i| {
117 let t = if n <= 1 {
118 0.5
119 } else {
120 i as f64 / (n - 1) as f64
121 };
122 lerp_color(low, high, t)
123 })
124 .collect()
125}
126
127const DEFAULT_LOW: (u8, u8, u8) = (50, 50, 200);
129const DEFAULT_HIGH: (u8, u8, u8) = (200, 50, 50);
130
131impl ScaleFillBinned {
132 pub fn new(aesthetic: Aesthetic, breaks: Vec<f64>, colors: Vec<(u8, u8, u8)>) -> Self {
137 let (breaks, edges_corrected) = sorted_edges(breaks);
138 let n = breaks.len() - 1;
139 let (colors, colors_corrected) = if colors.len() == n {
140 (colors, false)
141 } else {
142 (default_colors(n, DEFAULT_LOW, DEFAULT_HIGH), true)
143 };
144 ScaleFillBinned {
145 aesthetic,
146 breaks,
147 colors,
148 counts: vec![0; n],
149 na_count: 0,
150 seen: HashSet::new(),
151 na_color: (127, 127, 127),
152 na_pattern: None,
153 na_label: "no data".to_string(),
154 name: String::new(),
155 corrected: edges_corrected || colors_corrected,
156 }
157 }
158
159 pub fn two(
162 aesthetic: Aesthetic,
163 breaks: Vec<f64>,
164 low: (u8, u8, u8),
165 high: (u8, u8, u8),
166 ) -> Self {
167 let (breaks, corrected) = sorted_edges(breaks);
168 let n = breaks.len() - 1;
169 ScaleFillBinned {
170 aesthetic,
171 colors: default_colors(n, low, high),
172 counts: vec![0; n],
173 breaks,
174 na_count: 0,
175 seen: HashSet::new(),
176 na_color: (127, 127, 127),
177 na_pattern: None,
178 na_label: "no data".to_string(),
179 name: String::new(),
180 corrected,
181 }
182 }
183
184 pub fn na_color(mut self, color: (u8, u8, u8)) -> Self {
189 self.na_color = color;
190 self
191 }
192
193 pub fn na_pattern(mut self, pattern: FillPattern) -> Self {
200 self.na_pattern = Some(pattern);
201 self
202 }
203
204 pub fn na_label(mut self, label: &str) -> Self {
206 self.na_label = label.to_string();
207 self
208 }
209
210 fn bin_index(&self, v: f64) -> usize {
215 let n = self.colors.len();
216 let v = v.clamp(self.breaks[0], self.breaks[n]);
217 for (i, w) in self.breaks[1..n].iter().enumerate() {
218 if v < *w {
219 return i;
220 }
221 }
222 n - 1
223 }
224
225 fn class_label(&self, i: usize) -> String {
227 format!(
228 "{}–{} ({})",
229 format_value(&Value::Float(self.breaks[i])),
230 format_value(&Value::Float(self.breaks[i + 1])),
231 self.counts[i]
232 )
233 }
234
235 fn na_label_with_count(&self) -> String {
237 format!("{} ({})", self.na_label, self.na_count)
238 }
239}
240
241impl Scale for ScaleFillBinned {
242 fn aesthetic(&self) -> Aesthetic {
243 self.aesthetic.clone()
244 }
245
246 fn train(&mut self, values: &[Value]) {
247 if !self.seen.insert(signature(values)) {
251 return;
252 }
253 for v in values {
254 match v {
255 Value::Na => self.na_count += 1,
256 _ => {
257 if let Some(f) = v.as_f64() {
258 if f.is_finite() {
259 let i = self.bin_index(f);
260 self.counts[i] += 1;
261 }
262 }
263 }
264 }
265 }
266 }
267
268 fn map(&self, _value: &Value) -> f64 {
271 0.0
272 }
273
274 fn breaks(&self) -> Vec<(f64, String)> {
275 let n = self.colors.len();
276 let (lo0, hi_n) = (self.breaks[0], self.breaks[n]);
277 let span = (hi_n - lo0).max(f64::EPSILON);
278 let mut out: Vec<(f64, String)> = (0..n)
279 .map(|i| {
280 let center = (self.breaks[i] + self.breaks[i + 1]) / 2.0;
281 ((center - lo0) / span, self.class_label(i))
282 })
283 .collect();
284 if self.na_count > 0 {
285 out.push((1.0, self.na_label_with_count()));
286 }
287 out
288 }
289
290 fn name(&self) -> &str {
291 &self.name
292 }
293
294 fn set_name(&mut self, name: &str) {
295 self.name = name.to_string();
296 }
297
298 fn is_discrete(&self) -> bool {
299 true
300 }
301
302 fn map_to_color(&self, value: &Value) -> Option<(u8, u8, u8)> {
303 if matches!(value, Value::Na) {
304 return Some(self.na_color);
305 }
306 if let Some(f) = value.as_f64() {
307 if f.is_finite() {
308 return Some(self.colors[self.bin_index(f)]);
309 }
310 }
311 if let Value::Str(s) = value {
312 if s == &self.na_label_with_count() {
313 return Some(self.na_color);
314 }
315 for i in 0..self.colors.len() {
316 if self.class_label(i) == *s {
317 return Some(self.colors[i]);
318 }
319 }
320 }
321 None
322 }
323
324 fn domain(&self) -> Option<(f64, f64)> {
325 let n = self.colors.len();
326 Some((self.breaks[0], self.breaks[n]))
327 }
328
329 fn na_fill_pattern(&self) -> Option<FillPattern> {
335 self.na_pattern
336 }
337
338 fn is_na_break(&self, label: &str) -> bool {
339 self.na_count > 0 && label == self.na_label_with_count()
340 }
341
342 fn validate(&self, warnings: &mut Vec<String>) {
343 if self.corrected {
344 warnings.push(
345 "scale_fill_binned: breaks need >= 2 increasing finite edges, with one colour \
346 per bin — falling back to a default range/palette"
347 .to_string(),
348 );
349 }
350 }
351
352 fn clone_box(&self) -> Box<dyn Scale> {
353 Box::new(self.clone())
354 }
355
356 fn reset_training(&mut self) {
357 self.counts.iter_mut().for_each(|c| *c = 0);
358 self.na_count = 0;
359 self.seen.clear();
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 fn scale() -> ScaleFillBinned {
368 ScaleFillBinned::new(
369 Aesthetic::Fill,
370 vec![0.0, 10.0, 50.0, 100.0],
371 vec![(0, 0, 255), (0, 255, 0), (255, 0, 0)],
372 )
373 }
374
375 #[test]
376 fn bins_are_half_open_except_the_last() {
377 let s = scale();
378 assert_eq!(s.bin_index(0.0), 0);
379 assert_eq!(s.bin_index(9.999), 0);
380 assert_eq!(s.bin_index(10.0), 1); assert_eq!(s.bin_index(49.999), 1);
382 assert_eq!(s.bin_index(50.0), 2);
383 assert_eq!(s.bin_index(100.0), 2); assert_eq!(
385 s.bin_index(1000.0),
386 2,
387 "out-of-range clamps to the last bin"
388 );
389 assert_eq!(
390 s.bin_index(-50.0),
391 0,
392 "out-of-range clamps to the first bin"
393 );
394 }
395
396 #[test]
397 fn train_tallies_counts_and_na_separately() {
398 let mut s = scale();
399 s.train(&[
400 Value::Float(1.0),
401 Value::Float(5.0),
402 Value::Float(20.0),
403 Value::Float(99.0),
404 Value::Float(100.0),
405 Value::Na,
406 Value::Na,
407 ]);
408 assert_eq!(s.counts, vec![2, 1, 2]);
409 assert_eq!(s.na_count, 2);
410 }
411
412 #[test]
418 fn training_the_same_batch_twice_does_not_double_count() {
419 let batch = [Value::Float(1.0), Value::Float(20.0), Value::Na];
420 let mut s = scale();
421 s.train(&batch);
422 s.train(&batch); assert_eq!(s.counts, vec![1, 1, 0]);
424 assert_eq!(s.na_count, 1);
425 s.train(&[Value::Float(60.0)]);
427 assert_eq!(s.counts, vec![1, 1, 1]);
428 }
429
430 #[test]
431 fn breaks_label_each_class_with_its_count_and_na_with_its_own() {
432 let mut s = scale();
433 s.train(&[Value::Float(1.0), Value::Float(20.0), Value::Na]);
434 let breaks = s.breaks();
435 let labels: Vec<&str> = breaks.iter().map(|(_, l)| l.as_str()).collect();
436 assert_eq!(
437 labels,
438 vec!["0–10 (1)", "10–50 (1)", "50–100 (0)", "no data (1)"]
439 );
440 }
441
442 #[test]
443 fn no_na_key_when_nothing_is_missing() {
444 let mut s = scale();
445 s.train(&[Value::Float(1.0)]);
446 assert_eq!(s.breaks().len(), 3, "no trailing no-data entry");
447 }
448
449 #[test]
450 fn map_to_color_matches_the_bin_and_roundtrips_legend_labels() {
451 let mut s = scale();
452 s.train(&[Value::Float(1.0), Value::Float(20.0), Value::Na]);
453 assert_eq!(s.map_to_color(&Value::Float(5.0)), Some((0, 0, 255)));
454 assert_eq!(s.map_to_color(&Value::Float(20.0)), Some((0, 255, 0)));
455 assert_eq!(s.map_to_color(&Value::Float(75.0)), Some((255, 0, 0)));
456 assert_eq!(s.map_to_color(&Value::Na), Some((127, 127, 127)));
457 for (_, label) in s.breaks() {
458 let via_label = s.map_to_color(&Value::Str(label.clone()));
459 assert!(via_label.is_some(), "{label} should round-trip to a colour");
460 }
461 }
462
463 #[test]
464 fn na_color_and_label_are_configurable() {
465 let mut s = scale().na_color((10, 20, 30)).na_label("missing");
466 s.train(&[Value::Na]);
467 assert_eq!(s.map_to_color(&Value::Na), Some((10, 20, 30)));
468 assert!(s.breaks().iter().any(|(_, l)| l == "missing (1)"));
469 assert_eq!(
470 s.map_to_color(&Value::Str("missing (1)".to_string())),
471 Some((10, 20, 30))
472 );
473 }
474
475 #[test]
476 fn na_pattern_is_none_until_set() {
477 let s = scale();
478 assert_eq!(s.na_fill_pattern(), None);
479 }
480
481 #[test]
482 fn na_pattern_is_reported_only_for_the_no_data_break_label() {
483 let pattern = FillPattern::stripe((10, 20, 30), (200, 210, 220));
484 let mut s = scale().na_pattern(pattern);
485 s.train(&[Value::Na, Value::Float(5.0)]);
486 assert_eq!(s.na_fill_pattern(), Some(pattern));
487 assert!(s.is_na_break(&s.na_label_with_count()));
488 let ordinary = s.class_label(0);
490 assert!(!s.is_na_break(&ordinary));
491 }
492
493 #[test]
494 fn no_na_break_when_nothing_trained_as_na() {
495 let s = scale().na_pattern(FillPattern::stripe((1, 2, 3), (4, 5, 6)));
498 assert!(!s.is_na_break(&s.na_label_with_count()));
499 }
500
501 #[test]
502 fn two_interpolates_colors_across_bins() {
503 let s = ScaleFillBinned::two(
504 Aesthetic::Fill,
505 vec![0.0, 25.0, 50.0, 75.0, 100.0],
506 (0, 0, 0),
507 (255, 255, 255),
508 );
509 assert_eq!(s.colors[0], (0, 0, 0));
510 assert_eq!(s.colors[3], (255, 255, 255));
511 assert!(s.colors[1].0 < s.colors[2].0);
512 }
513
514 #[test]
515 fn mismatched_colors_fall_back_and_warn() {
516 let s = ScaleFillBinned::new(
517 Aesthetic::Fill,
518 vec![0.0, 10.0, 20.0],
519 vec![(0, 0, 0)], );
521 assert_eq!(s.colors.len(), 2);
522 let mut warnings = Vec::new();
523 s.validate(&mut warnings);
524 assert_eq!(warnings.len(), 1);
525 }
526
527 #[test]
528 fn too_few_breaks_falls_back_to_a_single_bin_rather_than_panicking() {
529 let s = ScaleFillBinned::new(Aesthetic::Fill, vec![5.0], vec![]);
530 assert_eq!(s.breaks.len(), 2);
531 assert_eq!(s.colors.len(), 1);
532 let mut warnings = Vec::new();
533 s.validate(&mut warnings);
534 assert_eq!(warnings.len(), 1);
535 }
536
537 #[test]
538 fn domain_spans_the_outer_edges() {
539 let s = scale();
540 assert_eq!(s.domain(), Some((0.0, 100.0)));
541 }
542
543 #[test]
544 fn reset_training_clears_counts_but_keeps_breaks_and_colors() {
545 let mut s = scale();
546 s.train(&[Value::Float(1.0), Value::Na]);
547 s.reset_training();
548 assert_eq!(s.counts, vec![0, 0, 0]);
549 assert_eq!(s.na_count, 0);
550 assert_eq!(s.breaks, vec![0.0, 10.0, 50.0, 100.0]);
551 }
552}