1const MAX_START_CANDIDATES: f64 = 10_000.0;
4
5pub fn is_degenerate_range(dmin: f64, dmax: f64) -> bool {
8 let range = (dmax - dmin).abs();
9 range < f64::EPSILON || range <= 1e-10 * dmin.abs().max(dmax.abs())
10}
11
12pub fn stepped_breaks(start: f64, end: f64, step: f64) -> Vec<f64> {
17 const MAX_BREAKS: f64 = 1_000.0;
18 if !(start.is_finite() && end.is_finite() && step.is_finite()) || step <= 0.0 {
19 return vec![];
20 }
21 let n = ((end + step * 0.001 - start) / step).floor();
22 if !(0.0..=MAX_BREAKS).contains(&n) {
23 return vec![];
24 }
25 (0..=n as usize).map(|i| start + i as f64 * step).collect()
26}
27
28pub fn time_breaks(dmin: f64, dmax: f64, m: usize) -> Vec<f64> {
32 if !(dmin.is_finite() && dmax.is_finite()) || dmax <= dmin {
33 return vec![];
34 }
35 const STEPS: [f64; 18] = [
36 1.0, 2.0, 5.0, 10.0, 15.0, 30.0, 60.0, 120.0, 300.0, 600.0, 900.0, 1800.0, 3600.0, 7200.0,
37 10800.0, 21600.0, 43200.0, 86400.0,
38 ];
39 let span = dmax - dmin;
40 let m = m.max(2) as f64;
41 let step = STEPS
42 .iter()
43 .copied()
44 .find(|s| span / s <= m)
45 .unwrap_or_else(|| (span / m / 86400.0).ceil() * 86400.0);
46 let mut v = (dmin / step).ceil() * step;
47 let mut out = Vec::new();
48 while v <= dmax + step * 1e-9 {
49 out.push(v);
50 v += step;
51 }
52 out
53}
54
55pub fn format_hms(secs: f64) -> String {
58 if !secs.is_finite() {
59 return "NA".into();
60 }
61 let sign = if secs < 0.0 { "-" } else { "" };
62 let t = secs.abs().round() as u64;
63 format!("{sign}{:02}:{:02}:{:02}", t / 3600, (t / 60) % 60, t % 60)
64}
65
66pub fn extended_breaks(dmin: f64, dmax: f64, m: usize) -> Vec<f64> {
70 if !(dmin.is_finite() && dmax.is_finite()) || dmax <= dmin || m < 2 {
71 return vec![];
72 }
73 if is_degenerate_range(dmin, dmax) {
77 return vec![];
78 }
79 const Q: [f64; 6] = [1.0, 5.0, 2.0, 2.5, 4.0, 3.0];
82 const W: [f64; 4] = [0.25, 0.2, 0.5, 0.05];
83 let m = m as f64;
84
85 let simplicity = |qi: usize, j: f64, lmin: f64, lmax: f64, step: f64| {
86 let eps = 1e-10;
87 let modulo = lmin.rem_euclid(step);
88 let v = if (modulo < eps || step - modulo < eps) && lmin <= 0.0 && lmax >= 0.0 {
89 1.0
90 } else {
91 0.0
92 };
93 1.0 - qi as f64 / (Q.len() as f64 - 1.0) - j + v
94 };
95 let simplicity_max = |qi: usize, j: f64| 1.0 - qi as f64 / (Q.len() as f64 - 1.0) - j + 1.0;
96 let coverage = |lmin: f64, lmax: f64| {
97 let r = dmax - dmin;
98 1.0 - 0.5 * ((dmax - lmax).powi(2) + (dmin - lmin).powi(2)) / (0.1 * r).powi(2)
99 };
100 let coverage_max = |span: f64| {
101 let r = dmax - dmin;
102 if span > r {
103 let half = (span - r) / 2.0;
104 1.0 - 0.5 * (half * half + half * half) / (0.1 * r).powi(2)
105 } else {
106 1.0
107 }
108 };
109 let density = |k: f64, lmin: f64, lmax: f64| {
110 let r = (k - 1.0) / (lmax - lmin);
111 let rt = (m - 1.0) / (lmax.max(dmax) - lmin.min(dmin));
112 2.0 - (r / rt).max(rt / r)
113 };
114 let density_max = |k: f64| {
115 if k >= m {
116 2.0 - (k - 1.0) / (m - 1.0)
117 } else {
118 1.0
119 }
120 };
121
122 let mut best_score = -2.0;
123 let mut best = (dmin, dmax, (dmax - dmin) / (m - 1.0));
124
125 for j_i in 1..=4u32 {
126 let j = j_i as f64;
127 for (qi, &q) in Q.iter().enumerate() {
128 let sm = simplicity_max(qi, j);
129 if W[0] * sm + W[1] + W[2] + W[3] < best_score {
130 break;
131 }
132 for k_i in 2..=(2.0 * m + 4.0) as usize {
133 let k = k_i as f64;
134 let dm = density_max(k);
135 if W[0] * sm + W[1] + W[2] * dm + W[3] < best_score {
136 break;
137 }
138 let delta = (dmax - dmin) / (k + 1.0) / j / q;
139 let z0 = delta.log10().ceil() as i32;
140 for z in z0..(z0 + 6) {
141 let step = j * q * 10f64.powi(z);
142 let cm = coverage_max(step * (k - 1.0));
143 if W[0] * sm + W[1] * cm + W[2] * dm + W[3] < best_score {
144 break;
145 }
146 let min_start = (dmax / step).floor() * j - (k - 1.0) * j;
147 let max_start = (dmin / step).ceil() * j;
148 if min_start > max_start {
149 continue;
150 }
151 let n_starts = max_start - min_start;
154 if !n_starts.is_finite() || n_starts > MAX_START_CANDIDATES {
155 continue;
156 }
157 for si in 0..=(n_starts as u64) {
158 let start = min_start + si as f64;
159 let lmin = start * step / j;
160 let lmax = lmin + step * (k - 1.0);
161 let s = simplicity(qi, j, lmin, lmax, step);
162 let c = coverage(lmin, lmax);
163 let g = density(k, lmin, lmax);
164 let score = W[0] * s + W[1] * c + W[2] * g + W[3];
165 if score > best_score {
166 best_score = score;
167 best = (lmin, lmax, step);
168 }
169 }
170 }
171 }
172 }
173 }
174
175 let (lmin, lmax, step) = best;
176 if step <= 0.0 {
177 return vec![];
178 }
179 let n = ((lmax - lmin) / step).round() as usize;
180 (0..=n).map(|i| lmin + i as f64 * step).collect()
181}
182
183pub fn log_breaks(dmin: f64, dmax: f64, m: usize, base: f64) -> Vec<f64> {
190 let m = m.max(2);
191 if !(dmin.is_finite() && dmax.is_finite())
192 || dmin <= 0.0
193 || dmax <= 0.0
194 || dmax < dmin
195 || base <= 1.0
196 {
197 return extended_breaks(dmin, dmax, m);
198 }
199 if is_degenerate_range(dmin, dmax) {
200 return vec![];
201 }
202
203 let log_min = dmin.log(base);
204 let log_max = dmax.log(base);
205 let min_e = log_min.floor();
206 let max_e = log_max.ceil();
207 if (max_e - min_e).abs() < 1e-9 {
208 return vec![base.powf(min_e)];
209 }
210
211 let want = (m as i64 - 2).max(0) as f64;
212 let exp_seq = |by: f64| -> Vec<f64> {
213 let mut out = Vec::new();
214 let mut e = min_e;
215 while e <= max_e + 1e-9 {
216 out.push(base.powf(e));
217 e += by;
218 }
219 out
220 };
221 let count_relevant = |breaks: &[f64]| -> usize {
222 let lo = base.powf(log_min) * (1.0 - 1e-9);
223 let hi = base.powf(log_max) * (1.0 + 1e-9);
224 breaks.iter().filter(|&&b| b >= lo && b <= hi).count()
225 };
226
227 let mut by = ((max_e - min_e) / m as f64).floor() + 1.0;
228 loop {
229 let breaks = exp_seq(by);
230 if count_relevant(&breaks) as f64 >= want {
231 return breaks;
232 }
233 if by <= 1.0 {
234 break;
235 }
236 by -= 1.0;
237 }
238
239 log_sub_breaks(dmin, dmax, min_e, max_e, m, base)
240}
241
242fn log_sub_breaks(dmin: f64, dmax: f64, min_e: f64, max_e: f64, m: usize, base: f64) -> Vec<f64> {
245 let want = (m as i64 - 2).max(0) as f64;
246 let lo = dmin * (1.0 - 1e-9);
247 let hi = dmax * (1.0 + 1e-9);
248
249 if base <= 2.0 {
250 let mut out = Vec::new();
251 let mut e = min_e;
252 while e <= max_e + 1e-9 {
253 out.push(base.powf(e));
254 e += 1.0;
255 }
256 return out;
257 }
258
259 let exps: Vec<i64> = ((min_e as i64)..=(max_e as i64)).collect();
260 let mut steps: Vec<f64> = vec![1.0];
261 let mut candidates: Vec<f64> = (2..=(base.floor() as i64))
264 .filter(|&x| (x as f64) < base)
265 .map(|x| x as f64)
266 .collect();
267
268 let delta = |x: f64, steps: &[f64]| -> f64 {
271 let mut vals: Vec<f64> = steps.to_vec();
272 vals.push(x);
273 vals.push(base);
274 vals.sort_by(|a, b| a.partial_cmp(b).unwrap());
275 vals.windows(2)
276 .map(|w| w[1].log(base) - w[0].log(base))
277 .fold(f64::INFINITY, f64::min)
278 };
279
280 let mut breaks: Vec<f64> = Vec::new();
281 let mut relevant = 0usize;
282 while !candidates.is_empty() {
283 let (best_i, _) = candidates
284 .iter()
285 .enumerate()
286 .map(|(i, &c)| (i, delta(c, &steps)))
287 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
288 .unwrap();
289 steps.push(candidates.remove(best_i));
290 breaks = exps
291 .iter()
292 .flat_map(|&e| steps.iter().map(move |&s| base.powf(e as f64) * s))
293 .collect();
294 relevant = breaks.iter().filter(|&&b| b >= lo && b <= hi).count();
295 if relevant as f64 >= want {
296 break;
297 }
298 }
299
300 if relevant as f64 >= want && !breaks.is_empty() {
301 breaks.sort_by(|a, b| a.partial_cmp(b).unwrap());
302 let first_in = breaks.iter().position(|&b| b >= lo);
303 let last_in = breaks.iter().rposition(|&b| b <= hi);
304 match (first_in, last_in) {
305 (Some(fi), Some(li)) => {
306 let start = fi.saturating_sub(1);
307 let end = (li + 1).min(breaks.len() - 1);
308 breaks[start..=end].to_vec()
309 }
310 _ => breaks,
311 }
312 } else {
313 extended_breaks(dmin, dmax, m)
314 }
315}
316
317pub fn nice_step(raw: f64) -> f64 {
319 let magnitude = 10f64.powf(raw.abs().log10().floor());
320 let fraction = raw / magnitude;
321
322 let nice = if fraction <= 1.5 {
323 1.0
324 } else if fraction <= 3.5 {
325 2.0
326 } else if fraction <= 7.5 {
327 5.0
328 } else {
329 10.0
330 };
331
332 nice * magnitude
333}
334
335pub fn format_numbers(values: &[f64]) -> Vec<String> {
343 fn fmt(v: f64, decimals: usize) -> String {
344 if v == v.round() && v.abs() < 1e10 {
345 return format!("{}", v as i64);
346 }
347 let s = format!("{v:.decimals$}");
348 let s = s.trim_end_matches('0').trim_end_matches('.');
349 if s == "-0" {
350 "0".to_string()
351 } else {
352 s.to_string()
353 }
354 }
355 let ok = |labels: &[String]| {
356 let distinct = values
357 .iter()
358 .zip(labels)
359 .all(|(a, la)| values.iter().zip(labels).all(|(b, lb)| a == b || la != lb));
360 let nonzero = values
361 .iter()
362 .zip(labels)
363 .all(|(v, l)| *v == 0.0 || l != "0");
364 distinct && nonzero
365 };
366 let mut labels = Vec::new();
367 for decimals in 2..=12 {
368 labels = values.iter().map(|&v| fmt(v, decimals)).collect();
369 if ok(&labels) {
370 break;
371 }
372 }
373 labels
374}
375
376pub fn format_number(v: f64) -> String {
377 if v == v.round() && v.abs() < 1e10 {
378 format!("{}", v as i64)
379 } else {
380 let s = format!("{:.2}", v);
381 s.trim_end_matches('0').trim_end_matches('.').to_string()
382 }
383}
384
385pub fn format_numbers_locale(values: &[f64], locale: &crate::locale::Locale) -> Vec<String> {
390 format_numbers(values)
391 .into_iter()
392 .map(|s| locale.localize_number(&s))
393 .collect()
394}
395
396pub fn format_number_locale(v: f64, locale: &crate::locale::Locale) -> String {
398 locale.localize_number(&format_number(v))
399}
400
401pub(crate) fn format_numbers_opt(
403 values: &[f64],
404 locale: Option<&crate::locale::Locale>,
405) -> Vec<String> {
406 match locale {
407 Some(l) => format_numbers_locale(values, l),
408 None => format_numbers(values),
409 }
410}
411
412pub(crate) fn format_number_opt(v: f64, locale: Option<&crate::locale::Locale>) -> String {
414 match locale {
415 Some(l) => format_number_locale(v, l),
416 None => format_number(v),
417 }
418}
419
420#[cfg(test)]
421mod log_breaks_tests {
422 use super::*;
423
424 #[test]
425 fn wide_range_ticks_whole_decades() {
426 assert_eq!(
428 log_breaks(1.0, 1000.0, 5, 10.0),
429 vec![1.0, 10.0, 100.0, 1000.0]
430 );
431 }
432
433 #[test]
434 fn narrow_range_falls_back_to_sub_decade_steps() {
435 let breaks = log_breaks(1.0, 170.0, 5, 10.0);
438 for &b in &breaks {
440 let digits = format!("{b}");
441 assert!(!digits.contains('.'), "non-round log break: {b}");
442 }
443 assert!(breaks.contains(&1.0));
444 assert!(breaks.iter().any(|&b| b >= 100.0));
445 }
446
447 #[test]
448 fn base2_ticks_whole_powers_only() {
449 assert_eq!(log_breaks(1.0, 8.0, 5, 2.0), vec![1.0, 2.0, 4.0, 8.0]);
450 }
451
452 #[test]
453 fn degenerate_or_invalid_range_falls_back() {
454 assert!(log_breaks(5.0, 5.0, 5, 10.0).is_empty());
455 assert_eq!(
458 log_breaks(-1.0, 10.0, 5, 10.0),
459 extended_breaks(-1.0, 10.0, 5)
460 );
461 }
462}
463
464#[cfg(test)]
465mod locale_tests {
466 use super::*;
467 use crate::locale::Locale;
468
469 #[test]
470 fn joint_labels_keep_distinctness_with_locale_marks() {
471 let de = Locale::de_de();
472 assert_eq!(
473 format_numbers_locale(&[0.99, 0.992, 0.994], &de),
474 ["0,99", "0,992", "0,994"]
475 );
476 assert_eq!(
477 format_numbers_locale(&[0.0, 1000.0, 2000.0, 3000.0], &de),
478 ["0", "1.000", "2.000", "3.000"]
479 );
480 assert_eq!(
481 format_numbers_locale(&[-1500.5, 0.0, 1500.5], &de),
482 ["-1.500,5", "0", "1.500,5"]
483 );
484 assert_eq!(
485 format_numbers_locale(&[1000.0, 1000.001, 1000.002], &de),
486 ["1.000", "1.000,001", "1.000,002"]
487 );
488 assert_eq!(
489 format_numbers_locale(&[0.001, 0.01, 0.1], &Locale::fr_fr()),
490 ["0,001", "0,01", "0,1"]
491 );
492 let us = Locale::en_us();
493 assert_eq!(
494 format_numbers_locale(&[0.0, 2500.0, 5000.0], &us),
495 ["0", "2,500", "5,000"]
496 );
497 assert_eq!(format_number_locale(1234.5678, &de), "1.234,57");
498 assert_eq!(format_numbers_opt(&[0.0, 1000.0], None), ["0", "1000"]);
500 assert_eq!(format_number_opt(1234.5, None), "1234.5");
501 }
502}