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 set_default_locale(&mut self, locale: &crate::locale::Locale) {
61        self.inner.set_default_locale(locale)
62    }
63    fn locale(&self) -> Option<&crate::locale::Locale> {
64        self.inner.locale()
65    }
66    fn name(&self) -> &str {
67        self.inner.name()
68    }
69    fn set_name(&mut self, name: &str) {
70        self.inner.set_name(name)
71    }
72    fn is_discrete(&self) -> bool {
73        self.inner.is_discrete()
74    }
75    fn map_to_color(&self, value: &Value) -> Option<(u8, u8, u8)> {
76        self.inner
77            .map_to_color(value)
78            .map(|c| adjust_lightness(c, self.lightness))
79    }
80    fn map_to_shape(&self, value: &Value) -> Option<PointShape> {
81        self.inner.map_to_shape(value)
82    }
83    fn map_to_linetype(&self, value: &Value) -> Option<Linetype> {
84        self.inner.map_to_linetype(value)
85    }
86    fn map_to_size(&self, value: &Value) -> Option<f64> {
87        self.inner.map_to_size(value)
88    }
89    fn map_to_alpha(&self, value: &Value) -> Option<f64> {
90        self.inner.map_to_alpha(value)
91    }
92    fn sec_axis(&self) -> Option<&SecAxis> {
93        None
94    }
95    fn domain(&self) -> Option<(f64, f64)> {
96        self.inner.domain()
97    }
98    fn clone_box(&self) -> Box<dyn Scale> {
99        Box::new(ScaleColorModified {
100            inner: self.inner.clone_box(),
101            aesthetic: self.aesthetic.clone(),
102            lightness: self.lightness,
103        })
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn darken_and_lighten() {
113        assert_eq!(adjust_lightness((100, 100, 100), -0.5), (50, 50, 50));
114        assert_eq!(adjust_lightness((100, 100, 100), 0.0), (100, 100, 100));
115        assert_eq!(adjust_lightness((100, 100, 100), 1.0), (255, 255, 255));
116        assert_eq!(adjust_lightness((100, 100, 100), -1.0), (0, 0, 0));
117    }
118}