Skip to main content

ggplot_rs/scale/
modified.rs

1//! A color scale that wraps another scale and adjusts the lightness of its
2//! mapped colors — the backing implementation for `after_scale` color
3//! derivations (e.g. a fill that is a darker version of the mapped color).
4
5use crate::aes::Aesthetic;
6use crate::data::Value;
7use crate::render::backend::{Linetype, PointShape};
8
9use super::sec_axis::SecAxis;
10use super::Scale;
11
12/// Adjust an RGB color's lightness: `f > 0` blends toward white, `f < 0` toward
13/// black, clamped to `-1.0..=1.0`.
14fn adjust_lightness((r, g, b): (u8, u8, u8), f: f64) -> (u8, u8, u8) {
15    let f = f.clamp(-1.0, 1.0);
16    let ch = |c: u8| -> u8 {
17        let c = c as f64;
18        let out = if f >= 0.0 {
19            c + (255.0 - c) * f
20        } else {
21            c * (1.0 + f)
22        };
23        out.round().clamp(0.0, 255.0) as u8
24    };
25    (ch(r), ch(g), ch(b))
26}
27
28/// Wraps a source scale, exposing it under a different (target) aesthetic and
29/// lightness-adjusting every mapped color. All non-color behaviour delegates to
30/// the inner scale.
31pub struct ScaleColorModified {
32    inner: Box<dyn Scale>,
33    aesthetic: Aesthetic,
34    lightness: f64,
35}
36
37impl ScaleColorModified {
38    pub fn new(inner: Box<dyn Scale>, target: Aesthetic, lightness: f64) -> Self {
39        ScaleColorModified {
40            inner,
41            aesthetic: target,
42            lightness,
43        }
44    }
45}
46
47impl Scale for ScaleColorModified {
48    fn aesthetic(&self) -> Aesthetic {
49        self.aesthetic.clone()
50    }
51    fn train(&mut self, values: &[Value]) {
52        self.inner.train(values)
53    }
54    fn map(&self, value: &Value) -> f64 {
55        self.inner.map(value)
56    }
57    fn breaks(&self) -> Vec<(f64, String)> {
58        self.inner.breaks()
59    }
60    fn name(&self) -> &str {
61        self.inner.name()
62    }
63    fn set_name(&mut self, name: &str) {
64        self.inner.set_name(name)
65    }
66    fn is_discrete(&self) -> bool {
67        self.inner.is_discrete()
68    }
69    fn map_to_color(&self, value: &Value) -> Option<(u8, u8, u8)> {
70        self.inner
71            .map_to_color(value)
72            .map(|c| adjust_lightness(c, self.lightness))
73    }
74    fn map_to_shape(&self, value: &Value) -> Option<PointShape> {
75        self.inner.map_to_shape(value)
76    }
77    fn map_to_linetype(&self, value: &Value) -> Option<Linetype> {
78        self.inner.map_to_linetype(value)
79    }
80    fn map_to_size(&self, value: &Value) -> Option<f64> {
81        self.inner.map_to_size(value)
82    }
83    fn map_to_alpha(&self, value: &Value) -> Option<f64> {
84        self.inner.map_to_alpha(value)
85    }
86    fn sec_axis(&self) -> Option<&SecAxis> {
87        None
88    }
89    fn domain(&self) -> Option<(f64, f64)> {
90        self.inner.domain()
91    }
92    fn clone_box(&self) -> Box<dyn Scale> {
93        Box::new(ScaleColorModified {
94            inner: self.inner.clone_box(),
95            aesthetic: self.aesthetic.clone(),
96            lightness: self.lightness,
97        })
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn darken_and_lighten() {
107        assert_eq!(adjust_lightness((100, 100, 100), -0.5), (50, 50, 50));
108        assert_eq!(adjust_lightness((100, 100, 100), 0.0), (100, 100, 100));
109        assert_eq!(adjust_lightness((100, 100, 100), 1.0), (255, 255, 255));
110        assert_eq!(adjust_lightness((100, 100, 100), -1.0), (0, 0, 0));
111    }
112}