1use crate::aes::Aesthetic;
2use crate::data::Value;
3
4use super::Scale;
5
6#[derive(Clone, Debug)]
8pub struct ScaleSizeContinuous {
9 name: String,
10 min: f64,
11 max: f64,
12 trained: bool,
13 range: (f64, f64),
15 locale: Option<crate::locale::Locale>,
17 area_max: Option<f64>,
22}
23
24impl ScaleSizeContinuous {
25 pub fn new() -> Self {
26 ScaleSizeContinuous {
27 name: String::new(),
28 min: f64::INFINITY,
29 max: f64::NEG_INFINITY,
30 trained: false,
31 range: (1.0, 6.0),
32 locale: None,
33 area_max: None,
34 }
35 }
36
37 pub fn with_range(mut self, min: f64, max: f64) -> Self {
38 self.range = (min, max);
39 self
40 }
41
42 pub fn with_name(mut self, name: &str) -> Self {
43 self.name = name.to_string();
44 self
45 }
46
47 pub fn area(mut self, max_size: f64) -> Self {
54 self.area_max = Some(max_size);
55 self
56 }
57
58 fn sanitized_area_max(max_size: f64) -> f64 {
59 if max_size.is_finite() && max_size > 0.0 {
60 max_size
61 } else {
62 6.0
63 }
64 }
65}
66
67impl Default for ScaleSizeContinuous {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73impl Scale for ScaleSizeContinuous {
74 fn set_default_locale(&mut self, locale: &crate::locale::Locale) {
75 if self.locale.is_none() {
76 self.locale = Some(locale.clone());
77 }
78 }
79
80 fn locale(&self) -> Option<&crate::locale::Locale> {
81 self.locale.as_ref()
82 }
83
84 fn format_value(&self, value: &Value) -> Option<String> {
85 let f = value.as_f64()?;
86 if !f.is_finite() {
87 return None;
88 }
89 Some(super::util::format_number_opt(f, self.locale.as_ref()))
90 }
91
92 fn aesthetic(&self) -> Aesthetic {
93 Aesthetic::Size
94 }
95
96 fn train(&mut self, values: &[Value]) {
97 for v in values {
98 if let Some(f) = v.as_f64() {
99 if f.is_finite() {
100 if f < self.min {
101 self.min = f;
102 }
103 if f > self.max {
104 self.max = f;
105 }
106 }
107 }
108 }
109 self.trained = true;
110 }
111
112 fn map(&self, value: &Value) -> f64 {
113 let f = match value.as_f64() {
114 Some(f) => f,
115 None => return 0.0,
116 };
117 let range = self.max - self.min;
118 if range.abs() < f64::EPSILON {
119 0.5
120 } else {
121 ((f - self.min) / range).clamp(0.0, 1.0)
122 }
123 }
124
125 fn breaks(&self) -> Vec<(f64, String)> {
126 if !self.trained || self.min > self.max {
127 return vec![];
128 }
129 let range = self.max - self.min;
130 if super::util::is_degenerate_range(self.min, self.max) {
131 return vec![(
132 0.5,
133 super::util::format_number_opt(self.min, self.locale.as_ref()),
134 )];
135 }
136 let n_breaks = 4;
137 let step = super::util::nice_step(range / n_breaks as f64);
138 let start = (self.min / step).ceil() * step;
139 let values = super::util::stepped_breaks(start, self.max, step);
140 let labels = super::util::format_numbers_opt(&values, self.locale.as_ref());
141 values
142 .iter()
143 .zip(labels)
144 .map(|(&v, l)| (self.map(&Value::Float(v)), l))
145 .collect()
146 }
147
148 fn name(&self) -> &str {
149 &self.name
150 }
151
152 fn set_name(&mut self, name: &str) {
153 self.name = name.to_string();
154 }
155
156 fn map_to_size(&self, value: &Value) -> Option<f64> {
157 if let Some(max_size) = self.area_max {
158 let v = value.as_f64()?;
159 if !v.is_finite() {
160 return None;
161 }
162 let max_size = Self::sanitized_area_max(max_size);
163 let denom = self.min.abs().max(self.max.abs());
164 return Some(if denom < f64::EPSILON {
165 0.0
166 } else {
167 max_size * (v.abs() / denom).sqrt()
168 });
169 }
170 let t = self.map(value);
171 let (lo, hi) = self.range;
172 Some(lo + t * (hi - lo))
173 }
174
175 fn validate(&self, warnings: &mut Vec<String>) {
176 if let Some(max_size) = self.area_max {
177 if !(max_size.is_finite() && max_size > 0.0) {
178 warnings.push(format!(
179 "scale_size_area: max_size({max_size}) must be finite and positive; falling back to 6"
180 ));
181 }
182 }
183 }
184
185 fn map_fraction_to_size(&self, fraction: f64) -> Option<f64> {
186 let (lo, hi) = self.range;
187 Some(lo + fraction.clamp(0.0, 1.0) * (hi - lo))
188 }
189
190 fn clone_box(&self) -> Box<dyn Scale> {
191 Box::new(self.clone())
192 }
193
194 fn reset_training(&mut self) {
195 self.min = f64::INFINITY;
196 self.max = f64::NEG_INFINITY;
197 self.trained = false;
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn format_value_formats_a_finite_number_and_rejects_non_finite() {
207 let s = ScaleSizeContinuous::new();
208 assert_eq!(
209 s.format_value(&Value::Float(1234.0)),
210 Some("1234".to_string())
211 );
212 assert_eq!(s.format_value(&Value::Float(f64::NAN)), None);
213 assert_eq!(s.format_value(&Value::Na), None);
214 }
215
216 #[test]
217 fn scale_size_area_matches_r_ggplot2() {
218 let mut s = ScaleSizeContinuous::new().area(6.0);
219 s.train(&[
220 Value::Float(0.0),
221 Value::Float(1.0),
222 Value::Float(4.0),
223 Value::Float(16.0),
224 ]);
225 let got: Vec<f64> = [0.0, 1.0, 4.0, 16.0]
226 .iter()
227 .map(|&v| s.map_to_size(&Value::Float(v)).unwrap())
228 .collect();
229 for (g, e) in got.iter().zip([0.0, 1.5, 3.0, 6.0]) {
230 assert!((g - e).abs() < 1e-9, "got {got:?}");
231 }
232 }
233
234 #[test]
235 fn scale_size_area_rejects_na_and_non_finite() {
236 let mut s = ScaleSizeContinuous::new().area(6.0);
237 s.train(&[Value::Float(0.0), Value::Float(10.0)]);
238 assert_eq!(s.map_to_size(&Value::Na), None);
239 assert_eq!(s.map_to_size(&Value::Float(f64::NAN)), None);
240 }
241
242 #[test]
243 fn scale_size_area_bad_max_warns_and_falls_back_to_six() {
244 let mut s = ScaleSizeContinuous::new().area(-1.0);
245 s.train(&[Value::Float(0.0), Value::Float(16.0)]);
246 let mut warnings = Vec::new();
247 s.validate(&mut warnings);
248 assert!(warnings.iter().any(|w| w.contains("max_size")));
249 assert_eq!(
252 s.map_to_size(&Value::Float(16.0)),
253 Some(6.0),
254 "falls back to max_size 6"
255 );
256 }
257}