Skip to main content

ggplot_rs/
format.rs

1//! Public value and label formatting helpers.
2//!
3//! [`format_value`] renders a single data [`Value`] the way ggplot-rs shows it
4//! in hover tooltips (rounded numbers, calendar dates for datetimes). The
5//! `label_*` axis/legend formatters from [`crate::scale::format`] are
6//! re-exported here so hosts can format numbers consistently with the plot.
7
8use crate::data::Value;
9
10pub use crate::locale::{CurrencyPosition, Locale};
11pub use crate::scale::format::{
12    label_bytes, label_comma, label_comma_locale, label_comma_marks, label_currency,
13    label_currency_locale, label_currency_marks, label_dollar, label_number, label_number_locale,
14    label_number_marks, label_ordinal, label_percent, label_percent_locale, label_percent_marks,
15    label_scientific, label_si, LabelFormatter,
16};
17
18/// Format one data value for display (tooltips, data labels):
19///
20/// - strings as-is, booleans as `true`/`false`, `NA` as the empty string;
21/// - numbers rounded to three decimals with trailing zeros dropped
22///   (`3.14159` → `"3.142"`, `2.0` → `"2"`);
23/// - datetimes (epoch seconds) as `YYYY-MM-DD[ HH:MM:SS]` UTC.
24///
25/// ```
26/// use ggplot_rs::data::Value;
27/// use ggplot_rs::format::format_value;
28/// assert_eq!(format_value(&Value::Float(3.14159)), "3.142");
29/// assert_eq!(format_value(&Value::DateTime(86_400)), "1970-01-02");
30/// assert_eq!(format_value(&Value::Na), "");
31/// ```
32pub fn format_value(v: &Value) -> String {
33    match v {
34        Value::Str(s) => s.clone(),
35        Value::Bool(b) => b.to_string(),
36        Value::Na => String::new(),
37        // A datetime axis reads as a calendar date, not raw epoch seconds.
38        Value::DateTime(secs) => crate::data::format_epoch_secs(*secs),
39        _ => v
40            .as_f64()
41            .map(|f| format!("{}", (f * 1000.0).round() / 1000.0))
42            .unwrap_or_default(),
43    }
44}
45
46/// [`format_value`] in the conventions of `locale`: numbers get its decimal
47/// and thousands marks, datetimes its date format and month names.
48///
49/// ```
50/// use ggplot_rs::data::Value;
51/// use ggplot_rs::format::{format_value_locale, Locale};
52/// let de = Locale::de_de();
53/// assert_eq!(format_value_locale(&Value::Float(1234.56789), &de), "1.234,568");
54/// assert_eq!(format_value_locale(&Value::DateTime(1_792_022_400), &de), "15. Okt. 2026");
55/// ```
56pub fn format_value_locale(v: &Value, locale: &Locale) -> String {
57    match v {
58        Value::DateTime(secs) => locale.format_datetime(*secs),
59        Value::Str(_) | Value::Bool(_) | Value::Na => format_value(v),
60        _ => locale.localize_number(&format_value(v)),
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn formats_each_variant() {
70        assert_eq!(format_value(&Value::Str("a<b".into())), "a<b");
71        assert_eq!(format_value(&Value::Bool(true)), "true");
72        assert_eq!(format_value(&Value::Integer(-42)), "-42");
73        assert_eq!(format_value(&Value::Float(2.0)), "2");
74        assert_eq!(format_value(&Value::Float(0.12345)), "0.123");
75        assert_eq!(format_value(&Value::DateTime(3_661)), "1970-01-01 01:01:01");
76        assert_eq!(format_value(&Value::Na), "");
77    }
78
79    #[test]
80    fn formats_each_variant_with_locale() {
81        let de = Locale::de_de();
82        assert_eq!(format_value_locale(&Value::Str("1.5".into()), &de), "1.5");
83        assert_eq!(format_value_locale(&Value::Integer(-4200), &de), "-4.200");
84        assert_eq!(format_value_locale(&Value::Float(0.5), &de), "0,5");
85        assert_eq!(format_value_locale(&Value::Na, &de), "");
86        assert_eq!(
87            format_value_locale(&Value::DateTime(1_792_022_400), &Locale::en_us()),
88            "Oct 15, 2026"
89        );
90        assert_eq!(
91            format_value_locale(&Value::Float(1234.5), &Locale::en_us()),
92            "1,234.5"
93        );
94    }
95}