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 extended_breaks(dmin: f64, dmax: f64, m: usize) -> Vec<f64> {
32 if !(dmin.is_finite() && dmax.is_finite()) || dmax <= dmin || m < 2 {
33 return vec![];
34 }
35 if is_degenerate_range(dmin, dmax) {
39 return vec![];
40 }
41 const Q: [f64; 6] = [1.0, 5.0, 2.0, 2.5, 4.0, 3.0];
44 const W: [f64; 4] = [0.25, 0.2, 0.5, 0.05];
45 let m = m as f64;
46
47 let simplicity = |qi: usize, j: f64, lmin: f64, lmax: f64, step: f64| {
48 let eps = 1e-10;
49 let modulo = lmin.rem_euclid(step);
50 let v = if (modulo < eps || step - modulo < eps) && lmin <= 0.0 && lmax >= 0.0 {
51 1.0
52 } else {
53 0.0
54 };
55 1.0 - qi as f64 / (Q.len() as f64 - 1.0) - j + v
56 };
57 let simplicity_max = |qi: usize, j: f64| 1.0 - qi as f64 / (Q.len() as f64 - 1.0) - j + 1.0;
58 let coverage = |lmin: f64, lmax: f64| {
59 let r = dmax - dmin;
60 1.0 - 0.5 * ((dmax - lmax).powi(2) + (dmin - lmin).powi(2)) / (0.1 * r).powi(2)
61 };
62 let coverage_max = |span: f64| {
63 let r = dmax - dmin;
64 if span > r {
65 let half = (span - r) / 2.0;
66 1.0 - 0.5 * (half * half + half * half) / (0.1 * r).powi(2)
67 } else {
68 1.0
69 }
70 };
71 let density = |k: f64, lmin: f64, lmax: f64| {
72 let r = (k - 1.0) / (lmax - lmin);
73 let rt = (m - 1.0) / (lmax.max(dmax) - lmin.min(dmin));
74 2.0 - (r / rt).max(rt / r)
75 };
76 let density_max = |k: f64| {
77 if k >= m {
78 2.0 - (k - 1.0) / (m - 1.0)
79 } else {
80 1.0
81 }
82 };
83
84 let mut best_score = -2.0;
85 let mut best = (dmin, dmax, (dmax - dmin) / (m - 1.0));
86
87 for j_i in 1..=4u32 {
88 let j = j_i as f64;
89 for (qi, &q) in Q.iter().enumerate() {
90 let sm = simplicity_max(qi, j);
91 if W[0] * sm + W[1] + W[2] + W[3] < best_score {
92 break;
93 }
94 for k_i in 2..=(2.0 * m + 4.0) as usize {
95 let k = k_i as f64;
96 let dm = density_max(k);
97 if W[0] * sm + W[1] + W[2] * dm + W[3] < best_score {
98 break;
99 }
100 let delta = (dmax - dmin) / (k + 1.0) / j / q;
101 let z0 = delta.log10().ceil() as i32;
102 for z in z0..(z0 + 6) {
103 let step = j * q * 10f64.powi(z);
104 let cm = coverage_max(step * (k - 1.0));
105 if W[0] * sm + W[1] * cm + W[2] * dm + W[3] < best_score {
106 break;
107 }
108 let min_start = (dmax / step).floor() * j - (k - 1.0) * j;
109 let max_start = (dmin / step).ceil() * j;
110 if min_start > max_start {
111 continue;
112 }
113 let n_starts = max_start - min_start;
116 if !n_starts.is_finite() || n_starts > MAX_START_CANDIDATES {
117 continue;
118 }
119 for si in 0..=(n_starts as u64) {
120 let start = min_start + si as f64;
121 let lmin = start * step / j;
122 let lmax = lmin + step * (k - 1.0);
123 let s = simplicity(qi, j, lmin, lmax, step);
124 let c = coverage(lmin, lmax);
125 let g = density(k, lmin, lmax);
126 let score = W[0] * s + W[1] * c + W[2] * g + W[3];
127 if score > best_score {
128 best_score = score;
129 best = (lmin, lmax, step);
130 }
131 }
132 }
133 }
134 }
135 }
136
137 let (lmin, lmax, step) = best;
138 if step <= 0.0 {
139 return vec![];
140 }
141 let n = ((lmax - lmin) / step).round() as usize;
142 (0..=n).map(|i| lmin + i as f64 * step).collect()
143}
144
145pub fn log_breaks(dmin: f64, dmax: f64, m: usize, base: f64) -> Vec<f64> {
152 let m = m.max(2);
153 if !(dmin.is_finite() && dmax.is_finite())
154 || dmin <= 0.0
155 || dmax <= 0.0
156 || dmax < dmin
157 || base <= 1.0
158 {
159 return extended_breaks(dmin, dmax, m);
160 }
161 if is_degenerate_range(dmin, dmax) {
162 return vec![];
163 }
164
165 let log_min = dmin.log(base);
166 let log_max = dmax.log(base);
167 let min_e = log_min.floor();
168 let max_e = log_max.ceil();
169 if (max_e - min_e).abs() < 1e-9 {
170 return vec![base.powf(min_e)];
171 }
172
173 let want = (m as i64 - 2).max(0) as f64;
174 let exp_seq = |by: f64| -> Vec<f64> {
175 let mut out = Vec::new();
176 let mut e = min_e;
177 while e <= max_e + 1e-9 {
178 out.push(base.powf(e));
179 e += by;
180 }
181 out
182 };
183 let count_relevant = |breaks: &[f64]| -> usize {
184 let lo = base.powf(log_min) * (1.0 - 1e-9);
185 let hi = base.powf(log_max) * (1.0 + 1e-9);
186 breaks.iter().filter(|&&b| b >= lo && b <= hi).count()
187 };
188
189 let mut by = ((max_e - min_e) / m as f64).floor() + 1.0;
190 loop {
191 let breaks = exp_seq(by);
192 if count_relevant(&breaks) as f64 >= want {
193 return breaks;
194 }
195 if by <= 1.0 {
196 break;
197 }
198 by -= 1.0;
199 }
200
201 log_sub_breaks(dmin, dmax, min_e, max_e, m, base)
202}
203
204fn log_sub_breaks(dmin: f64, dmax: f64, min_e: f64, max_e: f64, m: usize, base: f64) -> Vec<f64> {
207 let want = (m as i64 - 2).max(0) as f64;
208 let lo = dmin * (1.0 - 1e-9);
209 let hi = dmax * (1.0 + 1e-9);
210
211 if base <= 2.0 {
212 let mut out = Vec::new();
213 let mut e = min_e;
214 while e <= max_e + 1e-9 {
215 out.push(base.powf(e));
216 e += 1.0;
217 }
218 return out;
219 }
220
221 let exps: Vec<i64> = ((min_e as i64)..=(max_e as i64)).collect();
222 let mut steps: Vec<f64> = vec![1.0];
223 let mut candidates: Vec<f64> = (2..=(base.floor() as i64))
226 .filter(|&x| (x as f64) < base)
227 .map(|x| x as f64)
228 .collect();
229
230 let delta = |x: f64, steps: &[f64]| -> f64 {
233 let mut vals: Vec<f64> = steps.to_vec();
234 vals.push(x);
235 vals.push(base);
236 vals.sort_by(|a, b| a.partial_cmp(b).unwrap());
237 vals.windows(2)
238 .map(|w| w[1].log(base) - w[0].log(base))
239 .fold(f64::INFINITY, f64::min)
240 };
241
242 let mut breaks: Vec<f64> = Vec::new();
243 let mut relevant = 0usize;
244 while !candidates.is_empty() {
245 let (best_i, _) = candidates
246 .iter()
247 .enumerate()
248 .map(|(i, &c)| (i, delta(c, &steps)))
249 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
250 .unwrap();
251 steps.push(candidates.remove(best_i));
252 breaks = exps
253 .iter()
254 .flat_map(|&e| steps.iter().map(move |&s| base.powf(e as f64) * s))
255 .collect();
256 relevant = breaks.iter().filter(|&&b| b >= lo && b <= hi).count();
257 if relevant as f64 >= want {
258 break;
259 }
260 }
261
262 if relevant as f64 >= want && !breaks.is_empty() {
263 breaks.sort_by(|a, b| a.partial_cmp(b).unwrap());
264 let first_in = breaks.iter().position(|&b| b >= lo);
265 let last_in = breaks.iter().rposition(|&b| b <= hi);
266 match (first_in, last_in) {
267 (Some(fi), Some(li)) => {
268 let start = fi.saturating_sub(1);
269 let end = (li + 1).min(breaks.len() - 1);
270 breaks[start..=end].to_vec()
271 }
272 _ => breaks,
273 }
274 } else {
275 extended_breaks(dmin, dmax, m)
276 }
277}
278
279pub fn nice_step(raw: f64) -> f64 {
281 let magnitude = 10f64.powf(raw.abs().log10().floor());
282 let fraction = raw / magnitude;
283
284 let nice = if fraction <= 1.5 {
285 1.0
286 } else if fraction <= 3.5 {
287 2.0
288 } else if fraction <= 7.5 {
289 5.0
290 } else {
291 10.0
292 };
293
294 nice * magnitude
295}
296
297pub fn format_numbers(values: &[f64]) -> Vec<String> {
305 fn fmt(v: f64, decimals: usize) -> String {
306 if v == v.round() && v.abs() < 1e10 {
307 return format!("{}", v as i64);
308 }
309 let s = format!("{v:.decimals$}");
310 let s = s.trim_end_matches('0').trim_end_matches('.');
311 if s == "-0" {
312 "0".to_string()
313 } else {
314 s.to_string()
315 }
316 }
317 let ok = |labels: &[String]| {
318 let distinct = values
319 .iter()
320 .zip(labels)
321 .all(|(a, la)| values.iter().zip(labels).all(|(b, lb)| a == b || la != lb));
322 let nonzero = values
323 .iter()
324 .zip(labels)
325 .all(|(v, l)| *v == 0.0 || l != "0");
326 distinct && nonzero
327 };
328 let mut labels = Vec::new();
329 for decimals in 2..=12 {
330 labels = values.iter().map(|&v| fmt(v, decimals)).collect();
331 if ok(&labels) {
332 break;
333 }
334 }
335 labels
336}
337
338pub fn format_number(v: f64) -> String {
339 if v == v.round() && v.abs() < 1e10 {
340 format!("{}", v as i64)
341 } else {
342 let s = format!("{:.2}", v);
343 s.trim_end_matches('0').trim_end_matches('.').to_string()
344 }
345}
346
347pub fn format_numbers_locale(values: &[f64], locale: &crate::locale::Locale) -> Vec<String> {
352 format_numbers(values)
353 .into_iter()
354 .map(|s| locale.localize_number(&s))
355 .collect()
356}
357
358pub fn format_number_locale(v: f64, locale: &crate::locale::Locale) -> String {
360 locale.localize_number(&format_number(v))
361}
362
363pub(crate) fn format_numbers_opt(
365 values: &[f64],
366 locale: Option<&crate::locale::Locale>,
367) -> Vec<String> {
368 match locale {
369 Some(l) => format_numbers_locale(values, l),
370 None => format_numbers(values),
371 }
372}
373
374pub(crate) fn format_number_opt(v: f64, locale: Option<&crate::locale::Locale>) -> String {
376 match locale {
377 Some(l) => format_number_locale(v, l),
378 None => format_number(v),
379 }
380}
381
382#[cfg(test)]
383mod log_breaks_tests {
384 use super::*;
385
386 #[test]
387 fn wide_range_ticks_whole_decades() {
388 assert_eq!(
390 log_breaks(1.0, 1000.0, 5, 10.0),
391 vec![1.0, 10.0, 100.0, 1000.0]
392 );
393 }
394
395 #[test]
396 fn narrow_range_falls_back_to_sub_decade_steps() {
397 let breaks = log_breaks(1.0, 170.0, 5, 10.0);
400 for &b in &breaks {
402 let digits = format!("{b}");
403 assert!(!digits.contains('.'), "non-round log break: {b}");
404 }
405 assert!(breaks.contains(&1.0));
406 assert!(breaks.iter().any(|&b| b >= 100.0));
407 }
408
409 #[test]
410 fn base2_ticks_whole_powers_only() {
411 assert_eq!(log_breaks(1.0, 8.0, 5, 2.0), vec![1.0, 2.0, 4.0, 8.0]);
412 }
413
414 #[test]
415 fn degenerate_or_invalid_range_falls_back() {
416 assert!(log_breaks(5.0, 5.0, 5, 10.0).is_empty());
417 assert_eq!(
420 log_breaks(-1.0, 10.0, 5, 10.0),
421 extended_breaks(-1.0, 10.0, 5)
422 );
423 }
424}
425
426#[cfg(test)]
427mod locale_tests {
428 use super::*;
429 use crate::locale::Locale;
430
431 #[test]
432 fn joint_labels_keep_distinctness_with_locale_marks() {
433 let de = Locale::de_de();
434 assert_eq!(
435 format_numbers_locale(&[0.99, 0.992, 0.994], &de),
436 ["0,99", "0,992", "0,994"]
437 );
438 assert_eq!(
439 format_numbers_locale(&[0.0, 1000.0, 2000.0, 3000.0], &de),
440 ["0", "1.000", "2.000", "3.000"]
441 );
442 assert_eq!(
443 format_numbers_locale(&[-1500.5, 0.0, 1500.5], &de),
444 ["-1.500,5", "0", "1.500,5"]
445 );
446 assert_eq!(
447 format_numbers_locale(&[1000.0, 1000.001, 1000.002], &de),
448 ["1.000", "1.000,001", "1.000,002"]
449 );
450 assert_eq!(
451 format_numbers_locale(&[0.001, 0.01, 0.1], &Locale::fr_fr()),
452 ["0,001", "0,01", "0,1"]
453 );
454 let us = Locale::en_us();
455 assert_eq!(
456 format_numbers_locale(&[0.0, 2500.0, 5000.0], &us),
457 ["0", "2,500", "5,000"]
458 );
459 assert_eq!(format_number_locale(1234.5678, &de), "1.234,57");
460 assert_eq!(format_numbers_opt(&[0.0, 1000.0], None), ["0", "1000"]);
462 assert_eq!(format_number_opt(1234.5, None), "1234.5");
463 }
464}