1use std::sync::Arc;
2
3use crate::aes::Aesthetic;
4use crate::data::Value;
5
6use super::format::LabelFormatter;
7use super::transform::ScaleTransform;
8use super::Scale;
9
10#[derive(Clone)]
19pub struct ScaleLinewidthContinuous {
20 name: String,
21 min: f64,
22 max: f64,
23 trained: bool,
24 limits_fixed: bool,
28 range: (f64, f64),
30 scale_transform: ScaleTransform,
31 custom_breaks: Option<Vec<f64>>,
32 custom_labels: Option<Vec<String>>,
33 label_formatter: Option<LabelFormatter>,
34 locale: Option<crate::locale::Locale>,
36}
37
38impl ScaleLinewidthContinuous {
39 pub fn new() -> Self {
40 ScaleLinewidthContinuous {
41 name: String::new(),
42 min: f64::INFINITY,
43 max: f64::NEG_INFINITY,
44 trained: false,
45 limits_fixed: false,
46 range: (1.0, 6.0),
47 scale_transform: ScaleTransform::Identity,
48 custom_breaks: None,
49 custom_labels: None,
50 label_formatter: None,
51 locale: None,
52 }
53 }
54
55 pub fn with_range(mut self, min: f64, max: f64) -> Self {
57 self.range = (min, max);
58 self
59 }
60
61 pub fn with_limits(mut self, min: f64, max: f64) -> Self {
64 self.min = min;
65 self.max = max;
66 self.trained = true;
67 self.limits_fixed = true;
68 self
69 }
70
71 pub fn with_transform(mut self, transform: ScaleTransform) -> Self {
80 self.scale_transform = transform;
81 self
82 }
83
84 pub fn with_breaks(mut self, breaks: Vec<f64>) -> Self {
86 self.custom_breaks = Some(breaks);
87 self
88 }
89
90 pub fn with_labels(mut self, labels: Vec<String>) -> Self {
92 self.custom_labels = Some(labels);
93 self
94 }
95
96 pub fn with_label_formatter<F>(mut self, f: F) -> Self
99 where
100 F: Fn(f64) -> String + Send + Sync + 'static,
101 {
102 self.label_formatter = Some(Arc::new(f));
103 self
104 }
105
106 pub fn with_name(mut self, name: &str) -> Self {
107 self.name = name.to_string();
108 self
109 }
110
111 pub fn with_locale(mut self, locale: crate::locale::Locale) -> Self {
115 self.locale = Some(locale);
116 self
117 }
118
119 fn format_label(&self, v: f64) -> String {
120 if let Some(f) = &self.label_formatter {
121 f(v)
122 } else {
123 super::util::format_number_opt(v, self.locale.as_ref())
124 }
125 }
126
127 fn domain_transform_is_broken(&self) -> bool {
136 if !self.trained || !self.min.is_finite() || !self.max.is_finite() {
137 return false;
138 }
139 let t_min = self.scale_transform.apply(self.min);
140 let t_max = self.scale_transform.apply(self.max);
141 !t_min.is_finite() || !t_max.is_finite()
142 }
143
144 fn fraction(&self, value: &Value) -> f64 {
168 let f = match value.as_f64() {
169 Some(f) => f,
170 None => return 0.0,
171 };
172 if !f.is_finite() {
173 return 0.0;
174 }
175 if !self.trained || !self.min.is_finite() || !self.max.is_finite() {
176 return 0.5;
177 }
178 if self.domain_transform_is_broken() {
179 return 0.5;
180 }
181 let (dom_lo, dom_hi) = (self.min.min(self.max), self.min.max(self.max));
182 let clamped = f.clamp(dom_lo, dom_hi);
183 let t_min = self.scale_transform.apply(self.min);
184 let t_max = self.scale_transform.apply(self.max);
185 let t_v = self.scale_transform.apply(clamped);
186 let (lo, hi) = (t_min.min(t_max), t_min.max(t_max));
187 let range = hi - lo;
188 if !t_v.is_finite() || range.abs() < f64::EPSILON {
189 return 0.5;
190 }
191 ((t_v - lo) / range).clamp(0.0, 1.0)
192 }
193}
194
195impl Default for ScaleLinewidthContinuous {
196 fn default() -> Self {
197 Self::new()
198 }
199}
200
201impl Scale for ScaleLinewidthContinuous {
202 fn set_default_locale(&mut self, locale: &crate::locale::Locale) {
203 if self.locale.is_none() {
204 self.locale = Some(locale.clone());
205 }
206 }
207
208 fn locale(&self) -> Option<&crate::locale::Locale> {
209 self.locale.as_ref()
210 }
211
212 fn aesthetic(&self) -> Aesthetic {
213 Aesthetic::Linewidth
214 }
215
216 fn train(&mut self, values: &[Value]) {
217 if self.limits_fixed {
218 self.trained = true;
219 return;
220 }
221 for v in values {
222 if let Some(f) = v.as_f64() {
223 if f.is_finite() {
224 if f < self.min {
225 self.min = f;
226 }
227 if f > self.max {
228 self.max = f;
229 }
230 }
231 }
232 }
233 self.trained = true;
234 }
235
236 fn map(&self, value: &Value) -> f64 {
237 self.fraction(value)
238 }
239
240 fn breaks(&self) -> Vec<(f64, String)> {
241 if !self.trained || self.min > self.max {
242 return vec![];
243 }
244 if let Some(ref custom) = self.custom_breaks {
245 return custom
246 .iter()
247 .enumerate()
248 .map(|(i, &v)| {
249 let pos = self.map(&Value::Float(v));
250 let label = self
251 .custom_labels
252 .as_ref()
253 .and_then(|labels| labels.get(i).cloned())
254 .unwrap_or_else(|| self.format_label(v));
255 (pos, label)
256 })
257 .collect();
258 }
259 if super::util::is_degenerate_range(self.min, self.max) {
260 return vec![(0.5, self.format_label(self.min))];
261 }
262 let n_breaks = 4;
263 let range = self.max - self.min;
264 let step = super::util::nice_step(range / n_breaks as f64);
265 let start = (self.min / step).ceil() * step;
266 let values = super::util::stepped_breaks(start, self.max, step);
267 values
268 .iter()
269 .map(|&v| (self.map(&Value::Float(v)), self.format_label(v)))
270 .collect()
271 }
272
273 fn name(&self) -> &str {
274 &self.name
275 }
276
277 fn set_name(&mut self, name: &str) {
278 self.name = name.to_string();
279 }
280
281 fn map_to_size(&self, value: &Value) -> Option<f64> {
282 let f = value.as_f64()?;
287 if !f.is_finite() {
288 return None;
289 }
290 if self.domain_transform_is_broken() {
296 return None;
297 }
298 let t = self.fraction(value);
299 let (lo, hi) = self.range;
300 Some(lo + t * (hi - lo))
301 }
302
303 fn map_fraction_to_size(&self, fraction: f64) -> Option<f64> {
304 let (lo, hi) = self.range;
305 Some(lo + fraction.clamp(0.0, 1.0) * (hi - lo))
306 }
307
308 fn domain(&self) -> Option<(f64, f64)> {
309 if self.trained {
310 Some((self.min, self.max))
311 } else {
312 None
313 }
314 }
315
316 fn clone_box(&self) -> Box<dyn Scale> {
317 Box::new(self.clone())
318 }
319
320 fn reset_training(&mut self) {
321 if !self.limits_fixed {
322 self.min = f64::INFINITY;
323 self.max = f64::NEG_INFINITY;
324 self.trained = false;
325 }
326 }
327
328 fn validate(&self, warnings: &mut Vec<String>) {
329 if self.domain_transform_is_broken() {
330 warnings.push(format!(
331 "scale_linewidth: {:?} is undefined at the domain limits ({}, {}); \
332 every row falls back to the geom's own linewidth instead of a mapped size \
333 (ggplot2's equivalent: NA plus a warning)",
334 self.scale_transform, self.min, self.max
335 ));
336 }
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[test]
345 fn range_transform_and_limits_match_the_documented_formula() {
346 let vmax = 10.0;
347 let scale = ScaleLinewidthContinuous::new()
348 .with_range(0.75, 8.0)
349 .with_transform(ScaleTransform::Sqrt)
350 .with_limits(0.0, vmax);
351 for v in [0.0, 2.5, 5.0, 7.5, 10.0] {
352 let width = scale.map_to_size(&Value::Float(v)).unwrap();
353 let expected = 0.75 + 7.25 * (v / vmax).sqrt();
354 assert!(
355 (width - expected).abs() < 1e-9,
356 "v={v}: width={width}, expected={expected}"
357 );
358 }
359 }
360
361 #[test]
362 fn limits_squish_out_of_range_values() {
363 let scale = ScaleLinewidthContinuous::new()
364 .with_range(1.0, 6.0)
365 .with_limits(0.0, 10.0);
366 assert_eq!(scale.map_to_size(&Value::Float(-5.0)), Some(1.0));
367 assert_eq!(scale.map_to_size(&Value::Float(50.0)), Some(6.0));
368 }
369
370 #[test]
371 fn limits_fixed_domain_is_not_widened_by_training() {
372 let mut scale = ScaleLinewidthContinuous::new().with_limits(0.0, 10.0);
373 scale.train(&[Value::Float(-100.0), Value::Float(100.0)]);
374 assert_eq!(scale.domain(), Some((0.0, 10.0)));
375 }
376
377 #[test]
378 fn default_range_is_1_to_6() {
379 let mut scale = ScaleLinewidthContinuous::new();
380 scale.train(&[Value::Float(0.0), Value::Float(10.0)]);
381 assert_eq!(scale.map_to_size(&Value::Float(0.0)), Some(1.0));
382 assert_eq!(scale.map_to_size(&Value::Float(10.0)), Some(6.0));
383 }
384
385 #[test]
391 fn log10_transform_with_a_nonzero_lower_limit() {
392 let scale = ScaleLinewidthContinuous::new()
393 .with_range(1.0, 6.0)
394 .with_transform(ScaleTransform::Log10)
395 .with_limits(1.0, 100.0);
396 let cases = [(1.0, 1.0), (10.0, 3.5), (100.0, 6.0)];
398 for (v, expected) in cases {
399 let width = scale.map_to_size(&Value::Float(v)).unwrap();
400 assert!(
401 (width - expected).abs() < 1e-9,
402 "v={v}: width={width}, expected={expected}"
403 );
404 }
405 }
406
407 #[test]
408 fn reverse_transform_flips_which_end_gets_the_larger_width() {
409 let scale = ScaleLinewidthContinuous::new()
410 .with_range(1.0, 6.0)
411 .with_transform(ScaleTransform::Reverse)
412 .with_limits(1.0, 100.0);
413 let min_width = scale.map_to_size(&Value::Float(1.0)).unwrap();
415 let max_width = scale.map_to_size(&Value::Float(100.0)).unwrap();
416 assert!((min_width - 6.0).abs() < 1e-9, "min_width={min_width}");
417 assert!((max_width - 1.0).abs() < 1e-9, "max_width={max_width}");
418 }
419
420 #[test]
421 fn sqrt_transform_with_a_nonzero_lower_limit() {
422 let scale = ScaleLinewidthContinuous::new()
423 .with_range(1.0, 6.0)
424 .with_transform(ScaleTransform::Sqrt)
425 .with_limits(4.0, 100.0);
426 let cases = [(4.0, 1.0), (16.0, 2.25), (100.0, 6.0)];
428 for (v, expected) in cases {
429 let width = scale.map_to_size(&Value::Float(v)).unwrap();
430 assert!(
431 (width - expected).abs() < 1e-9,
432 "v={v}: width={width}, expected={expected}"
433 );
434 }
435 }
436
437 #[test]
442 fn na_or_non_finite_input_returns_none_not_the_minimum() {
443 let scale = ScaleLinewidthContinuous::new()
444 .with_range(1.0, 6.0)
445 .with_limits(0.0, 10.0);
446 assert_eq!(scale.map_to_size(&Value::Na), None);
447 assert_eq!(scale.map_to_size(&Value::Str("x".into())), None);
448 assert_eq!(scale.map_to_size(&Value::Float(f64::NAN)), None);
449 assert_eq!(scale.map_to_size(&Value::Float(f64::INFINITY)), None);
450 }
451
452 #[test]
460 fn log10_with_a_zero_lower_limit_maps_every_value_to_none_and_warns() {
461 let scale = ScaleLinewidthContinuous::new()
462 .with_range(1.0, 6.0)
463 .with_transform(ScaleTransform::Log10)
464 .with_limits(0.0, 100.0);
465 for v in [0.0, 1.0, 50.0, 100.0] {
466 assert_eq!(
467 scale.map_to_size(&Value::Float(v)),
468 None,
469 "v={v} should map to None, not the midpoint width"
470 );
471 }
472 let mut warnings = Vec::new();
473 scale.validate(&mut warnings);
474 assert_eq!(warnings.len(), 1, "{warnings:?}");
475 assert!(warnings[0].contains("Log10"));
476 }
477
478 #[test]
479 fn a_value_trained_to_zero_breaks_log10_the_same_way() {
480 let mut scale = ScaleLinewidthContinuous::new()
483 .with_range(1.0, 6.0)
484 .with_transform(ScaleTransform::Log10);
485 scale.train(&[Value::Float(0.0), Value::Float(100.0)]);
486 assert_eq!(scale.map_to_size(&Value::Float(50.0)), None);
487 let mut warnings = Vec::new();
488 scale.validate(&mut warnings);
489 assert_eq!(warnings.len(), 1);
490 }
491
492 #[test]
493 fn out_of_limits_value_squishes_to_the_nearest_end_even_under_log10() {
494 let scale = ScaleLinewidthContinuous::new()
497 .with_range(1.0, 6.0)
498 .with_transform(ScaleTransform::Log10)
499 .with_limits(1.0, 100.0);
500 assert_eq!(scale.map_to_size(&Value::Float(0.0)), Some(1.0));
501 assert_eq!(scale.map_to_size(&Value::Float(-50.0)), Some(1.0));
502 assert_eq!(scale.map_to_size(&Value::Float(1000.0)), Some(6.0));
503
504 let mut warnings = Vec::new();
505 scale.validate(&mut warnings);
506 assert!(
507 warnings.is_empty(),
508 "a fine domain shouldn't warn: {warnings:?}"
509 );
510 }
511
512 #[test]
513 fn fraction_stays_finite_even_when_the_domain_transform_is_broken() {
514 let scale = ScaleLinewidthContinuous::new()
516 .with_transform(ScaleTransform::Log10)
517 .with_limits(0.0, 100.0);
518 let t = scale.map(&Value::Float(50.0));
519 assert!(t.is_finite());
520 }
521
522 #[test]
525 fn broken_domain_transform_warning_reaches_the_plot_warnings() {
526 use crate::prelude::*;
527
528 let (_svg, warnings) = GGPlot::new(vec![
529 ("x".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
530 ("y".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
531 ("xend".into(), vec![Value::Float(0.5), Value::Float(1.5)]),
532 ("yend".into(), vec![Value::Float(0.5), Value::Float(1.5)]),
533 ("size".into(), vec![Value::Float(0.0), Value::Float(100.0)]),
534 ])
535 .aes(
536 Aes::new()
537 .x("x")
538 .y("y")
539 .xend("xend")
540 .yend("yend")
541 .linewidth("size"),
542 )
543 .geom_segment()
544 .scale_linewidth(ScaleLinewidthContinuous::new().with_transform(ScaleTransform::Log10))
545 .render_svg_native_with_warnings(200, 200)
546 .unwrap();
547
548 assert!(warnings.iter().any(|w| w.contains("Log10")), "{warnings:?}");
549 }
550
551 #[test]
554 fn with_labels_overrides_the_formatted_label_for_each_break() {
555 let scale = ScaleLinewidthContinuous::new()
556 .with_name("flow size")
557 .with_breaks(vec![1.0, 2.0])
558 .with_labels(vec!["small".into(), "big".into()])
559 .with_limits(0.0, 10.0);
560 assert_eq!(scale.name(), "flow size");
561 let breaks = scale.breaks();
562 let labels: Vec<&str> = breaks.iter().map(|(_, l)| l.as_str()).collect();
563 assert_eq!(labels, vec!["small", "big"]);
564 }
565
566 #[test]
567 fn with_labels_shorter_than_breaks_falls_back_to_the_formatted_number() {
568 let scale = ScaleLinewidthContinuous::new()
571 .with_breaks(vec![1.0, 2.5])
572 .with_labels(vec!["small".into()])
573 .with_limits(0.0, 10.0);
574 let breaks = scale.breaks();
575 assert_eq!(breaks[0].1, "small");
576 assert_eq!(breaks[1].1, "2.5");
577 }
578
579 #[test]
580 fn with_label_formatter_formats_every_break() {
581 let scale = ScaleLinewidthContinuous::new()
582 .with_breaks(vec![1.0, 2.0])
583 .with_label_formatter(|v| format!("{v:.1}mm"))
584 .with_limits(0.0, 10.0);
585 let labels: Vec<String> = scale.breaks().into_iter().map(|(_, l)| l).collect();
586 assert_eq!(labels, vec!["1.0mm", "2.0mm"]);
587 }
588
589 #[test]
590 fn with_locale_formats_default_break_labels() {
591 let mut scale = ScaleLinewidthContinuous::new().with_locale(crate::locale::Locale::de_de());
592 scale.train(&[Value::Float(1000.0), Value::Float(2000.0)]);
593 let breaks = scale.breaks();
594 assert!(!breaks.is_empty());
595 assert!(breaks.iter().any(|(_, l)| l.contains('.')), "{breaks:?}");
598 }
599
600 #[test]
601 fn set_default_locale_only_applies_when_no_locale_was_set() {
602 let mut with_own =
603 ScaleLinewidthContinuous::new().with_locale(crate::locale::Locale::de_de());
604 with_own.set_default_locale(&crate::locale::Locale::en_us());
605 assert_eq!(
606 with_own.locale().unwrap().tag,
607 crate::locale::Locale::de_de().tag
608 );
609
610 let mut without_own = ScaleLinewidthContinuous::new();
611 assert!(without_own.locale().is_none());
612 without_own.set_default_locale(&crate::locale::Locale::en_us());
613 assert_eq!(
614 without_own.locale().unwrap().tag,
615 crate::locale::Locale::en_us().tag
616 );
617 }
618
619 #[test]
620 fn degenerate_range_gives_a_single_break_at_the_midpoint() {
621 let mut scale = ScaleLinewidthContinuous::new();
622 scale.train(&[Value::Float(5.0), Value::Float(5.0)]);
623 let breaks = scale.breaks();
624 assert_eq!(breaks.len(), 1);
625 assert_eq!(breaks[0].0, 0.5);
626 assert_eq!(breaks[0].1, "5");
627 }
628
629 #[test]
630 fn untrained_scale_has_no_breaks() {
631 let scale = ScaleLinewidthContinuous::new();
632 assert!(scale.breaks().is_empty());
633 }
634
635 #[test]
636 fn clone_box_preserves_configuration() {
637 let scale = ScaleLinewidthContinuous::new()
638 .with_range(2.0, 9.0)
639 .with_name("w")
640 .with_limits(0.0, 5.0);
641 let cloned = scale.clone_box();
642 assert_eq!(cloned.name(), "w");
643 assert_eq!(
644 cloned.map_to_size(&Value::Float(5.0)),
645 scale.map_to_size(&Value::Float(5.0))
646 );
647 }
648
649 #[test]
650 fn reset_training_clears_an_auto_trained_domain_but_not_fixed_limits() {
651 let mut auto = ScaleLinewidthContinuous::new();
652 auto.train(&[Value::Float(1.0), Value::Float(9.0)]);
653 assert_eq!(auto.domain(), Some((1.0, 9.0)));
654 auto.reset_training();
655 assert_eq!(auto.domain(), None);
656
657 let mut fixed = ScaleLinewidthContinuous::new().with_limits(0.0, 10.0);
658 fixed.reset_training();
659 assert_eq!(fixed.domain(), Some((0.0, 10.0)));
660 }
661
662 #[test]
663 fn default_trait_impl_matches_new() {
664 let default_scale = ScaleLinewidthContinuous::default();
665 let new_scale = ScaleLinewidthContinuous::new();
666 assert_eq!(default_scale.name(), new_scale.name());
667 assert_eq!(default_scale.breaks(), new_scale.breaks());
668 }
669
670 #[test]
671 fn set_name_overrides_the_axis_legend_title() {
672 let mut scale = ScaleLinewidthContinuous::new();
673 scale.set_name("custom title");
674 assert_eq!(scale.name(), "custom title");
675 }
676
677 #[test]
683 fn map_handles_na_non_finite_and_untrained_without_panicking() {
684 let untrained = ScaleLinewidthContinuous::new();
685 assert_eq!(untrained.map(&Value::Float(5.0)), 0.5);
686
687 let trained = ScaleLinewidthContinuous::new().with_limits(0.0, 10.0);
688 assert_eq!(trained.map(&Value::Na), 0.0);
689 assert_eq!(trained.map(&Value::Str("x".into())), 0.0);
690 assert_eq!(trained.map(&Value::Float(f64::NAN)), 0.0);
691 }
692}