1#[derive(Clone, Debug)]
6pub struct Ticks {
7 pub positions: Vec<f64>,
9 pub labels: Vec<String>,
11}
12
13pub fn nice_ticks(min: f64, max: f64, target_count: usize) -> Ticks {
18 if (max - min).abs() < 1e-15 {
19 let label = format_tick(min);
20 return Ticks {
21 positions: vec![min],
22 labels: vec![label],
23 };
24 }
25
26 let target = target_count.max(2) as f64;
27 let range = nice_num(max - min, false);
28 let step = nice_num(range / (target - 1.0), true);
29
30 let graph_min = (min / step).floor() * step;
31 let graph_max = (max / step).ceil() * step;
32
33 let mut positions = Vec::new();
34 let mut v = graph_min;
35 let max_ticks = (target_count + 5) * 2;
37 while v <= graph_max + step * 0.5 && positions.len() < max_ticks {
38 positions.push(v);
39 v += step;
40 }
41
42 let labels = positions.iter().map(|&v| format_tick(v)).collect();
43
44 Ticks { positions, labels }
45}
46
47pub fn nice_ticks_log(min: f64, max: f64) -> Ticks {
49 let log_min = min.max(1e-15).log10().floor() as i32;
50 let log_max = max.max(1e-15).log10().ceil() as i32;
51
52 let mut positions = Vec::new();
53 for exp in log_min..=log_max {
54 positions.push(10.0_f64.powi(exp));
55 }
56
57 let labels = positions.iter().map(|&v| format_tick(v)).collect();
58 Ticks { positions, labels }
59}
60
61fn nice_num(x: f64, round: bool) -> f64 {
66 let exp = x.abs().log10().floor();
67 let frac = x / 10.0_f64.powf(exp);
68
69 let nice_frac = if round {
70 if frac < 1.5 {
71 1.0
72 } else if frac < 3.0 {
73 2.0
74 } else if frac < 7.0 {
75 5.0
76 } else {
77 10.0
78 }
79 } else if frac <= 1.0 {
80 1.0
81 } else if frac <= 2.0 {
82 2.0
83 } else if frac <= 5.0 {
84 5.0
85 } else {
86 10.0
87 };
88
89 nice_frac * 10.0_f64.powf(exp)
90}
91
92pub fn format_tick(value: f64) -> String {
94 if value == 0.0 {
95 return "0".to_string();
96 }
97
98 let abs = value.abs();
99 let sign = if value < 0.0 { "-" } else { "" };
100
101 if abs >= 1e9 {
102 let v = value / 1e9;
103 format_si(v, sign, "B")
104 } else if abs >= 1e6 {
105 let v = value / 1e6;
106 format_si(v, sign, "M")
107 } else if abs >= 1e4 {
108 format_with_commas(value)
110 } else if abs >= 1.0 {
111 if (value - value.round()).abs() < 1e-9 {
112 format!("{}", value as i64)
113 } else {
114 format!("{value:.1}")
115 }
116 } else if abs >= 0.01 {
117 format!("{value:.2}")
118 } else if abs >= 1e-6 {
119 if abs >= 1e-3 {
121 let v = value * 1e3;
122 format_si(v, sign, "m")
123 } else {
124 let v = value * 1e6;
125 format_si(v, sign, "\u{00B5}") }
127 } else {
128 format!("{value:.2e}")
129 }
130}
131
132fn format_si(v: f64, sign: &str, suffix: &str) -> String {
133 let abs_v = v.abs();
134 if (abs_v - abs_v.round()).abs() < 0.05 {
135 format!("{sign}{}{suffix}", abs_v.round() as i64)
136 } else {
137 format!("{sign}{abs_v:.1}{suffix}")
138 }
139}
140
141fn format_with_commas(value: f64) -> String {
142 let rounded = value.round() as i64;
143 let s = rounded.abs().to_string();
144 let mut result = String::new();
145 for (i, c) in s.chars().rev().enumerate() {
146 if i > 0 && i % 3 == 0 {
147 result.push(',');
148 }
149 result.push(c);
150 }
151 if rounded < 0 {
152 result.push('-');
153 }
154 result.chars().rev().collect()
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn test_nice_ticks_basic() {
163 let ticks = nice_ticks(0.0, 100.0, 5);
164 assert!(!ticks.positions.is_empty());
165 assert!(ticks.positions[0] <= 0.0);
166 assert!(*ticks.positions.last().unwrap() >= 100.0);
167 if ticks.positions.len() >= 2 {
169 let step = ticks.positions[1] - ticks.positions[0];
170 assert!(step > 0.0);
171 }
172 }
173
174 #[test]
175 fn test_nice_ticks_small_range() {
176 let ticks = nice_ticks(0.0, 1.0, 5);
177 assert!(ticks.positions.len() >= 2);
178 }
179
180 #[test]
181 fn test_format_tick() {
182 assert_eq!(format_tick(0.0), "0");
183 assert_eq!(format_tick(100.0), "100");
184 assert_eq!(format_tick(2.5), "2.5");
185 assert_eq!(format_tick(1_000_000.0), "1M");
187 assert_eq!(format_tick(2_500_000.0), "2.5M");
188 assert_eq!(format_tick(1_000_000_000.0), "1B");
189 assert_eq!(format_tick(-3_000_000.0), "-3M");
190 assert_eq!(format_tick(12_000.0), "12,000");
192 assert_eq!(format_tick(100_000.0), "100,000");
193 assert_eq!(format_tick(0.001), "1m");
195 assert_eq!(format_tick(0.0002), "200\u{00B5}");
196 }
197
198 #[test]
199 fn test_nice_ticks_same_value() {
200 let ticks = nice_ticks(5.0, 5.0, 5);
201 assert_eq!(ticks.positions.len(), 1);
202 }
203}