Skip to main content

ggplot_rs/scale/
values.rs

1//! Manual and identity scales for the numeric aesthetics (#408):
2//! ggplot2's `scale_size_manual()` / `scale_alpha_manual()` /
3//! `scale_linewidth_manual()` ([`ScaleValueManual`]) and
4//! `scale_size_identity()` / `scale_alpha_identity()` /
5//! `scale_linewidth_identity()` / `scale_shape_identity()`
6//! ([`ScaleValueIdentity`]).
7
8use crate::aes::Aesthetic;
9use crate::data::Value;
10use crate::render::backend::PointShape;
11use indexmap::IndexSet;
12
13use super::Scale;
14
15/// A discrete scale mapping named levels to user-chosen numbers — point
16/// sizes, alpha values or line widths, by `aesthetic` (`Size`, `Alpha` or
17/// `Linewidth`). Levels the data has beyond `values` cycle through them,
18/// as [`ScaleShapeManual`](super::shape_manual::ScaleShapeManual) does.
19#[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/// An identity scale: the data already holds the size / alpha / line width
120/// (numbers) or point shape (ggplot2 `pch` codes, or a shape name such as
121/// `"triangle"`) to draw with, used as-is. No legend, as in ggplot2
122/// (`guide = "none"`).
123#[derive(Clone, Debug)]
124pub struct ScaleValueIdentity {
125    aesthetic: Aesthetic,
126}
127
128impl ScaleValueIdentity {
129    /// `aesthetic` is `Size`, `Alpha`, `Linewidth` or `Shape`.
130    pub fn new(aesthetic: Aesthetic) -> Self {
131        ScaleValueIdentity { aesthetic }
132    }
133}
134
135/// A ggplot2 `pch` code or shape name as the nearest [`PointShape`].
136pub(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/// A binned scale for a non-position aesthetic (#408): ggplot2's
219/// `scale_size_binned()` / `scale_alpha_binned()` / `scale_shape_binned()` /
220/// `scale_linetype_binned()` / `scale_linewidth_binned()`. A continuous value
221/// is cut into round, right-closed bins between about `n` breaks; each bin gets one size /
222/// alpha / line width (spread over `range`; sizes by area) or one shape /
223/// linetype, and the legend shows one key per bin, labelled `lo–hi`.
224#[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    /// `aesthetic` is `Size`, `Alpha`, `Linewidth`, `Shape` or `Linetype`.
236    /// The default output range is ggplot2's (size 1–6, alpha 0.1–1,
237    /// linewidth 1–6).
238    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    /// The output range for size / alpha / linewidth.
254    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    /// The bin of `value` — a number, or a legend label passed back.
288    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            // ggplot2's size scales spread *area*.
361            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        // 25 is right-closed: in the first bin.
433        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}