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> {
193 let t = fraction.clamp(0.0, 1.0);
194 if let Some(max_size) = self.area_max {
195 if !self.trained || self.max <= self.min {
196 return Some(0.0);
197 }
198 let max_size = Self::sanitized_area_max(max_size);
199 let v = self.min + t * (self.max - self.min);
200 let denom = self.min.abs().max(self.max.abs());
201 return Some(if denom < f64::EPSILON {
202 0.0
203 } else {
204 max_size * (v.abs() / denom).sqrt()
205 });
206 }
207 let (lo, hi) = self.range;
208 Some(lo + t * (hi - lo))
209 }
210
211 fn clone_box(&self) -> Box<dyn Scale> {
212 Box::new(self.clone())
213 }
214
215 fn reset_training(&mut self) {
216 self.min = f64::INFINITY;
217 self.max = f64::NEG_INFINITY;
218 self.trained = false;
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn format_value_formats_a_finite_number_and_rejects_non_finite() {
228 let s = ScaleSizeContinuous::new();
229 assert_eq!(
230 s.format_value(&Value::Float(1234.0)),
231 Some("1234".to_string())
232 );
233 assert_eq!(s.format_value(&Value::Float(f64::NAN)), None);
234 assert_eq!(s.format_value(&Value::Na), None);
235 }
236
237 #[test]
238 fn scale_size_area_matches_r_ggplot2() {
239 let mut s = ScaleSizeContinuous::new().area(6.0);
240 s.train(&[
241 Value::Float(0.0),
242 Value::Float(1.0),
243 Value::Float(4.0),
244 Value::Float(16.0),
245 ]);
246 let got: Vec<f64> = [0.0, 1.0, 4.0, 16.0]
247 .iter()
248 .map(|&v| s.map_to_size(&Value::Float(v)).unwrap())
249 .collect();
250 for (g, e) in got.iter().zip([0.0, 1.5, 3.0, 6.0]) {
251 assert!((g - e).abs() < 1e-9, "got {got:?}");
252 }
253 }
254
255 #[test]
260 fn scale_size_area_legend_fraction_matches_map_to_size() {
261 let mut s = ScaleSizeContinuous::new().area(6.0);
262 s.train(&[Value::Float(0.0), Value::Float(16.0)]);
263 for v in [0.0, 1.0, 4.0, 9.0, 16.0] {
264 let direct = s.map_to_size(&Value::Float(v)).unwrap();
265 let pos = s.map(&Value::Float(v));
266 let via_fraction = s.map_fraction_to_size(pos).unwrap();
267 assert!(
268 (direct - via_fraction).abs() < 1e-9,
269 "value {v}: map_to_size={direct}, map_fraction_to_size={via_fraction}"
270 );
271 }
272 }
273
274 #[test]
277 fn linear_size_legend_fraction_still_uses_range() {
278 let mut s = ScaleSizeContinuous::new().with_range(2.0, 10.0);
279 s.train(&[Value::Float(0.0), Value::Float(100.0)]);
280 assert_eq!(s.map_fraction_to_size(0.0), Some(2.0));
281 assert_eq!(s.map_fraction_to_size(1.0), Some(10.0));
282 assert_eq!(s.map_fraction_to_size(0.5), Some(6.0));
283 }
284
285 #[test]
286 fn scale_size_area_rejects_na_and_non_finite() {
287 let mut s = ScaleSizeContinuous::new().area(6.0);
288 s.train(&[Value::Float(0.0), Value::Float(10.0)]);
289 assert_eq!(s.map_to_size(&Value::Na), None);
290 assert_eq!(s.map_to_size(&Value::Float(f64::NAN)), None);
291 }
292
293 #[test]
294 fn scale_size_area_bad_max_warns_and_falls_back_to_six() {
295 let mut s = ScaleSizeContinuous::new().area(-1.0);
296 s.train(&[Value::Float(0.0), Value::Float(16.0)]);
297 let mut warnings = Vec::new();
298 s.validate(&mut warnings);
299 assert!(warnings.iter().any(|w| w.contains("max_size")));
300 assert_eq!(
303 s.map_to_size(&Value::Float(16.0)),
304 Some(6.0),
305 "falls back to max_size 6"
306 );
307 }
308}