1use crate::aes::Aesthetic;
2use crate::data::Value;
3
4use super::color::RGBAColor;
5use super::util::nice_step;
6use super::Scale;
7
8#[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: Option<crate::locale::Locale>,
21 limits: Option<(f64, f64)>,
25 oob: super::Oob,
28 na_value: (u8, u8, u8),
32}
33
34impl ScaleColorGradient2 {
35 pub fn new(aesthetic: Aesthetic) -> Self {
36 ScaleColorGradient2 {
37 aesthetic,
38 name: String::new(),
39 low: RGBAColor::new(0, 0, 255),
40 mid: RGBAColor::new(255, 255, 255),
41 high: RGBAColor::new(255, 0, 0),
42 midpoint: 0.0,
43 min: f64::INFINITY,
44 max: f64::NEG_INFINITY,
45 locale: None,
46 limits: None,
47 oob: super::Oob::Censor,
48 na_value: super::DEFAULT_NA_VALUE,
49 }
50 }
51
52 pub fn with_colors(mut self, low: RGBAColor, mid: RGBAColor, high: RGBAColor) -> Self {
53 self.low = low;
54 self.mid = mid;
55 self.high = high;
56 self
57 }
58
59 pub fn with_midpoint(mut self, midpoint: f64) -> Self {
60 self.midpoint = midpoint;
61 self
62 }
63
64 pub fn with_limits(mut self, lo: f64, hi: f64) -> Self {
68 self.limits = Some((lo, hi));
69 self
70 }
71
72 pub fn with_oob(mut self, oob: super::Oob) -> Self {
76 self.oob = oob;
77 self
78 }
79
80 pub fn with_na_value(mut self, color: (u8, u8, u8)) -> Self {
84 self.na_value = color;
85 self
86 }
87}
88
89impl Scale for ScaleColorGradient2 {
90 fn set_default_locale(&mut self, locale: &crate::locale::Locale) {
91 if self.locale.is_none() {
92 self.locale = Some(locale.clone());
93 }
94 }
95
96 fn locale(&self) -> Option<&crate::locale::Locale> {
97 self.locale.as_ref()
98 }
99
100 fn format_value(&self, value: &Value) -> Option<String> {
101 let f = value.as_f64()?;
102 if !f.is_finite() {
103 return None;
104 }
105 Some(super::util::format_number_opt(f, self.locale.as_ref()))
106 }
107
108 fn aesthetic(&self) -> Aesthetic {
109 self.aesthetic.clone()
110 }
111
112 fn train(&mut self, values: &[Value]) {
113 if self.limits.is_some() {
114 return;
115 }
116 for v in values {
117 if let Some(f) = v.as_f64() {
118 if f.is_finite() {
119 if f < self.min {
120 self.min = f;
121 }
122 if f > self.max {
123 self.max = f;
124 }
125 }
126 }
127 }
128 }
129
130 fn map(&self, value: &Value) -> f64 {
131 let f = match value.as_f64() {
132 Some(f) => f,
133 None => return 0.0,
134 };
135 let (lo, hi) = self.limits.unwrap_or((self.min, self.max));
136 let range = hi - lo;
137 if range.abs() < f64::EPSILON {
138 0.5
139 } else {
140 (f - lo) / range
141 }
142 }
143
144 fn breaks(&self) -> Vec<(f64, String)> {
145 let (lo, hi) = self.limits.unwrap_or((self.min, self.max));
146 if lo > hi || !lo.is_finite() || !hi.is_finite() {
147 return vec![];
148 }
149 let range = hi - lo;
150 if super::util::is_degenerate_range(lo, hi) {
151 return vec![(
152 0.5,
153 super::util::format_number_opt(lo, self.locale.as_ref()),
154 )];
155 }
156 let n_breaks = 5;
157 let raw_step = range / n_breaks as f64;
158 let step = nice_step(raw_step);
159 let start = (lo / step).ceil() * step;
160 let values = super::util::stepped_breaks(start, hi, step);
161 let labels = super::util::format_numbers_opt(&values, self.locale.as_ref());
162 values
163 .iter()
164 .zip(labels)
165 .map(|(&v, l)| (self.map(&Value::Float(v)), l))
166 .collect()
167 }
168
169 fn name(&self) -> &str {
170 &self.name
171 }
172
173 fn set_name(&mut self, name: &str) {
174 self.name = name.to_string();
175 }
176
177 fn map_to_color(&self, value: &Value) -> Option<(u8, u8, u8)> {
178 if value.is_na() {
184 return Some(self.na_value);
185 }
186 let Some(f) = value.as_f64() else {
187 return Some(self.na_value);
188 };
189 if !f.is_finite() {
190 return Some(self.na_value);
191 }
192 let (lo, hi) = self.limits.unwrap_or((self.min, self.max));
193 let Some(f) = super::resolve_oob(f, self.limits, self.oob) else {
194 return Some(self.na_value);
195 };
196
197 let c = if f <= self.midpoint {
199 let range = self.midpoint - lo;
200 let t = if range.abs() < f64::EPSILON {
201 0.0
202 } else {
203 (f - lo) / range
204 };
205 self.low.lerp(&self.mid, t.clamp(0.0, 1.0))
206 } else {
207 let range = hi - self.midpoint;
208 let t = if range.abs() < f64::EPSILON {
209 1.0
210 } else {
211 (f - self.midpoint) / range
212 };
213 self.mid.lerp(&self.high, t.clamp(0.0, 1.0))
214 };
215
216 Some((c.r, c.g, c.b))
217 }
218
219 fn domain(&self) -> Option<(f64, f64)> {
220 if let Some((lo, hi)) = self.limits {
221 return Some((lo, hi));
222 }
223 if self.min.is_finite() && self.max.is_finite() && self.min <= self.max {
224 Some((self.min, self.max))
225 } else {
226 None
227 }
228 }
229
230 fn clone_box(&self) -> Box<dyn Scale> {
231 Box::new(self.clone())
232 }
233
234 fn reset_training(&mut self) {
235 self.min = f64::INFINITY;
236 self.max = f64::NEG_INFINITY;
237 }
238
239 fn validate(&self, warnings: &mut Vec<String>) {
240 if let Some((lo, hi)) = self.limits {
241 if lo > hi {
242 warnings.push(format!(
243 "scale_color/fill_gradient2: with_limits({lo}, {hi}) has lo > hi; every value maps to na_value"
244 ));
245 }
246 }
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn format_value_formats_a_finite_number_and_rejects_non_finite() {
256 let s = ScaleColorGradient2::new(Aesthetic::Color);
257 assert_eq!(s.format_value(&Value::Float(7.0)), Some("7".to_string()));
258 assert_eq!(s.format_value(&Value::Float(f64::NEG_INFINITY)), None);
259 }
260
261 #[test]
262 fn with_limits_fixes_the_domain_and_training_does_not_widen_it() {
263 let mut s = ScaleColorGradient2::new(Aesthetic::Fill).with_limits(-2.0, 2.0);
264 s.train(&[Value::Float(-50.0), Value::Float(50.0)]);
265 assert_eq!(s.domain(), Some((-2.0, 2.0)));
266 }
267}