Skip to main content

ggplot_rs/scale/
format.rs

1//! Label formatting functions for scale breaks.
2//! Analogous to R's `scales` package (comma, percent, dollar, scientific).
3
4/// Format with comma separators for thousands (e.g., 1,234,567).
5pub fn label_comma(v: f64) -> String {
6    if v == v.round() && v.abs() < 1e15 {
7        let s = format!("{}", v as i64);
8        add_commas(&s)
9    } else {
10        let s = format!("{:.2}", v);
11        let s = s.trim_end_matches('0').trim_end_matches('.');
12        if let Some((int_part, dec_part)) = s.split_once('.') {
13            format!("{}.{}", add_commas(int_part), dec_part)
14        } else {
15            add_commas(s)
16        }
17    }
18}
19
20/// Format as percentage (e.g., 0.5 → "50%").
21pub fn label_percent(v: f64) -> String {
22    let pct = v * 100.0;
23    if (pct - pct.round()).abs() < 1e-10 {
24        format!("{}%", pct.round() as i64)
25    } else {
26        format!("{:.1}%", pct)
27    }
28}
29
30/// Format as US dollar (e.g., 1234.5 → "$1,235").
31pub fn label_dollar(v: f64) -> String {
32    if v < 0.0 {
33        format!("-${}", label_comma(-v))
34    } else {
35        format!("${}", label_comma(v))
36    }
37}
38
39/// Format in scientific notation (e.g., 12345 → "1.23e4").
40pub fn label_scientific(v: f64) -> String {
41    if v == 0.0 {
42        return "0".to_string();
43    }
44    let exp = v.abs().log10().floor() as i32;
45    let mantissa = v / 10f64.powi(exp);
46    if (mantissa - mantissa.round()).abs() < 1e-10 {
47        format!("{}e{}", mantissa.round() as i64, exp)
48    } else {
49        let s = format!("{:.2}e{}", mantissa, exp);
50        // Trim trailing zeros in mantissa
51        if let Some((m, e)) = s.split_once('e') {
52            let m = m.trim_end_matches('0').trim_end_matches('.');
53            format!("{m}e{e}")
54        } else {
55            s
56        }
57    }
58}
59
60fn add_commas(s: &str) -> String {
61    let negative = s.starts_with('-');
62    let digits = if negative { &s[1..] } else { s };
63    let mut result = String::new();
64    for (i, ch) in digits.chars().rev().enumerate() {
65        if i > 0 && i % 3 == 0 {
66            result.push(',');
67        }
68        result.push(ch);
69    }
70    let formatted: String = result.chars().rev().collect();
71    if negative {
72        format!("-{formatted}")
73    } else {
74        formatted
75    }
76}
77
78/// A label formatter function type.
79pub type LabelFormatter = fn(f64) -> String;
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_label_comma() {
87        assert_eq!(label_comma(1000.0), "1,000");
88        assert_eq!(label_comma(1234567.0), "1,234,567");
89        assert_eq!(label_comma(42.0), "42");
90        assert_eq!(label_comma(-5000.0), "-5,000");
91    }
92
93    #[test]
94    fn test_label_percent() {
95        assert_eq!(label_percent(0.5), "50%");
96        assert_eq!(label_percent(0.0), "0%");
97        assert_eq!(label_percent(1.0), "100%");
98        assert_eq!(label_percent(0.123), "12.3%");
99    }
100
101    #[test]
102    fn test_label_dollar() {
103        assert_eq!(label_dollar(1000.0), "$1,000");
104        assert_eq!(label_dollar(0.0), "$0");
105        assert_eq!(label_dollar(-500.0), "-$500");
106    }
107
108    #[test]
109    fn test_label_scientific() {
110        assert_eq!(label_scientific(12345.0), "1.23e4");
111        assert_eq!(label_scientific(0.0), "0");
112        assert_eq!(label_scientific(100.0), "1e2");
113    }
114}