Skip to main content

ggplot_rs/scale/
gradient.rs

1use crate::aes::Aesthetic;
2use crate::data::Value;
3
4use super::color::RGBAColor;
5use super::util::nice_step;
6use super::Scale;
7
8/// Diverging gradient color scale (low → mid → high with midpoint).
9#[derive(Clone, Debug)]
10pub struct ScaleColorGradient2 {
11    aesthetic: Aesthetic,
12    name: String,
13    low: RGBAColor,
14    mid: RGBAColor,
15    high: RGBAColor,
16    midpoint: f64,
17    min: f64,
18    max: f64,
19    /// Locale for the default break labels (`None` = default marks).
20    locale: Option<crate::locale::Locale>,
21}
22
23impl ScaleColorGradient2 {
24    pub fn new(aesthetic: Aesthetic) -> Self {
25        ScaleColorGradient2 {
26            aesthetic,
27            name: String::new(),
28            low: RGBAColor::new(0, 0, 255),
29            mid: RGBAColor::new(255, 255, 255),
30            high: RGBAColor::new(255, 0, 0),
31            midpoint: 0.0,
32            min: f64::INFINITY,
33            max: f64::NEG_INFINITY,
34            locale: None,
35        }
36    }
37
38    pub fn with_colors(mut self, low: RGBAColor, mid: RGBAColor, high: RGBAColor) -> Self {
39        self.low = low;
40        self.mid = mid;
41        self.high = high;
42        self
43    }
44
45    pub fn with_midpoint(mut self, midpoint: f64) -> Self {
46        self.midpoint = midpoint;
47        self
48    }
49}
50
51impl Scale for ScaleColorGradient2 {
52    fn set_default_locale(&mut self, locale: &crate::locale::Locale) {
53        if self.locale.is_none() {
54            self.locale = Some(locale.clone());
55        }
56    }
57
58    fn locale(&self) -> Option<&crate::locale::Locale> {
59        self.locale.as_ref()
60    }
61
62    fn aesthetic(&self) -> Aesthetic {
63        self.aesthetic.clone()
64    }
65
66    fn train(&mut self, values: &[Value]) {
67        for v in values {
68            if let Some(f) = v.as_f64() {
69                if f.is_finite() {
70                    if f < self.min {
71                        self.min = f;
72                    }
73                    if f > self.max {
74                        self.max = f;
75                    }
76                }
77            }
78        }
79    }
80
81    fn map(&self, value: &Value) -> f64 {
82        let f = match value.as_f64() {
83            Some(f) => f,
84            None => return 0.0,
85        };
86        let range = self.max - self.min;
87        if range.abs() < f64::EPSILON {
88            0.5
89        } else {
90            (f - self.min) / range
91        }
92    }
93
94    fn breaks(&self) -> Vec<(f64, String)> {
95        if self.min > self.max || !self.min.is_finite() || !self.max.is_finite() {
96            return vec![];
97        }
98        let range = self.max - self.min;
99        if super::util::is_degenerate_range(self.min, self.max) {
100            return vec![(
101                0.5,
102                super::util::format_number_opt(self.min, self.locale.as_ref()),
103            )];
104        }
105        let n_breaks = 5;
106        let raw_step = range / n_breaks as f64;
107        let step = nice_step(raw_step);
108        let start = (self.min / step).ceil() * step;
109        let values = super::util::stepped_breaks(start, self.max, step);
110        let labels = super::util::format_numbers_opt(&values, self.locale.as_ref());
111        values
112            .iter()
113            .zip(labels)
114            .map(|(&v, l)| (self.map(&Value::Float(v)), l))
115            .collect()
116    }
117
118    fn name(&self) -> &str {
119        &self.name
120    }
121
122    fn set_name(&mut self, name: &str) {
123        self.name = name.to_string();
124    }
125
126    fn map_to_color(&self, value: &Value) -> Option<(u8, u8, u8)> {
127        let f = value.as_f64()?;
128
129        // Map relative to midpoint
130        let c = if f <= self.midpoint {
131            let range = self.midpoint - self.min;
132            let t = if range.abs() < f64::EPSILON {
133                0.0
134            } else {
135                (f - self.min) / range
136            };
137            self.low.lerp(&self.mid, t)
138        } else {
139            let range = self.max - self.midpoint;
140            let t = if range.abs() < f64::EPSILON {
141                1.0
142            } else {
143                (f - self.midpoint) / range
144            };
145            self.mid.lerp(&self.high, t)
146        };
147
148        Some((c.r, c.g, c.b))
149    }
150
151    fn domain(&self) -> Option<(f64, f64)> {
152        if self.min.is_finite() && self.max.is_finite() && self.min <= self.max {
153            Some((self.min, self.max))
154        } else {
155            None
156        }
157    }
158
159    fn clone_box(&self) -> Box<dyn Scale> {
160        Box::new(self.clone())
161    }
162
163    fn reset_training(&mut self) {
164        self.min = f64::INFINITY;
165        self.max = f64::NEG_INFINITY;
166    }
167}