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 format_value(&self, value: &Value) -> Option<String> {
213 let f = value.as_f64()?;
214 if !f.is_finite() {
215 return None;
216 }
217 Some(self.format_label(f))
218 }
219
220 fn aesthetic(&self) -> Aesthetic {
221 Aesthetic::Linewidth
222 }
223
224 fn train(&mut self, values: &[Value]) {
225 if self.limits_fixed {
226 self.trained = true;
227 return;
228 }
229 for v in values {
230 if let Some(f) = v.as_f64() {
231 if f.is_finite() {
232 if f < self.min {
233 self.min = f;
234 }
235 if f > self.max {
236 self.max = f;
237 }
238 }
239 }
240 }
241 self.trained = true;
242 }
243
244 fn map(&self, value: &Value) -> f64 {
245 self.fraction(value)
246 }
247
248 fn breaks(&self) -> Vec<(f64, String)> {
249 if !self.trained || self.min > self.max {
250 return vec![];
251 }
252 if let Some(ref custom) = self.custom_breaks {
253 return custom
254 .iter()
255 .enumerate()
256 .map(|(i, &v)| {
257 let pos = self.map(&Value::Float(v));
258 let label = self
259 .custom_labels
260 .as_ref()
261 .and_then(|labels| labels.get(i).cloned())
262 .unwrap_or_else(|| self.format_label(v));
263 (pos, label)
264 })
265 .collect();
266 }
267 if super::util::is_degenerate_range(self.min, self.max) {
268 return vec![(0.5, self.format_label(self.min))];
269 }
270 let n_breaks = 4;
271 let range = self.max - self.min;
272 let step = super::util::nice_step(range / n_breaks as f64);
273 let start = (self.min / step).ceil() * step;
274 let values = super::util::stepped_breaks(start, self.max, step);
275 values
276 .iter()
277 .map(|&v| (self.map(&Value::Float(v)), self.format_label(v)))
278 .collect()
279 }
280
281 fn name(&self) -> &str {
282 &self.name
283 }
284
285 fn set_name(&mut self, name: &str) {
286 self.name = name.to_string();
287 }
288
289 fn map_to_size(&self, value: &Value) -> Option<f64> {
290 let f = value.as_f64()?;
295 if !f.is_finite() {
296 return None;
297 }
298 if self.domain_transform_is_broken() {
304 return None;
305 }
306 let t = self.fraction(value);
307 let (lo, hi) = self.range;
308 Some(lo + t * (hi - lo))
309 }
310
311 fn map_fraction_to_size(&self, fraction: f64) -> Option<f64> {
312 let (lo, hi) = self.range;
313 Some(lo + fraction.clamp(0.0, 1.0) * (hi - lo))
314 }
315
316 fn domain(&self) -> Option<(f64, f64)> {
317 if self.trained {
318 Some((self.min, self.max))
319 } else {
320 None
321 }
322 }
323
324 fn clone_box(&self) -> Box<dyn Scale> {
325 Box::new(self.clone())
326 }
327
328 fn reset_training(&mut self) {
329 if !self.limits_fixed {
330 self.min = f64::INFINITY;
331 self.max = f64::NEG_INFINITY;
332 self.trained = false;
333 }
334 }
335
336 fn validate(&self, warnings: &mut Vec<String>) {
337 if self.domain_transform_is_broken() {
338 warnings.push(format!(
339 "scale_linewidth: {:?} is undefined at the domain limits ({}, {}); \
340 every row falls back to the geom's own linewidth instead of a mapped size \
341 (ggplot2's equivalent: NA plus a warning)",
342 self.scale_transform, self.min, self.max
343 ));
344 }
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn format_value_falls_back_to_format_label_and_rejects_non_finite() {
354 let s = ScaleLinewidthContinuous::new().with_label_formatter(|v| format!("{v:.1}mm"));
355 assert_eq!(
356 s.format_value(&Value::Float(2.0)),
357 Some("2.0mm".to_string())
358 );
359 assert_eq!(s.format_value(&Value::Float(f64::NAN)), None);
360 }
361
362 #[test]
363 fn range_transform_and_limits_match_the_documented_formula() {
364 let vmax = 10.0;
365 let scale = ScaleLinewidthContinuous::new()
366 .with_range(0.75, 8.0)
367 .with_transform(ScaleTransform::Sqrt)
368 .with_limits(0.0, vmax);
369 for v in [0.0, 2.5, 5.0, 7.5, 10.0] {
370 let width = scale.map_to_size(&Value::Float(v)).unwrap();
371 let expected = 0.75 + 7.25 * (v / vmax).sqrt();
372 assert!(
373 (width - expected).abs() < 1e-9,
374 "v={v}: width={width}, expected={expected}"
375 );
376 }
377 }
378
379 #[test]
380 fn limits_squish_out_of_range_values() {
381 let scale = ScaleLinewidthContinuous::new()
382 .with_range(1.0, 6.0)
383 .with_limits(0.0, 10.0);
384 assert_eq!(scale.map_to_size(&Value::Float(-5.0)), Some(1.0));
385 assert_eq!(scale.map_to_size(&Value::Float(50.0)), Some(6.0));
386 }
387
388 #[test]
389 fn limits_fixed_domain_is_not_widened_by_training() {
390 let mut scale = ScaleLinewidthContinuous::new().with_limits(0.0, 10.0);
391 scale.train(&[Value::Float(-100.0), Value::Float(100.0)]);
392 assert_eq!(scale.domain(), Some((0.0, 10.0)));
393 }
394
395 #[test]
396 fn default_range_is_1_to_6() {
397 let mut scale = ScaleLinewidthContinuous::new();
398 scale.train(&[Value::Float(0.0), Value::Float(10.0)]);
399 assert_eq!(scale.map_to_size(&Value::Float(0.0)), Some(1.0));
400 assert_eq!(scale.map_to_size(&Value::Float(10.0)), Some(6.0));
401 }
402
403 #[test]
409 fn log10_transform_with_a_nonzero_lower_limit() {
410 let scale = ScaleLinewidthContinuous::new()
411 .with_range(1.0, 6.0)
412 .with_transform(ScaleTransform::Log10)
413 .with_limits(1.0, 100.0);
414 let cases = [(1.0, 1.0), (10.0, 3.5), (100.0, 6.0)];
416 for (v, expected) in cases {
417 let width = scale.map_to_size(&Value::Float(v)).unwrap();
418 assert!(
419 (width - expected).abs() < 1e-9,
420 "v={v}: width={width}, expected={expected}"
421 );
422 }
423 }
424
425 #[test]
426 fn reverse_transform_flips_which_end_gets_the_larger_width() {
427 let scale = ScaleLinewidthContinuous::new()
428 .with_range(1.0, 6.0)
429 .with_transform(ScaleTransform::Reverse)
430 .with_limits(1.0, 100.0);
431 let min_width = scale.map_to_size(&Value::Float(1.0)).unwrap();
433 let max_width = scale.map_to_size(&Value::Float(100.0)).unwrap();
434 assert!((min_width - 6.0).abs() < 1e-9, "min_width={min_width}");
435 assert!((max_width - 1.0).abs() < 1e-9, "max_width={max_width}");
436 }
437
438 #[test]
439 fn sqrt_transform_with_a_nonzero_lower_limit() {
440 let scale = ScaleLinewidthContinuous::new()
441 .with_range(1.0, 6.0)
442 .with_transform(ScaleTransform::Sqrt)
443 .with_limits(4.0, 100.0);
444 let cases = [(4.0, 1.0), (16.0, 2.25), (100.0, 6.0)];
446 for (v, expected) in cases {
447 let width = scale.map_to_size(&Value::Float(v)).unwrap();
448 assert!(
449 (width - expected).abs() < 1e-9,
450 "v={v}: width={width}, expected={expected}"
451 );
452 }
453 }
454
455 #[test]
460 fn na_or_non_finite_input_returns_none_not_the_minimum() {
461 let scale = ScaleLinewidthContinuous::new()
462 .with_range(1.0, 6.0)
463 .with_limits(0.0, 10.0);
464 assert_eq!(scale.map_to_size(&Value::Na), None);
465 assert_eq!(scale.map_to_size(&Value::Str("x".into())), None);
466 assert_eq!(scale.map_to_size(&Value::Float(f64::NAN)), None);
467 assert_eq!(scale.map_to_size(&Value::Float(f64::INFINITY)), None);
468 }
469
470 #[test]
478 fn log10_with_a_zero_lower_limit_maps_every_value_to_none_and_warns() {
479 let scale = ScaleLinewidthContinuous::new()
480 .with_range(1.0, 6.0)
481 .with_transform(ScaleTransform::Log10)
482 .with_limits(0.0, 100.0);
483 for v in [0.0, 1.0, 50.0, 100.0] {
484 assert_eq!(
485 scale.map_to_size(&Value::Float(v)),
486 None,
487 "v={v} should map to None, not the midpoint width"
488 );
489 }
490 let mut warnings = Vec::new();
491 scale.validate(&mut warnings);
492 assert_eq!(warnings.len(), 1, "{warnings:?}");
493 assert!(warnings[0].contains("Log10"));
494 }
495
496 #[test]
497 fn a_value_trained_to_zero_breaks_log10_the_same_way() {
498 let mut scale = ScaleLinewidthContinuous::new()
501 .with_range(1.0, 6.0)
502 .with_transform(ScaleTransform::Log10);
503 scale.train(&[Value::Float(0.0), Value::Float(100.0)]);
504 assert_eq!(scale.map_to_size(&Value::Float(50.0)), None);
505 let mut warnings = Vec::new();
506 scale.validate(&mut warnings);
507 assert_eq!(warnings.len(), 1);
508 }
509
510 #[test]
511 fn out_of_limits_value_squishes_to_the_nearest_end_even_under_log10() {
512 let scale = ScaleLinewidthContinuous::new()
515 .with_range(1.0, 6.0)
516 .with_transform(ScaleTransform::Log10)
517 .with_limits(1.0, 100.0);
518 assert_eq!(scale.map_to_size(&Value::Float(0.0)), Some(1.0));
519 assert_eq!(scale.map_to_size(&Value::Float(-50.0)), Some(1.0));
520 assert_eq!(scale.map_to_size(&Value::Float(1000.0)), Some(6.0));
521
522 let mut warnings = Vec::new();
523 scale.validate(&mut warnings);
524 assert!(
525 warnings.is_empty(),
526 "a fine domain shouldn't warn: {warnings:?}"
527 );
528 }
529
530 #[test]
531 fn fraction_stays_finite_even_when_the_domain_transform_is_broken() {
532 let scale = ScaleLinewidthContinuous::new()
534 .with_transform(ScaleTransform::Log10)
535 .with_limits(0.0, 100.0);
536 let t = scale.map(&Value::Float(50.0));
537 assert!(t.is_finite());
538 }
539
540 #[test]
543 fn broken_domain_transform_warning_reaches_the_plot_warnings() {
544 use crate::prelude::*;
545
546 let (_svg, warnings) = GGPlot::new(vec![
547 ("x".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
548 ("y".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
549 ("xend".into(), vec![Value::Float(0.5), Value::Float(1.5)]),
550 ("yend".into(), vec![Value::Float(0.5), Value::Float(1.5)]),
551 ("size".into(), vec![Value::Float(0.0), Value::Float(100.0)]),
552 ])
553 .aes(
554 Aes::new()
555 .x("x")
556 .y("y")
557 .xend("xend")
558 .yend("yend")
559 .linewidth("size"),
560 )
561 .geom_segment()
562 .scale_linewidth(ScaleLinewidthContinuous::new().with_transform(ScaleTransform::Log10))
563 .render_svg_native_with_warnings(200, 200)
564 .unwrap();
565
566 assert!(warnings.iter().any(|w| w.contains("Log10")), "{warnings:?}");
567 }
568
569 #[test]
572 fn with_labels_overrides_the_formatted_label_for_each_break() {
573 let scale = ScaleLinewidthContinuous::new()
574 .with_name("flow size")
575 .with_breaks(vec![1.0, 2.0])
576 .with_labels(vec!["small".into(), "big".into()])
577 .with_limits(0.0, 10.0);
578 assert_eq!(scale.name(), "flow size");
579 let breaks = scale.breaks();
580 let labels: Vec<&str> = breaks.iter().map(|(_, l)| l.as_str()).collect();
581 assert_eq!(labels, vec!["small", "big"]);
582 }
583
584 #[test]
585 fn with_labels_shorter_than_breaks_falls_back_to_the_formatted_number() {
586 let scale = ScaleLinewidthContinuous::new()
589 .with_breaks(vec![1.0, 2.5])
590 .with_labels(vec!["small".into()])
591 .with_limits(0.0, 10.0);
592 let breaks = scale.breaks();
593 assert_eq!(breaks[0].1, "small");
594 assert_eq!(breaks[1].1, "2.5");
595 }
596
597 #[test]
598 fn with_label_formatter_formats_every_break() {
599 let scale = ScaleLinewidthContinuous::new()
600 .with_breaks(vec![1.0, 2.0])
601 .with_label_formatter(|v| format!("{v:.1}mm"))
602 .with_limits(0.0, 10.0);
603 let labels: Vec<String> = scale.breaks().into_iter().map(|(_, l)| l).collect();
604 assert_eq!(labels, vec!["1.0mm", "2.0mm"]);
605 }
606
607 #[test]
608 fn with_locale_formats_default_break_labels() {
609 let mut scale = ScaleLinewidthContinuous::new().with_locale(crate::locale::Locale::de_de());
610 scale.train(&[Value::Float(1000.0), Value::Float(2000.0)]);
611 let breaks = scale.breaks();
612 assert!(!breaks.is_empty());
613 assert!(breaks.iter().any(|(_, l)| l.contains('.')), "{breaks:?}");
616 }
617
618 #[test]
619 fn set_default_locale_only_applies_when_no_locale_was_set() {
620 let mut with_own =
621 ScaleLinewidthContinuous::new().with_locale(crate::locale::Locale::de_de());
622 with_own.set_default_locale(&crate::locale::Locale::en_us());
623 assert_eq!(
624 with_own.locale().unwrap().tag,
625 crate::locale::Locale::de_de().tag
626 );
627
628 let mut without_own = ScaleLinewidthContinuous::new();
629 assert!(without_own.locale().is_none());
630 without_own.set_default_locale(&crate::locale::Locale::en_us());
631 assert_eq!(
632 without_own.locale().unwrap().tag,
633 crate::locale::Locale::en_us().tag
634 );
635 }
636
637 #[test]
638 fn degenerate_range_gives_a_single_break_at_the_midpoint() {
639 let mut scale = ScaleLinewidthContinuous::new();
640 scale.train(&[Value::Float(5.0), Value::Float(5.0)]);
641 let breaks = scale.breaks();
642 assert_eq!(breaks.len(), 1);
643 assert_eq!(breaks[0].0, 0.5);
644 assert_eq!(breaks[0].1, "5");
645 }
646
647 #[test]
648 fn untrained_scale_has_no_breaks() {
649 let scale = ScaleLinewidthContinuous::new();
650 assert!(scale.breaks().is_empty());
651 }
652
653 #[test]
654 fn clone_box_preserves_configuration() {
655 let scale = ScaleLinewidthContinuous::new()
656 .with_range(2.0, 9.0)
657 .with_name("w")
658 .with_limits(0.0, 5.0);
659 let cloned = scale.clone_box();
660 assert_eq!(cloned.name(), "w");
661 assert_eq!(
662 cloned.map_to_size(&Value::Float(5.0)),
663 scale.map_to_size(&Value::Float(5.0))
664 );
665 }
666
667 #[test]
668 fn reset_training_clears_an_auto_trained_domain_but_not_fixed_limits() {
669 let mut auto = ScaleLinewidthContinuous::new();
670 auto.train(&[Value::Float(1.0), Value::Float(9.0)]);
671 assert_eq!(auto.domain(), Some((1.0, 9.0)));
672 auto.reset_training();
673 assert_eq!(auto.domain(), None);
674
675 let mut fixed = ScaleLinewidthContinuous::new().with_limits(0.0, 10.0);
676 fixed.reset_training();
677 assert_eq!(fixed.domain(), Some((0.0, 10.0)));
678 }
679
680 #[test]
681 fn default_trait_impl_matches_new() {
682 let default_scale = ScaleLinewidthContinuous::default();
683 let new_scale = ScaleLinewidthContinuous::new();
684 assert_eq!(default_scale.name(), new_scale.name());
685 assert_eq!(default_scale.breaks(), new_scale.breaks());
686 }
687
688 #[test]
689 fn set_name_overrides_the_axis_legend_title() {
690 let mut scale = ScaleLinewidthContinuous::new();
691 scale.set_name("custom title");
692 assert_eq!(scale.name(), "custom title");
693 }
694
695 #[test]
701 fn map_handles_na_non_finite_and_untrained_without_panicking() {
702 let untrained = ScaleLinewidthContinuous::new();
703 assert_eq!(untrained.map(&Value::Float(5.0)), 0.5);
704
705 let trained = ScaleLinewidthContinuous::new().with_limits(0.0, 10.0);
706 assert_eq!(trained.map(&Value::Na), 0.0);
707 assert_eq!(trained.map(&Value::Str("x".into())), 0.0);
708 assert_eq!(trained.map(&Value::Float(f64::NAN)), 0.0);
709 }
710}