1use crate::aes::Aesthetic;
9use crate::data::Value;
10use crate::render::backend::PointShape;
11use indexmap::IndexSet;
12
13use super::Scale;
14
15#[derive(Clone, Debug)]
20pub struct ScaleValueManual {
21 aesthetic: Aesthetic,
22 name: String,
23 configured: Vec<String>,
24 levels: IndexSet<String>,
25 values: Vec<f64>,
26}
27
28impl ScaleValueManual {
29 pub fn new(aesthetic: Aesthetic, values: Vec<(&str, f64)>) -> Self {
30 let configured: Vec<String> = values.iter().map(|(k, _)| k.to_string()).collect();
31 ScaleValueManual {
32 aesthetic,
33 name: String::new(),
34 levels: configured.iter().cloned().collect(),
35 configured,
36 values: values.iter().map(|(_, v)| *v).collect(),
37 }
38 }
39
40 fn value_for(&self, value: &Value) -> Option<f64> {
41 if self.values.is_empty() {
42 return None;
43 }
44 let idx = self
45 .levels
46 .get_index_of(value.key_str().as_ref())
47 .unwrap_or(0);
48 Some(self.values[idx % self.values.len()])
49 }
50}
51
52impl Scale for ScaleValueManual {
53 fn aesthetic(&self) -> Aesthetic {
54 self.aesthetic.clone()
55 }
56
57 fn train(&mut self, values: &[Value]) {
58 for v in values {
59 if v.is_na() {
60 continue;
61 }
62 let key = v.key_str();
63 if !self.levels.contains(key.as_ref()) {
64 self.levels.insert(key.into_owned());
65 }
66 }
67 }
68
69 fn map(&self, value: &Value) -> f64 {
70 self.levels
71 .get_index_of(value.key_str().as_ref())
72 .map(|i| i as f64)
73 .unwrap_or(0.0)
74 }
75
76 fn breaks(&self) -> Vec<(f64, String)> {
77 self.levels
78 .iter()
79 .enumerate()
80 .map(|(i, l)| (i as f64, l.clone()))
81 .collect()
82 }
83
84 fn name(&self) -> &str {
85 &self.name
86 }
87
88 fn set_name(&mut self, name: &str) {
89 self.name = name.to_string();
90 }
91
92 fn is_discrete(&self) -> bool {
93 true
94 }
95
96 fn map_to_size(&self, value: &Value) -> Option<f64> {
97 match self.aesthetic {
98 Aesthetic::Size | Aesthetic::Linewidth => self.value_for(value),
99 _ => None,
100 }
101 }
102
103 fn map_to_alpha(&self, value: &Value) -> Option<f64> {
104 match self.aesthetic {
105 Aesthetic::Alpha => self.value_for(value).map(|a| a.clamp(0.0, 1.0)),
106 _ => None,
107 }
108 }
109
110 fn clone_box(&self) -> Box<dyn Scale> {
111 Box::new(self.clone())
112 }
113
114 fn reset_training(&mut self) {
115 self.levels = self.configured.iter().cloned().collect();
116 }
117}
118
119#[derive(Clone, Debug)]
124pub struct ScaleValueIdentity {
125 aesthetic: Aesthetic,
126}
127
128impl ScaleValueIdentity {
129 pub fn new(aesthetic: Aesthetic) -> Self {
131 ScaleValueIdentity { aesthetic }
132 }
133}
134
135pub(crate) fn shape_from_value(value: &Value) -> Option<PointShape> {
137 if let Some(name) = value.as_str() {
138 return match name.trim().to_ascii_lowercase().as_str() {
139 "circle" | "circle filled" | "circle open" | "circle small" => Some(PointShape::Circle),
140 "triangle" | "triangle filled" | "triangle open" => Some(PointShape::Triangle),
141 "square" | "square filled" | "square open" => Some(PointShape::Square),
142 "diamond" | "diamond filled" | "diamond open" => Some(PointShape::Diamond),
143 "cross" => Some(PointShape::Cross),
144 "plus" => Some(PointShape::Plus),
145 n => n.parse::<f64>().ok().and_then(shape_from_code),
146 };
147 }
148 value.as_f64().and_then(shape_from_code)
149}
150
151fn shape_from_code(f: f64) -> Option<PointShape> {
152 if !f.is_finite() || f.fract() != 0.0 {
153 return None;
154 }
155 match f as i64 {
156 1 | 10 | 13 | 16 | 19 | 20 | 21 => Some(PointShape::Circle),
157 2 | 6 | 17 | 24 | 25 => Some(PointShape::Triangle),
158 0 | 7 | 12 | 14 | 15 | 22 => Some(PointShape::Square),
159 5 | 9 | 18 | 23 => Some(PointShape::Diamond),
160 4 | 8 => Some(PointShape::Cross),
161 3 => Some(PointShape::Plus),
162 _ => None,
163 }
164}
165
166impl Scale for ScaleValueIdentity {
167 fn aesthetic(&self) -> Aesthetic {
168 self.aesthetic.clone()
169 }
170
171 fn train(&mut self, _values: &[Value]) {}
172
173 fn map(&self, _value: &Value) -> f64 {
174 0.0
175 }
176
177 fn breaks(&self) -> Vec<(f64, String)> {
178 Vec::new()
179 }
180
181 fn name(&self) -> &str {
182 ""
183 }
184
185 fn set_name(&mut self, _name: &str) {}
186
187 fn map_to_size(&self, value: &Value) -> Option<f64> {
188 match self.aesthetic {
189 Aesthetic::Size | Aesthetic::Linewidth => {
190 value.as_f64().filter(|f| f.is_finite() && *f >= 0.0)
191 }
192 _ => None,
193 }
194 }
195
196 fn map_to_alpha(&self, value: &Value) -> Option<f64> {
197 match self.aesthetic {
198 Aesthetic::Alpha => value
199 .as_f64()
200 .filter(|f| f.is_finite())
201 .map(|a| a.clamp(0.0, 1.0)),
202 _ => None,
203 }
204 }
205
206 fn map_to_shape(&self, value: &Value) -> Option<PointShape> {
207 match self.aesthetic {
208 Aesthetic::Shape => shape_from_value(value),
209 _ => None,
210 }
211 }
212
213 fn clone_box(&self) -> Box<dyn Scale> {
214 Box::new(self.clone())
215 }
216}
217
218#[derive(Clone, Debug)]
225pub struct ScaleValueBinned {
226 aesthetic: Aesthetic,
227 name: String,
228 n: usize,
229 range: (f64, f64),
230 min: f64,
231 max: f64,
232}
233
234impl ScaleValueBinned {
235 pub fn new(aesthetic: Aesthetic, n: usize) -> Self {
239 let range = match aesthetic {
240 Aesthetic::Alpha => (0.1, 1.0),
241 _ => (1.0, 6.0),
242 };
243 ScaleValueBinned {
244 aesthetic,
245 name: String::new(),
246 n: n.max(1),
247 range,
248 min: f64::INFINITY,
249 max: f64::NEG_INFINITY,
250 }
251 }
252
253 pub fn with_range(mut self, lo: f64, hi: f64) -> Self {
255 self.range = (lo, hi);
256 self
257 }
258
259 fn edges(&self) -> Vec<f64> {
260 if self.min > self.max {
261 return vec![];
262 }
263 let (lo, hi) = if self.max > self.min {
264 (self.min, self.max)
265 } else {
266 (self.min - 0.5, self.max + 0.5)
267 };
268 let mut e = super::util::extended_breaks(lo, hi, self.n);
269 if e.len() < 2 {
270 e = vec![lo, hi];
271 }
272 let step = e[1] - e[0];
273 while e[0] > lo + step * 1e-9 {
274 e.insert(0, e[0] - step);
275 }
276 while *e.last().unwrap() < hi - step * 1e-9 {
277 e.push(e.last().unwrap() + step);
278 }
279 e
280 }
281
282 fn labels(&self, e: &[f64]) -> Vec<String> {
283 let l = super::util::format_numbers_opt(e, None);
284 l.windows(2).map(|w| format!("{}–{}", w[0], w[1])).collect()
285 }
286
287 fn bin(&self, value: &Value) -> Option<(usize, usize)> {
289 let e = self.edges();
290 let nb = e.len().checked_sub(1).filter(|n| *n > 0)?;
291 if let Some(f) = value.as_f64().filter(|f| f.is_finite()) {
292 let k = e.windows(2).position(|w| f <= w[1]).unwrap_or(nb - 1);
293 return Some((k, nb));
294 }
295 let s = value.as_str()?;
296 self.labels(&e).iter().position(|l| l == s).map(|k| (k, nb))
297 }
298
299 fn fraction(&self, value: &Value) -> Option<f64> {
300 self.bin(value).map(|(k, nb)| {
301 if nb > 1 {
302 k as f64 / (nb - 1) as f64
303 } else {
304 0.5
305 }
306 })
307 }
308}
309
310impl Scale for ScaleValueBinned {
311 fn aesthetic(&self) -> Aesthetic {
312 self.aesthetic.clone()
313 }
314
315 fn train(&mut self, values: &[Value]) {
316 for f in values
317 .iter()
318 .filter_map(Value::as_f64)
319 .filter(|f| f.is_finite())
320 {
321 self.min = self.min.min(f);
322 self.max = self.max.max(f);
323 }
324 }
325
326 fn map(&self, value: &Value) -> f64 {
327 self.bin(value).map_or(0.0, |(k, _)| k as f64)
328 }
329
330 fn breaks(&self) -> Vec<(f64, String)> {
331 let e = self.edges();
332 self.labels(&e)
333 .into_iter()
334 .enumerate()
335 .map(|(i, l)| (i as f64, l))
336 .collect()
337 }
338
339 fn bin_edge_labels(&self) -> Option<Vec<String>> {
340 let e = self.edges();
341 (e.len() >= 2).then(|| super::util::format_numbers_opt(&e, None))
342 }
343
344 fn name(&self) -> &str {
345 &self.name
346 }
347
348 fn set_name(&mut self, name: &str) {
349 self.name = name.to_string();
350 }
351
352 fn is_discrete(&self) -> bool {
353 true
354 }
355
356 fn map_to_size(&self, value: &Value) -> Option<f64> {
357 let t = self.fraction(value)?;
358 let (lo, hi) = self.range;
359 match self.aesthetic {
360 Aesthetic::Size => Some((lo * lo + t * (hi * hi - lo * lo)).max(0.0).sqrt()),
362 Aesthetic::Linewidth => Some(lo + t * (hi - lo)),
363 _ => None,
364 }
365 }
366
367 fn map_to_alpha(&self, value: &Value) -> Option<f64> {
368 let t = self.fraction(value)?;
369 let (lo, hi) = self.range;
370 matches!(self.aesthetic, Aesthetic::Alpha).then(|| (lo + t * (hi - lo)).clamp(0.0, 1.0))
371 }
372
373 fn map_to_shape(&self, value: &Value) -> Option<PointShape> {
374 let (k, _) = self.bin(value)?;
375 matches!(self.aesthetic, Aesthetic::Shape)
376 .then(|| PointShape::ALL[k % PointShape::ALL.len()])
377 }
378
379 fn map_to_linetype(&self, value: &Value) -> Option<crate::render::backend::Linetype> {
380 use crate::render::backend::Linetype as L;
381 let (k, _) = self.bin(value)?;
382 let all = [
383 L::Solid,
384 L::Dashed,
385 L::Dotted,
386 L::DashDot,
387 L::LongDash,
388 L::TwoDash,
389 ];
390 matches!(self.aesthetic, Aesthetic::Linetype).then(|| all[k % all.len()].clone())
391 }
392
393 fn clone_box(&self) -> Box<dyn Scale> {
394 Box::new(self.clone())
395 }
396
397 fn reset_training(&mut self) {
398 self.min = f64::INFINITY;
399 self.max = f64::NEG_INFINITY;
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn manual_maps_levels_and_cycles() {
409 let mut s = ScaleValueManual::new(Aesthetic::Size, vec![("a", 2.0), ("b", 6.0)]);
410 s.train(&[Value::Str("c".into())]);
411 assert_eq!(s.map_to_size(&Value::Str("b".into())), Some(6.0));
412 assert_eq!(s.map_to_size(&Value::Str("c".into())), Some(2.0));
413 assert_eq!(s.map_to_alpha(&Value::Str("a".into())), None);
414 assert_eq!(s.breaks().len(), 3);
415 s.reset_training();
416 assert_eq!(s.breaks().len(), 2);
417 let a = ScaleValueManual::new(Aesthetic::Alpha, vec![("a", 1.5)]);
418 assert_eq!(a.map_to_alpha(&Value::Str("a".into())), Some(1.0));
419 }
420
421 #[test]
422 fn binned_maps_by_bin() {
423 let mut s = ScaleValueBinned::new(Aesthetic::Size, 5);
424 s.train(&[Value::Float(0.0), Value::Float(100.0)]);
425 let b = s.breaks();
426 assert_eq!(b.len(), 4, "{b:?}");
427 assert_eq!(b[0].1, "0–25");
428 let lo = s.map_to_size(&Value::Float(10.0)).unwrap();
429 let hi = s.map_to_size(&Value::Float(90.0)).unwrap();
430 assert!((lo - 1.0).abs() < 1e-9 && (hi - 6.0).abs() < 1e-9);
431 assert_eq!(s.map_to_size(&Value::Str("75–100".into())), Some(hi));
432 assert_eq!(s.map_to_size(&Value::Float(25.0)), Some(lo));
434 let mut sh = ScaleValueBinned::new(Aesthetic::Shape, 5);
435 sh.train(&[Value::Float(0.0), Value::Float(100.0)]);
436 assert_eq!(
437 sh.map_to_shape(&Value::Float(60.0)),
438 Some(PointShape::ALL[2])
439 );
440 let mut a = ScaleValueBinned::new(Aesthetic::Alpha, 5);
441 a.train(&[Value::Float(0.0), Value::Float(100.0)]);
442 assert_eq!(a.map_to_alpha(&Value::Float(0.0)), Some(0.1));
443 }
444
445 #[test]
446 fn identity_uses_the_values() {
447 let s = ScaleValueIdentity::new(Aesthetic::Size);
448 assert_eq!(s.map_to_size(&Value::Float(3.5)), Some(3.5));
449 assert!(s.breaks().is_empty());
450 let a = ScaleValueIdentity::new(Aesthetic::Alpha);
451 assert_eq!(a.map_to_alpha(&Value::Float(0.25)), Some(0.25));
452 let sh = ScaleValueIdentity::new(Aesthetic::Shape);
453 assert_eq!(
454 sh.map_to_shape(&Value::Integer(17)),
455 Some(PointShape::Triangle)
456 );
457 assert_eq!(
458 sh.map_to_shape(&Value::Str("diamond".into())),
459 Some(PointShape::Diamond)
460 );
461 assert_eq!(
462 sh.map_to_shape(&Value::Str("4".into())),
463 Some(PointShape::Cross)
464 );
465 assert_eq!(sh.map_to_shape(&Value::Float(99.0)), None);
466 }
467}