1use crate::aes::Aesthetic;
2use crate::data::Value;
3
4use super::color::RGBAColor;
5use super::format::UnwindSafeFormatter;
6use super::util::nice_step;
7use super::Scale;
8
9#[derive(Clone)]
11pub struct ScaleColorGradient2 {
12 aesthetic: Aesthetic,
13 name: String,
14 low: RGBAColor,
15 mid: RGBAColor,
16 high: RGBAColor,
17 midpoint: f64,
18 min: f64,
19 max: f64,
20 locale: Option<crate::locale::Locale>,
22 limits: Option<(f64, f64)>,
26 oob: super::Oob,
29 na_value: (u8, u8, u8),
33 value_formatter: Option<UnwindSafeFormatter>,
37}
38
39impl std::fmt::Debug for ScaleColorGradient2 {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.debug_struct("ScaleColorGradient2")
43 .field("aesthetic", &self.aesthetic)
44 .field("name", &self.name)
45 .field("low", &self.low)
46 .field("mid", &self.mid)
47 .field("high", &self.high)
48 .field("midpoint", &self.midpoint)
49 .field("min", &self.min)
50 .field("max", &self.max)
51 .field("locale", &self.locale)
52 .field("limits", &self.limits)
53 .field("oob", &self.oob)
54 .field("na_value", &self.na_value)
55 .field("value_formatter", &self.value_formatter.is_some())
56 .finish()
57 }
58}
59
60impl ScaleColorGradient2 {
61 pub fn new(aesthetic: Aesthetic) -> Self {
62 ScaleColorGradient2 {
63 aesthetic,
64 name: String::new(),
65 low: RGBAColor::new(0, 0, 255),
66 mid: RGBAColor::new(255, 255, 255),
67 high: RGBAColor::new(255, 0, 0),
68 midpoint: 0.0,
69 min: f64::INFINITY,
70 max: f64::NEG_INFINITY,
71 locale: None,
72 limits: None,
73 oob: super::Oob::Censor,
74 na_value: super::DEFAULT_NA_VALUE,
75 value_formatter: None,
76 }
77 }
78
79 pub fn with_value_formatter<F>(mut self, f: F) -> Self
84 where
85 F: Fn(f64) -> String + Send + Sync + 'static,
86 {
87 self.value_formatter = Some(UnwindSafeFormatter(std::sync::Arc::new(f)));
88 self
89 }
90
91 pub fn with_colors(mut self, low: RGBAColor, mid: RGBAColor, high: RGBAColor) -> Self {
92 self.low = low;
93 self.mid = mid;
94 self.high = high;
95 self
96 }
97
98 pub fn with_midpoint(mut self, midpoint: f64) -> Self {
99 self.midpoint = midpoint;
100 self
101 }
102
103 pub fn with_limits(mut self, lo: f64, hi: f64) -> Self {
107 self.limits = Some((lo, hi));
108 self
109 }
110
111 pub fn with_oob(mut self, oob: super::Oob) -> Self {
115 self.oob = oob;
116 self
117 }
118
119 pub fn with_na_value(mut self, color: (u8, u8, u8)) -> Self {
123 self.na_value = color;
124 self
125 }
126}
127
128impl Scale for ScaleColorGradient2 {
129 fn set_default_locale(&mut self, locale: &crate::locale::Locale) {
130 if self.locale.is_none() {
131 self.locale = Some(locale.clone());
132 }
133 }
134
135 fn locale(&self) -> Option<&crate::locale::Locale> {
136 self.locale.as_ref()
137 }
138
139 fn format_value(&self, value: &Value) -> Option<String> {
140 let f = value.as_f64()?;
141 if !f.is_finite() {
142 return None;
143 }
144 Some(match &self.value_formatter {
145 Some(fmt) => fmt.call(f),
146 None => super::util::format_number_opt(f, self.locale.as_ref()),
147 })
148 }
149
150 fn aesthetic(&self) -> Aesthetic {
151 self.aesthetic.clone()
152 }
153
154 fn train(&mut self, values: &[Value]) {
155 if self.limits.is_some() {
156 return;
157 }
158 for v in values {
159 if let Some(f) = v.as_f64() {
160 if f.is_finite() {
161 if f < self.min {
162 self.min = f;
163 }
164 if f > self.max {
165 self.max = f;
166 }
167 }
168 }
169 }
170 }
171
172 fn map(&self, value: &Value) -> f64 {
173 let f = match value.as_f64() {
174 Some(f) => f,
175 None => return 0.0,
176 };
177 let (lo, hi) = self.limits.unwrap_or((self.min, self.max));
178 let range = hi - lo;
179 if range.abs() < f64::EPSILON {
180 0.5
181 } else {
182 (f - lo) / range
183 }
184 }
185
186 fn breaks(&self) -> Vec<(f64, String)> {
187 let (lo, hi) = self.limits.unwrap_or((self.min, self.max));
188 if lo > hi || !lo.is_finite() || !hi.is_finite() {
189 return vec![];
190 }
191 let range = hi - lo;
192 if super::util::is_degenerate_range(lo, hi) {
193 return vec![(
194 0.5,
195 super::util::format_number_opt(lo, self.locale.as_ref()),
196 )];
197 }
198 let n_breaks = 5;
199 let raw_step = range / n_breaks as f64;
200 let step = nice_step(raw_step);
201 let start = (lo / step).ceil() * step;
202 let values = super::util::stepped_breaks(start, hi, step);
203 let labels = super::util::format_numbers_opt(&values, self.locale.as_ref());
204 values
205 .iter()
206 .zip(labels)
207 .map(|(&v, l)| (self.map(&Value::Float(v)), l))
208 .collect()
209 }
210
211 fn name(&self) -> &str {
212 &self.name
213 }
214
215 fn set_name(&mut self, name: &str) {
216 self.name = name.to_string();
217 }
218
219 fn map_to_color(&self, value: &Value) -> Option<(u8, u8, u8)> {
220 if value.is_na() {
226 return Some(self.na_value);
227 }
228 let Some(f) = value.as_f64() else {
229 return Some(self.na_value);
230 };
231 if !f.is_finite() {
232 return Some(self.na_value);
233 }
234 let (lo, hi) = self.limits.unwrap_or((self.min, self.max));
235 let Some(f) = super::resolve_oob(f, self.limits, self.oob) else {
236 return Some(self.na_value);
237 };
238
239 let c = if f <= self.midpoint {
241 let range = self.midpoint - lo;
242 let t = if range.abs() < f64::EPSILON {
243 0.0
244 } else {
245 (f - lo) / range
246 };
247 self.low.lerp(&self.mid, t.clamp(0.0, 1.0))
248 } else {
249 let range = hi - self.midpoint;
250 let t = if range.abs() < f64::EPSILON {
251 1.0
252 } else {
253 (f - self.midpoint) / range
254 };
255 self.mid.lerp(&self.high, t.clamp(0.0, 1.0))
256 };
257
258 Some((c.r, c.g, c.b))
259 }
260
261 fn domain(&self) -> Option<(f64, f64)> {
262 if let Some((lo, hi)) = self.limits {
263 return Some((lo, hi));
264 }
265 if self.min.is_finite() && self.max.is_finite() && self.min <= self.max {
266 Some((self.min, self.max))
267 } else {
268 None
269 }
270 }
271
272 fn clone_box(&self) -> Box<dyn Scale> {
273 Box::new(self.clone())
274 }
275
276 fn reset_training(&mut self) {
277 self.min = f64::INFINITY;
278 self.max = f64::NEG_INFINITY;
279 }
280
281 fn validate(&self, warnings: &mut Vec<String>) {
282 if let Some((lo, hi)) = self.limits {
283 if lo > hi {
284 warnings.push(format!(
285 "scale_color/fill_gradient2: with_limits({lo}, {hi}) has lo > hi; every value maps to na_value"
286 ));
287 }
288 }
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn format_value_formats_a_finite_number_and_rejects_non_finite() {
298 let s = ScaleColorGradient2::new(Aesthetic::Color);
299 assert_eq!(s.format_value(&Value::Float(7.0)), Some("7".to_string()));
300 assert_eq!(s.format_value(&Value::Float(f64::NEG_INFINITY)), None);
301 }
302
303 #[test]
304 fn with_value_formatter_overrides_format_value() {
305 let s =
306 ScaleColorGradient2::new(Aesthetic::Color).with_value_formatter(|v| format!("{v:.1}%"));
307 assert_eq!(s.format_value(&Value::Float(7.0)), Some("7.0%".to_string()));
308 assert_eq!(s.format_value(&Value::Float(f64::NEG_INFINITY)), None);
309 }
310
311 #[test]
312 fn debug_reports_whether_a_formatter_is_set() {
313 let without = format!("{:?}", ScaleColorGradient2::new(Aesthetic::Color));
314 assert!(without.contains("ScaleColorGradient2"));
315 assert!(without.contains("value_formatter: false"));
316 let with = format!(
317 "{:?}",
318 ScaleColorGradient2::new(Aesthetic::Color).with_value_formatter(|v| v.to_string())
319 );
320 assert!(with.contains("value_formatter: true"));
321 }
322
323 #[test]
325 fn scale_color_gradient2_keeps_every_auto_trait() {
326 fn assert_traits<
327 T: Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe + Clone,
328 >() {
329 }
330 assert_traits::<ScaleColorGradient2>();
331 }
332
333 #[test]
334 fn with_limits_fixes_the_domain_and_training_does_not_widen_it() {
335 let mut s = ScaleColorGradient2::new(Aesthetic::Fill).with_limits(-2.0, 2.0);
336 s.train(&[Value::Float(-50.0), Value::Float(50.0)]);
337 assert_eq!(s.domain(), Some((-2.0, 2.0)));
338 }
339}