Skip to main content

ggplot_rs/scale/
size.rs

1use crate::aes::Aesthetic;
2use crate::data::Value;
3
4use super::Scale;
5
6/// Continuous size scale — maps data values to a point radius range.
7#[derive(Clone, Debug)]
8pub struct ScaleSizeContinuous {
9    name: String,
10    min: f64,
11    max: f64,
12    trained: bool,
13    /// Output range in pixels: (min_radius, max_radius).
14    range: (f64, f64),
15    /// Locale for the default break labels (`None` = default marks).
16    locale: Option<crate::locale::Locale>,
17}
18
19impl ScaleSizeContinuous {
20    pub fn new() -> Self {
21        ScaleSizeContinuous {
22            name: String::new(),
23            min: f64::INFINITY,
24            max: f64::NEG_INFINITY,
25            trained: false,
26            range: (1.0, 6.0),
27            locale: None,
28        }
29    }
30
31    pub fn with_range(mut self, min: f64, max: f64) -> Self {
32        self.range = (min, max);
33        self
34    }
35
36    pub fn with_name(mut self, name: &str) -> Self {
37        self.name = name.to_string();
38        self
39    }
40}
41
42impl Default for ScaleSizeContinuous {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48impl Scale for ScaleSizeContinuous {
49    fn set_default_locale(&mut self, locale: &crate::locale::Locale) {
50        if self.locale.is_none() {
51            self.locale = Some(locale.clone());
52        }
53    }
54
55    fn locale(&self) -> Option<&crate::locale::Locale> {
56        self.locale.as_ref()
57    }
58
59    fn format_value(&self, value: &Value) -> Option<String> {
60        let f = value.as_f64()?;
61        if !f.is_finite() {
62            return None;
63        }
64        Some(super::util::format_number_opt(f, self.locale.as_ref()))
65    }
66
67    fn aesthetic(&self) -> Aesthetic {
68        Aesthetic::Size
69    }
70
71    fn train(&mut self, values: &[Value]) {
72        for v in values {
73            if let Some(f) = v.as_f64() {
74                if f.is_finite() {
75                    if f < self.min {
76                        self.min = f;
77                    }
78                    if f > self.max {
79                        self.max = f;
80                    }
81                }
82            }
83        }
84        self.trained = true;
85    }
86
87    fn map(&self, value: &Value) -> f64 {
88        let f = match value.as_f64() {
89            Some(f) => f,
90            None => return 0.0,
91        };
92        let range = self.max - self.min;
93        if range.abs() < f64::EPSILON {
94            0.5
95        } else {
96            ((f - self.min) / range).clamp(0.0, 1.0)
97        }
98    }
99
100    fn breaks(&self) -> Vec<(f64, String)> {
101        if !self.trained || self.min > self.max {
102            return vec![];
103        }
104        let range = self.max - self.min;
105        if super::util::is_degenerate_range(self.min, self.max) {
106            return vec![(
107                0.5,
108                super::util::format_number_opt(self.min, self.locale.as_ref()),
109            )];
110        }
111        let n_breaks = 4;
112        let step = super::util::nice_step(range / n_breaks as f64);
113        let start = (self.min / step).ceil() * step;
114        let values = super::util::stepped_breaks(start, self.max, step);
115        let labels = super::util::format_numbers_opt(&values, self.locale.as_ref());
116        values
117            .iter()
118            .zip(labels)
119            .map(|(&v, l)| (self.map(&Value::Float(v)), l))
120            .collect()
121    }
122
123    fn name(&self) -> &str {
124        &self.name
125    }
126
127    fn set_name(&mut self, name: &str) {
128        self.name = name.to_string();
129    }
130
131    fn map_to_size(&self, value: &Value) -> Option<f64> {
132        let t = self.map(value);
133        let (lo, hi) = self.range;
134        Some(lo + t * (hi - lo))
135    }
136
137    fn clone_box(&self) -> Box<dyn Scale> {
138        Box::new(self.clone())
139    }
140
141    fn reset_training(&mut self) {
142        self.min = f64::INFINITY;
143        self.max = f64::NEG_INFINITY;
144        self.trained = false;
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn format_value_formats_a_finite_number_and_rejects_non_finite() {
154        let s = ScaleSizeContinuous::new();
155        assert_eq!(
156            s.format_value(&Value::Float(1234.0)),
157            Some("1234".to_string())
158        );
159        assert_eq!(s.format_value(&Value::Float(f64::NAN)), None);
160        assert_eq!(s.format_value(&Value::Na), None);
161    }
162}