1#[derive(Clone, Debug)]
9pub enum Scale {
10 Linear {
12 domain: (f64, f64),
14 range: (f32, f32),
16 },
17 Log {
19 domain: (f64, f64),
21 range: (f32, f32),
23 base: f64,
25 },
26 Band {
28 domain: Vec<String>,
30 range: (f32, f32),
32 padding: f32,
34 },
35 Time {
37 domain: (i64, i64),
39 range: (f32, f32),
41 },
42 Sqrt {
44 domain: (f64, f64),
46 range: (f32, f32),
48 },
49 Power {
51 domain: (f64, f64),
53 range: (f32, f32),
55 exponent: f64,
57 },
58 Symlog {
60 domain: (f64, f64),
62 range: (f32, f32),
64 constant: f64,
66 },
67 Ordinal {
69 domain: Vec<String>,
71 range: Vec<f32>,
73 },
74}
75
76impl Scale {
77 pub fn map(&self, value: f64) -> f32 {
79 match self {
80 Self::Linear { domain, range } => {
81 let t = if (domain.1 - domain.0).abs() < 1e-15 {
82 0.5
83 } else {
84 (value - domain.0) / (domain.1 - domain.0)
85 };
86 range.0 + (range.1 - range.0) * t as f32
87 }
88 Self::Log {
89 domain,
90 range,
91 base,
92 } => {
93 let log_val = value.max(1e-15).log(*base);
94 let log_min = domain.0.max(1e-15).log(*base);
95 let log_max = domain.1.max(1e-15).log(*base);
96 let t = if (log_max - log_min).abs() < 1e-15 {
97 0.5
98 } else {
99 (log_val - log_min) / (log_max - log_min)
100 };
101 range.0 + (range.1 - range.0) * t as f32
102 }
103 Self::Band {
104 domain,
105 range,
106 padding,
107 } => {
108 if domain.is_empty() {
110 return (range.0 + range.1) * 0.5;
111 }
112 let total = range.1 - range.0;
113 let n = domain.len() as f32;
114 let band_width = total / (n + (n + 1.0) * padding);
115 let step = band_width + band_width * padding;
116 let idx = domain
118 .iter()
119 .position(|s| {
120 let v_str = format!("{value}");
122 s == &v_str
123 })
124 .unwrap_or(0) as f32;
125 range.0 + step * padding + idx * step + band_width * 0.5
126 }
127 Self::Time { domain, range } => {
128 let t = if domain.1 == domain.0 {
129 0.5
130 } else {
131 (value as i64 - domain.0) as f64 / (domain.1 - domain.0) as f64
132 };
133 range.0 + (range.1 - range.0) * t as f32
134 }
135 Self::Sqrt { domain, range } => {
136 let sqrt_val = value.max(0.0).sqrt();
137 let sqrt_min = domain.0.max(0.0).sqrt();
138 let sqrt_max = domain.1.max(0.0).sqrt();
139 let t = if (sqrt_max - sqrt_min).abs() < 1e-15 {
140 0.5
141 } else {
142 (sqrt_val - sqrt_min) / (sqrt_max - sqrt_min)
143 };
144 range.0 + (range.1 - range.0) * t as f32
145 }
146 Self::Power {
147 domain,
148 range,
149 exponent,
150 } => {
151 let pow_val = value.max(0.0).powf(*exponent);
152 let pow_min = domain.0.max(0.0).powf(*exponent);
153 let pow_max = domain.1.max(0.0).powf(*exponent);
154 let t = if (pow_max - pow_min).abs() < 1e-15 {
155 0.5
156 } else {
157 (pow_val - pow_min) / (pow_max - pow_min)
158 };
159 range.0 + (range.1 - range.0) * t as f32
160 }
161 Self::Symlog {
162 domain,
163 range,
164 constant,
165 } => {
166 let symlog = |v: f64| v.signum() * (v.abs() / constant).ln_1p();
167 let sl_val = symlog(value);
168 let sl_min = symlog(domain.0);
169 let sl_max = symlog(domain.1);
170 let t = if (sl_max - sl_min).abs() < 1e-15 {
171 0.5
172 } else {
173 (sl_val - sl_min) / (sl_max - sl_min)
174 };
175 range.0 + (range.1 - range.0) * t as f32
176 }
177 Self::Ordinal { domain, range } => {
178 let idx = domain
180 .iter()
181 .position(|s| {
182 let v_str = format!("{value}");
183 s == &v_str
184 })
185 .unwrap_or(0);
186 range.get(idx).copied().unwrap_or(0.0)
187 }
188 }
189 }
190
191 pub fn map_band(&self, category: &str) -> Option<(f32, f32)> {
193 match self {
194 Self::Band {
195 domain,
196 range,
197 padding,
198 } => {
199 let idx = domain.iter().position(|s| s == category)?;
200 let total = range.1 - range.0;
201 let n = domain.len() as f32;
202 let band_width = total / (n + (n + 1.0) * padding);
203 let step = band_width + band_width * padding;
204 let center = range.0 + step * padding + idx as f32 * step + band_width * 0.5;
205 Some((center, band_width))
206 }
207 _ => None,
208 }
209 }
210
211 pub fn invert(&self, visual: f32) -> f64 {
213 match self {
214 Self::Linear { domain, range } => {
215 let t = if (range.1 - range.0).abs() < 1e-10 {
216 0.5
217 } else {
218 (visual - range.0) / (range.1 - range.0)
219 };
220 domain.0 + (domain.1 - domain.0) * f64::from(t)
221 }
222 Self::Log {
223 domain,
224 range,
225 base,
226 } => {
227 let t = if (range.1 - range.0).abs() < 1e-10 {
228 0.5
229 } else {
230 (visual - range.0) / (range.1 - range.0)
231 };
232 let log_min = domain.0.max(1e-15).log(*base);
233 let log_max = domain.1.max(1e-15).log(*base);
234 let log_val = log_min + (log_max - log_min) * f64::from(t);
235 base.powf(log_val)
236 }
237 Self::Time { domain, range } => {
238 let t = if (range.1 - range.0).abs() < 1e-10 {
239 0.5
240 } else {
241 (visual - range.0) / (range.1 - range.0)
242 };
243 domain.0 as f64 + (domain.1 - domain.0) as f64 * f64::from(t)
244 }
245 Self::Band { .. } | Self::Ordinal { .. } => 0.0, Self::Sqrt { domain, range } => {
247 let t = if (range.1 - range.0).abs() < 1e-10 {
248 0.5
249 } else {
250 (visual - range.0) / (range.1 - range.0)
251 };
252 let sqrt_min = domain.0.max(0.0).sqrt();
253 let sqrt_max = domain.1.max(0.0).sqrt();
254 let sqrt_val = sqrt_min + (sqrt_max - sqrt_min) * f64::from(t);
255 sqrt_val * sqrt_val
256 }
257 Self::Power {
258 domain,
259 range,
260 exponent,
261 } => {
262 let t = if (range.1 - range.0).abs() < 1e-10 {
263 0.5
264 } else {
265 (visual - range.0) / (range.1 - range.0)
266 };
267 let pow_min = domain.0.max(0.0).powf(*exponent);
268 let pow_max = domain.1.max(0.0).powf(*exponent);
269 let pow_val = pow_min + (pow_max - pow_min) * f64::from(t);
270 pow_val.powf(1.0 / exponent)
271 }
272 Self::Symlog {
273 domain,
274 range,
275 constant,
276 } => {
277 let symlog = |v: f64| v.signum() * (v.abs() / constant).ln_1p();
278 let t = if (range.1 - range.0).abs() < 1e-10 {
279 0.5
280 } else {
281 (visual - range.0) / (range.1 - range.0)
282 };
283 let sl_min = symlog(domain.0);
284 let sl_max = symlog(domain.1);
285 let sl_val = sl_min + (sl_max - sl_min) * f64::from(t);
286 sl_val.signum() * constant * (sl_val.abs()).exp_m1()
288 }
289 }
290 }
291
292 pub fn ticks(&self, target_count: usize) -> Vec<f64> {
294 match self {
295 Self::Linear { domain, .. }
296 | Self::Sqrt { domain, .. }
297 | Self::Power { domain, .. }
298 | Self::Symlog { domain, .. } => nice_ticks_linear(domain.0, domain.1, target_count),
299 Self::Log { domain, base, .. } => nice_ticks_log(domain.0, domain.1, *base),
300 Self::Band { domain, .. } => (0..domain.len()).map(|i| i as f64).collect(),
301 Self::Time { domain, .. } => {
302 nice_ticks_linear(domain.0 as f64, domain.1 as f64, target_count)
303 }
304 Self::Ordinal { domain, .. } => (0..domain.len()).map(|i| i as f64).collect(),
305 }
306 }
307
308 pub fn nice(&self, target_count: usize) -> Self {
314 match self {
315 Self::Linear { domain, range } => {
316 let (min, max) = *domain;
317 if (max - min).abs() < 1e-15 {
318 return self.clone();
319 }
320 let step = tick_step(min, max, target_count);
321 let nice_min = (min / step).floor() * step;
322 let nice_max = (max / step).ceil() * step;
323 Self::Linear {
324 domain: (nice_min, nice_max),
325 range: *range,
326 }
327 }
328 _ => self.clone(),
330 }
331 }
332
333 pub fn format_tick(&self, value: f64) -> String {
335 match self {
336 Self::Band { domain, .. } | Self::Ordinal { domain, .. } => {
337 let idx = value as usize;
338 domain.get(idx).cloned().unwrap_or_default()
339 }
340 _ => format_number(value),
341 }
342 }
343}
344
345fn nice_ticks_linear(min: f64, max: f64, target_count: usize) -> Vec<f64> {
347 if (max - min).abs() < 1e-15 {
348 return vec![min];
349 }
350
351 let step = tick_step(min, max, target_count);
352
353 let graph_min = (min / step).floor() * step;
354 let graph_max = (max / step).ceil() * step;
355
356 let mut positions = Vec::new();
357 let mut v = graph_min;
358 let max_ticks = (target_count + 5) * 2;
359 while v <= graph_max + step * 0.5 && positions.len() < max_ticks {
360 positions.push(v);
361 v += step;
362 }
363 positions
364}
365
366fn nice_ticks_log(min: f64, max: f64, base: f64) -> Vec<f64> {
368 let log_min = min.max(1e-15).log(base).floor() as i32;
369 let log_max = max.max(1e-15).log(base).ceil() as i32;
370 (log_min..=log_max).map(|e| base.powi(e)).collect()
371}
372
373fn tick_step(start: f64, stop: f64, count: usize) -> f64 {
380 let step0 = (stop - start).abs() / count.max(1) as f64;
381 let mut step1 = 10.0_f64.powf(step0.log10().floor());
382 let error = step0 / step1;
383 if error >= 50.0_f64.sqrt() {
384 step1 *= 10.0;
386 } else if error >= 10.0_f64.sqrt() {
387 step1 *= 5.0;
389 } else if error >= 2.0_f64.sqrt() {
390 step1 *= 2.0;
392 }
393 step1
394}
395
396#[allow(dead_code)]
398fn nice_num(x: f64, round: bool) -> f64 {
399 let exp = x.abs().log10().floor();
400 let frac = x / 10.0_f64.powf(exp);
401
402 let nice_frac = if round {
403 if frac < 1.5 {
404 1.0
405 } else if frac < 3.0 {
406 2.0
407 } else if frac < 7.0 {
408 5.0
409 } else {
410 10.0
411 }
412 } else if frac <= 1.0 {
413 1.0
414 } else if frac <= 2.0 {
415 2.0
416 } else if frac <= 5.0 {
417 5.0
418 } else {
419 10.0
420 };
421
422 nice_frac * 10.0_f64.powf(exp)
423}
424
425pub fn format_number(value: f64) -> String {
430 if value == 0.0 {
431 return "0".to_string();
432 }
433 let abs = value.abs();
434 let sign = if value < 0.0 { "-" } else { "" };
435
436 if abs >= 1e9 {
437 let v = value / 1e9;
438 return format_si(v, sign, "B");
439 }
440 if abs >= 1e6 {
441 let v = value / 1e6;
442 return format_si(v, sign, "M");
443 }
444 if abs >= 1e4 {
445 let rounded = value.round() as i64;
447 return format_with_commas(rounded);
448 }
449 if abs >= 1.0 {
450 if (value - value.round()).abs() < 1e-9 {
452 return format!("{}", value as i64);
453 }
454 return format!("{value:.1}");
455 }
456 if abs >= 0.01 {
457 return format!("{value:.2}");
458 }
459 if abs >= 1e-6 {
460 let decimals = (-abs.log10().floor() as usize) + 2;
462 return format!("{value:.prec$}", prec = decimals.min(8));
463 }
464 format!("{value:.2e}")
466}
467
468fn format_si(v: f64, sign: &str, suffix: &str) -> String {
470 if (v.abs() - v.abs().round()).abs() < 0.05 {
471 format!("{sign}{}{suffix}", v.abs().round() as i64)
472 } else {
473 format!("{sign}{:.1}{suffix}", v.abs())
474 }
475}
476
477fn format_with_commas(value: i64) -> String {
479 let neg = value < 0;
480 let s = value.unsigned_abs().to_string();
481 let bytes = s.as_bytes();
482 let mut result = String::with_capacity(s.len() + s.len() / 3);
483 if neg {
484 result.push('-');
485 }
486 for (i, &b) in bytes.iter().enumerate() {
487 if i > 0 && (bytes.len() - i) % 3 == 0 {
488 result.push(',');
489 }
490 result.push(b as char);
491 }
492 result
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498
499 #[test]
500 fn linear_map() {
501 let s = Scale::Linear {
502 domain: (0.0, 100.0),
503 range: (0.0, 500.0),
504 };
505 assert!((s.map(50.0) - 250.0).abs() < 1e-3);
506 assert!((s.map(0.0)).abs() < 1e-3);
507 assert!((s.map(100.0) - 500.0).abs() < 1e-3);
508 }
509
510 #[test]
511 fn linear_invert() {
512 let s = Scale::Linear {
513 domain: (0.0, 100.0),
514 range: (0.0, 500.0),
515 };
516 assert!((s.invert(250.0) - 50.0).abs() < 1e-3);
517 }
518
519 #[test]
520 fn log_map() {
521 let s = Scale::Log {
522 domain: (1.0, 1000.0),
523 range: (0.0, 300.0),
524 base: 10.0,
525 };
526 assert!((s.map(1.0)).abs() < 1e-3);
527 assert!((s.map(1000.0) - 300.0).abs() < 1e-3);
528 assert!((s.map(10.0) - 100.0).abs() < 1e-3);
530 }
531
532 #[test]
533 fn band_map() {
534 let s = Scale::Band {
535 domain: vec!["A".into(), "B".into(), "C".into()],
536 range: (0.0, 300.0),
537 padding: 0.1,
538 };
539 let (center_a, width) = s.map_band("A").unwrap();
540 let (center_b, _) = s.map_band("B").unwrap();
541 assert!(center_a < center_b);
542 assert!(width > 0.0);
543 }
544
545 #[test]
546 fn sqrt_map() {
547 let s = Scale::Sqrt {
548 domain: (0.0, 100.0),
549 range: (0.0, 500.0),
550 };
551 assert!((s.map(0.0)).abs() < 1e-3);
552 assert!((s.map(100.0) - 500.0).abs() < 1e-3);
553 assert!((s.map(25.0) - 250.0).abs() < 1e-3);
555 }
556
557 #[test]
558 fn symlog_map() {
559 let s = Scale::Symlog {
560 domain: (-100.0, 100.0),
561 range: (0.0, 500.0),
562 constant: 1.0,
563 };
564 let mid = s.map(0.0);
566 assert!((mid - 250.0).abs() < 1e-3, "mid = {mid}");
567 let pos = s.map(50.0);
569 let neg = s.map(-50.0);
570 assert!((pos + neg - 500.0).abs() < 1e-2, "pos={pos}, neg={neg}");
571 }
572
573 #[test]
574 fn power_map() {
575 let s = Scale::Power {
576 domain: (0.0, 10.0),
577 range: (0.0, 100.0),
578 exponent: 2.0,
579 };
580 assert!((s.map(0.0)).abs() < 1e-3);
581 assert!((s.map(10.0) - 100.0).abs() < 1e-3);
582 assert!((s.map(5.0) - 25.0).abs() < 1e-3);
584 }
585
586 #[test]
587 fn ordinal_map() {
588 let s = Scale::Ordinal {
589 domain: vec!["low".into(), "med".into(), "high".into()],
590 range: vec![50.0, 150.0, 250.0],
591 };
592 let ticks = s.ticks(3);
595 assert_eq!(ticks, vec![0.0, 1.0, 2.0]);
596 assert_eq!(s.format_tick(0.0), "low");
597 assert_eq!(s.format_tick(2.0), "high");
598 }
599
600 #[test]
601 fn nice_ticks() {
602 let s = Scale::Linear {
603 domain: (0.0, 100.0),
604 range: (0.0, 500.0),
605 };
606 let ticks = s.ticks(5);
607 assert!(!ticks.is_empty());
608 assert!(ticks[0] <= 0.0);
609 assert!(*ticks.last().unwrap() >= 100.0);
610 }
611
612 #[test]
613 fn nice_expands_domain_to_tick_boundaries() {
614 let s = Scale::Linear {
615 domain: (3.7, 97.2),
616 range: (0.0, 500.0),
617 };
618 let niced = s.nice(5);
619 let Scale::Linear { domain, .. } = &niced else {
620 panic!("expected Linear");
621 };
622 assert!(domain.0 <= 3.7, "niced min {} should be <= 3.7", domain.0);
624 assert!(domain.1 >= 97.2, "niced max {} should be >= 97.2", domain.1);
625 let ticks = niced.ticks(5);
627 for &t in &ticks {
628 assert!(
629 t >= domain.0 - 1e-9 && t <= domain.1 + 1e-9,
630 "tick {} outside niced domain [{}, {}]",
631 t,
632 domain.0,
633 domain.1,
634 );
635 }
636 }
637}