1use motion_core::ValidationError;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6enum Mapping {
7 Linear,
8 Log10,
9}
10
11#[derive(Clone, Copy, Debug, PartialEq)]
27pub struct Scale {
28 domain: (f64, f64),
29 transformed: (f64, f64),
30 range: (f64, f64),
31 mapping: Mapping,
32 clamped: bool,
33}
34
35impl Scale {
36 pub fn linear(domain: (f64, f64), range: (f64, f64)) -> Result<Self, ValidationError> {
38 Self::new(domain, range, Mapping::Linear)
39 }
40
41 pub fn log10(domain: (f64, f64), range: (f64, f64)) -> Result<Self, ValidationError> {
43 Self::new(domain, range, Mapping::Log10)
44 }
45
46 pub fn clamp(mut self, enabled: bool) -> Self {
49 self.clamped = enabled;
50 self
51 }
52
53 pub fn domain(self) -> (f64, f64) {
55 self.domain
56 }
57
58 pub fn range(self) -> (f64, f64) {
60 self.range
61 }
62
63 pub fn map(self, mut value: f64) -> Result<f64, ValidationError> {
66 if !value.is_finite() || (self.mapping == Mapping::Log10 && value <= 0.0) {
67 return Err(invalid());
68 }
69 if self.clamped {
70 value = value.clamp(
71 self.domain.0.min(self.domain.1),
72 self.domain.0.max(self.domain.1),
73 );
74 }
75 if value == self.domain.0 {
76 return Ok(self.range.0);
77 }
78 if value == self.domain.1 {
79 return Ok(self.range.1);
80 }
81 let value = match self.mapping {
82 Mapping::Linear => value,
83 Mapping::Log10 => libm::log10(value),
84 };
85 let fraction = normalize(value, self.transformed);
86 let mapped = interpolate(self.range, fraction);
87 if mapped.is_finite() {
88 Ok(mapped)
89 } else {
90 Err(invalid())
91 }
92 }
93
94 pub fn invert(self, mut coordinate: f64) -> Result<f64, ValidationError> {
96 if !coordinate.is_finite() {
97 return Err(invalid());
98 }
99 if self.clamped {
100 coordinate = coordinate.clamp(
101 self.range.0.min(self.range.1),
102 self.range.0.max(self.range.1),
103 );
104 }
105 let fraction = normalize(coordinate, self.range);
106 self.value_at(fraction)
107 }
108
109 pub fn ticks(self, count: usize) -> Result<Vec<f64>, ValidationError> {
114 if !(2..=256).contains(&count) {
115 return Err(invalid());
116 }
117 let mut ticks = Vec::with_capacity(count);
118 for index in 0..count {
119 let value = self.value_at(index as f64 / (count - 1) as f64)?;
120 if ticks.last().is_some_and(|previous| {
121 if self.domain.1 > self.domain.0 {
122 value <= *previous
123 } else {
124 value >= *previous
125 }
126 }) {
127 return Err(invalid());
128 }
129 ticks.push(value);
130 }
131 Ok(ticks)
132 }
133
134 pub fn nice_ticks(self, max_count: usize) -> Result<Vec<f64>, ValidationError> {
141 if !(2..=256).contains(&max_count) {
142 return Err(invalid());
143 }
144 let lower = self.domain.0.min(self.domain.1);
145 let upper = self.domain.0.max(self.domain.1);
146 let log_lower = self.transformed.0.min(self.transformed.1);
147 let log_upper = self.transformed.0.max(self.transformed.1);
148 let logarithmic =
149 self.mapping == Mapping::Log10 && log_upper.floor() - log_lower.ceil() >= 1.0;
150 let mut ticks = if logarithmic {
151 let step = ((log_upper.floor() - log_lower.ceil()) / (max_count - 1) as f64)
152 .ceil()
153 .max(1.0);
154 tick_grid((log_lower, log_upper), step, max_count)?
155 .into_iter()
156 .map(|exponent| libm::pow(10.0, exponent))
157 .filter(|value| *value >= lower && *value <= upper)
158 .collect::<Vec<_>>()
159 } else {
160 let intervals = (max_count - 1) as f64;
161 let span = upper - lower;
162 let raw = if span.is_finite() {
163 span / intervals
164 } else {
165 upper / intervals - lower / intervals
166 }
167 .max(f64::from_bits(1));
168 let base = libm::pow(10.0, libm::log10(raw).floor()).max(f64::from_bits(1));
169 let ratio = raw / base;
170 let factor = if ratio <= 1.0 {
171 1.0
172 } else if ratio <= 2.0 {
173 2.0
174 } else if ratio <= 5.0 {
175 5.0
176 } else {
177 10.0
178 };
179 let mut step = factor * base;
180 if (upper / step).floor() - (lower / step).ceil() + 1.0 > max_count as f64 {
181 step = if factor == 1.0 {
182 2.0
183 } else if factor == 2.0 {
184 5.0
185 } else if factor == 5.0 {
186 10.0
187 } else {
188 20.0
189 } * base;
190 }
191 tick_grid((lower, upper), step, max_count)?
192 };
193 if ticks.iter().any(|value| !value.is_finite())
194 || ticks.windows(2).any(|pair| pair[0] >= pair[1])
195 {
196 return Err(invalid());
197 }
198 if self.domain.0 > self.domain.1 {
199 ticks.reverse();
200 }
201 Ok(ticks)
202 }
203
204 fn new(
205 domain: (f64, f64),
206 range: (f64, f64),
207 mapping: Mapping,
208 ) -> Result<Self, ValidationError> {
209 if ![domain.0, domain.1, range.0, range.1]
210 .iter()
211 .all(|value| value.is_finite())
212 || domain.0 == domain.1
213 || range.0 == range.1
214 || (mapping == Mapping::Log10 && (domain.0 <= 0.0 || domain.1 <= 0.0))
215 {
216 return Err(invalid());
217 }
218 let transformed = match mapping {
219 Mapping::Linear => domain,
220 Mapping::Log10 => (libm::log10(domain.0), libm::log10(domain.1)),
221 };
222 if transformed.0 == transformed.1 {
223 return Err(invalid());
224 }
225 Ok(Self {
226 domain,
227 transformed,
228 range,
229 mapping,
230 clamped: false,
231 })
232 }
233
234 fn value_at(self, fraction: f64) -> Result<f64, ValidationError> {
235 if !fraction.is_finite() {
236 return Err(invalid());
237 }
238 if fraction == 0.0 {
239 return Ok(self.domain.0);
240 }
241 if fraction == 1.0 {
242 return Ok(self.domain.1);
243 }
244 let transformed = interpolate(self.transformed, fraction);
245 let value = match self.mapping {
246 Mapping::Linear => transformed,
247 Mapping::Log10 => libm::pow(10.0, transformed),
248 };
249 if value.is_finite() && (self.mapping == Mapping::Linear || value > 0.0) {
250 Ok(value)
251 } else {
252 Err(invalid())
253 }
254 }
255}
256
257fn tick_grid(bounds: (f64, f64), step: f64, max_count: usize) -> Result<Vec<f64>, ValidationError> {
258 if !step.is_finite() || step <= 0.0 {
259 return Err(invalid());
260 }
261 let first = (bounds.0 / step).ceil();
262 let last = (bounds.1 / step).floor();
263 if !first.is_finite()
264 || !last.is_finite()
265 || first.abs() > 9007199254740992.0
266 || last.abs() > 9007199254740992.0
267 {
268 return Err(invalid());
269 }
270 if first > last {
271 return Ok(Vec::new());
272 }
273 let count = last - first + 1.0;
274 if count > max_count as f64 {
275 return Err(invalid());
276 }
277 let mut ticks = Vec::with_capacity(count as usize);
278 for offset in 0..count as usize {
279 let value = (first + offset as f64) * step;
280 if value >= bounds.0 && value <= bounds.1 {
281 ticks.push(if value == 0.0 { 0.0 } else { value });
282 }
283 }
284 Ok(ticks)
285}
286
287fn normalize(value: f64, endpoints: (f64, f64)) -> f64 {
288 let span = endpoints.1 - endpoints.0;
289 let offset = value - endpoints.0;
290 if span.is_finite() && offset.is_finite() {
291 offset / span
292 } else {
293 (value * 0.5 - endpoints.0 * 0.5) / (endpoints.1 * 0.5 - endpoints.0 * 0.5)
294 }
295}
296
297fn interpolate(endpoints: (f64, f64), fraction: f64) -> f64 {
298 let span = endpoints.1 - endpoints.0;
299 if span.is_finite() {
300 endpoints.0 + span * fraction
301 } else {
302 endpoints.0 * (1.0 - fraction) + endpoints.1 * fraction
303 }
304}
305
306fn invalid() -> ValidationError {
307 ValidationError::invalid(
308 "scale",
309 "domain",
310 "Scale requires distinct finite endpoints, positive log data, representable results/steps/indices and a tick count in 2..=256",
311 )
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn nice_linear_ticks_use_aligned_steps_without_forcing_endpoints() -> Result<(), ValidationError>
320 {
321 assert_eq!(
322 Scale::linear((3.0, 97.0), (0.0, 100.0))?.nice_ticks(6)?,
323 [20.0, 40.0, 60.0, 80.0]
324 );
325 assert_eq!(
326 Scale::linear((-97.0, -3.0), (0.0, 100.0))?.nice_ticks(6)?,
327 [-80.0, -60.0, -40.0, -20.0]
328 );
329 let ticks = Scale::linear((-3.0, 9.0), (0.0, 100.0))?.nice_ticks(5)?;
330 assert_eq!(ticks, [0.0, 5.0]);
331 assert_eq!(ticks[0].to_bits(), 0.0_f64.to_bits());
332 Ok(())
333 }
334
335 #[test]
336 fn nice_log_ticks_select_decades_and_fall_back_for_narrow_domains()
337 -> Result<(), ValidationError> {
338 let log = Scale::log10((0.1, 1000.0), (100.0, 860.0))?;
339 assert_eq!(log.nice_ticks(5)?, [0.1, 1.0, 10.0, 100.0, 1000.0]);
340 assert_eq!(log.nice_ticks(3)?, [1.0, 100.0]);
341 assert_eq!(
342 Scale::log10((2.0, 8.0), (0.0, 100.0))?.nice_ticks(5)?,
343 [2.0, 4.0, 6.0, 8.0]
344 );
345 Ok(())
346 }
347
348 #[test]
349 fn nice_ticks_follow_domain_order_not_range_clamp_or_evaluation_order()
350 -> Result<(), ValidationError> {
351 let forward = Scale::linear((3.0, 97.0), (100.0, 0.0))?;
352 let backward = Scale::linear((97.0, 3.0), (0.0, 100.0))?;
353 assert_eq!(backward.nice_ticks(6)?, [80.0, 60.0, 40.0, 20.0]);
354 let original = forward.nice_ticks(6)?;
355 for count in [2, 256, 3, 6] {
356 forward.nice_ticks(count)?;
357 }
358 assert_eq!(forward.clamp(true).nice_ticks(6)?, original);
359 assert_eq!(forward.nice_ticks(6)?, original);
360 assert_eq!(forward.domain(), (3.0, 97.0));
361 assert_eq!(forward.range(), (100.0, 0.0));
362 Ok(())
363 }
364
365 #[test]
366 fn nice_ticks_are_bounded_finite_ordered_and_inside_extreme_domains()
367 -> Result<(), ValidationError> {
368 for scale in [
369 Scale::linear((-1e100, 1e100), (0.0, 100.0))?,
370 Scale::linear((-0.07, 0.23), (0.0, 100.0))?,
371 Scale::linear((1e-250, 9e-250), (0.0, 100.0))?,
372 Scale::linear((f64::from_bits(1), f64::from_bits(5)), (0.0, 100.0))?,
373 Scale::log10((1e-300, 1e300), (0.0, 100.0))?,
374 ] {
375 for count in 2..=256 {
376 let ticks = scale.nice_ticks(count)?;
377 assert!(ticks.len() <= count);
378 assert!(ticks.windows(2).all(|pair| pair[0] < pair[1]));
379 assert!(ticks.iter().all(|value| value.is_finite()
380 && *value >= scale.domain().0
381 && *value <= scale.domain().1));
382 }
383 }
384 Ok(())
385 }
386
387 #[test]
388 fn nice_ticks_reject_invalid_budgets_and_unrepresentable_grid_steps_or_indices()
389 -> Result<(), ValidationError> {
390 let scale = Scale::linear((0.0, 100.0), (0.0, 100.0))?;
391 assert!(scale.nice_ticks(1).is_err());
392 assert!(scale.nice_ticks(257).is_err());
393 assert!(
394 Scale::linear((1.0, 1.0 + f64::EPSILON), (0.0, 100.0))?
395 .nice_ticks(5)
396 .is_err()
397 );
398 assert!(
399 Scale::linear((0.0, f64::MAX), (0.0, 100.0))?
400 .nice_ticks(2)
401 .is_err()
402 );
403 Ok(())
404 }
405
406 #[test]
407 fn linear_and_logarithmic_mappings_have_exact_endpoints_and_correct_midpoints()
408 -> Result<(), ValidationError> {
409 let linear = Scale::linear((0.0, 100.0), (20.0, 220.0))?;
410 assert_eq!(linear.map(50.0)?, 120.0);
411 assert_eq!(linear.invert(120.0)?, 50.0);
412 let log = Scale::log10((0.1, 1000.0), (100.0, 860.0))?;
413 assert_eq!(log.map(0.1)?, 100.0);
414 assert_eq!(log.map(1000.0)?, 860.0);
415 assert_eq!(log.invert(100.0)?, 0.1);
416 assert_eq!(log.invert(860.0)?, 1000.0);
417 assert!((log.map(10.0)? - 480.0).abs() < 0.001);
418 assert!((log.invert(480.0)? - 10.0).abs() < 1e-10);
419 Ok(())
420 }
421
422 #[test]
423 fn reversed_domains_ranges_and_clamping_are_explicit_and_replaceable()
424 -> Result<(), ValidationError> {
425 let scale = Scale::linear((100.0, 0.0), (0.0, 200.0))?;
426 assert_eq!(scale.map(75.0)?, 50.0);
427 assert_eq!(scale.map(150.0)?, -100.0);
428 assert_eq!(scale.clamp(true).map(150.0)?, 0.0);
429 assert_eq!(scale.clamp(true).invert(300.0)?, 0.0);
430 assert_eq!(scale.clamp(true).clamp(false), scale);
431 let log = Scale::log10((1.0, 100.0), (200.0, 0.0))?;
432 assert!((log.map(10.0)? - 100.0).abs() < 0.001);
433 assert_eq!(log.clamp(true).map(1000.0)?, 0.0);
434 assert_eq!(log.clamp(true).invert(300.0)?, 1.0);
435 assert!(log.clamp(true).map(0.0).is_err());
436 Ok(())
437 }
438
439 #[test]
440 fn huge_small_and_opposite_signed_domains_do_not_need_f32_data_coordinates()
441 -> Result<(), ValidationError> {
442 let scale = Scale::linear((-f64::MAX, f64::MAX), (0.0, 100.0))?;
443 assert_eq!(scale.map(0.0)?, 50.0);
444 assert_eq!(scale.invert(50.0)?, 0.0);
445 let scale = Scale::linear((0.0, 1e30), (0.0, 1000.0))?;
446 assert!((scale.map(5e29)? - 500.0).abs() < 0.001);
447 let scale = Scale::log10((1e-300, 1e300), (0.0, 600.0))?;
448 assert_eq!(scale.map(1.0)?, 300.0);
449 assert_eq!(scale.invert(300.0)?, 1.0);
450 let scale = Scale::linear((f64::from_bits(1), f64::from_bits(2)), (0.0, 100.0))?;
451 assert_eq!(scale.map(f64::from_bits(1))?, 0.0);
452 assert_eq!(scale.invert(100.0)?, f64::from_bits(2));
453 Ok(())
454 }
455
456 #[test]
457 fn ticks_are_bounded_ordered_data_values_not_rounded_or_pixel_quantized()
458 -> Result<(), ValidationError> {
459 let linear = Scale::linear((0.0, 100.0), (20.0, 220.0))?;
460 assert_eq!(linear.ticks(5)?, [0.0, 25.0, 50.0, 75.0, 100.0]);
461 let descending = Scale::linear((100.0, 0.0), (20.0, 220.0))?;
462 assert_eq!(descending.ticks(3)?, [100.0, 50.0, 0.0]);
463 let log = Scale::log10((1.0, 10000.0), (0.0, 1.0))?;
464 assert_eq!(log.ticks(5)?, [1.0, 10.0, 100.0, 1000.0, 10000.0]);
465 assert!(linear.ticks(1).is_err());
466 assert!(linear.ticks(257).is_err());
467 let narrow = Scale::linear((1.0, 1.0 + f64::EPSILON), (0.0, 100.0))?;
468 assert!(narrow.ticks(3).is_err());
469 Ok(())
470 }
471
472 #[test]
473 fn finite_round_trips_replay_in_any_order() -> Result<(), ValidationError> {
474 for scale in [
475 Scale::linear((-20.0, 500.0), (40.0, 800.0))?,
476 Scale::log10((0.01, 1e6), (800.0, 40.0))?,
477 ] {
478 for coordinate in [420.0, 40.0, 800.0, 200.0, 420.0] {
479 let value = scale.invert(coordinate)?;
480 assert!((scale.map(value)? - coordinate).abs() < 0.001);
481 }
482 }
483 Ok(())
484 }
485
486 #[test]
487 fn drawing_coordinates_retain_precision_beyond_f32() -> Result<(), ValidationError> {
488 let scale = Scale::linear((0.0, 1.0), (100_000_000.125, 100_000_000.625))?;
489 assert_eq!(scale.map(0.5)?, 100_000_000.375);
490 assert_eq!(scale.invert(100_000_000.375)?, 0.5);
491 assert_eq!(scale.map(0.5)?, scale.map(0.5)?);
492 Ok(())
493 }
494
495 #[test]
496 fn invalid_domains_inputs_and_unrepresentable_extrapolation_fail() -> Result<(), ValidationError>
497 {
498 assert!(Scale::linear((1.0, 1.0), (0.0, 100.0)).is_err());
499 assert!(Scale::linear((0.0, f64::INFINITY), (0.0, 100.0)).is_err());
500 assert!(Scale::linear((0.0, 1.0), (2.0, 2.0)).is_err());
501 assert!(Scale::linear((0.0, 1.0), (f64::NAN, 2.0)).is_err());
502 assert!(Scale::log10((0.0, 100.0), (0.0, 100.0)).is_err());
503 assert!(Scale::log10((-1.0, 100.0), (0.0, 100.0)).is_err());
504 let scale = Scale::linear((0.0, 1.0), (0.0, 100.0))?;
505 assert!(scale.map(f64::NAN).is_err());
506 assert!(scale.map(f64::MAX).is_err());
507 assert!(scale.invert(f64::INFINITY).is_err());
508 assert!(
509 Scale::log10((1.0, 10.0), (0.0, 1.0))?
510 .invert(1000.0)
511 .is_err()
512 );
513 Ok(())
514 }
515}