1use crate::config::{AxisOptions, AxisScale, Config};
2
3#[derive(Debug, Clone, PartialEq)]
4pub struct TickPlan {
5 pub min: f64,
6 pub max: f64,
7 pub major_spacing: f64,
8 pub minor_count: usize,
9}
10
11#[derive(Debug, Clone, PartialEq)]
12pub enum TickError {
13 InvalidRange,
14 NonPositiveLog,
15 ZeroTargetCount,
16}
17
18fn nice_num(range: f64, round: bool) -> f64 {
19 let exp = range.log10().floor();
20 let frac = range / 10f64.powf(exp);
21 let nice_frac = if round {
22 if frac < 1.5 {
23 1.0
24 } else if frac < 3.0 {
25 2.0
26 } else if frac < 7.0 {
27 5.0
28 } else {
29 10.0
30 }
31 } else {
32 if frac <= 1.0 {
33 1.0
34 } else if frac <= 2.0 {
35 2.0
36 } else if frac <= 5.0 {
37 5.0
38 } else {
39 10.0
40 }
41 };
42 nice_frac * 10f64.powf(exp)
43}
44
45fn finite_nice_value(value: f64, fallback: f64) -> f64 {
46 if value.is_finite() && value > 0.0 {
47 value
48 } else {
49 fallback
50 }
51}
52
53fn checked_plan(
54 min: f64,
55 max: f64,
56 major_spacing: f64,
57 minor_count: usize,
58 data_min: f64,
59 data_max: f64,
60 logarithmic: bool,
61) -> Result<TickPlan, TickError> {
62 if !(min.is_finite()
63 && max.is_finite()
64 && major_spacing.is_finite()
65 && major_spacing > 0.0
66 && min < max
67 && min <= data_min
68 && max >= data_max
69 && (!logarithmic || min > 0.0))
70 {
71 return Err(TickError::InvalidRange);
72 }
73 Ok(TickPlan {
74 min,
75 max,
76 major_spacing,
77 minor_count,
78 })
79}
80
81fn compute_linear(
82 data_min: f64,
83 data_max: f64,
84 target_count: usize,
85) -> Result<TickPlan, TickError> {
86 let span = data_max - data_min;
87 if !(span.is_finite() && span > 0.0) {
88 return Err(TickError::InvalidRange);
89 }
90 let range = finite_nice_value(nice_num(span, false), span);
91 let divisor = ((target_count as f64) - 1.0).max(1.0);
92 let raw_step = finite_nice_value(range / divisor, range);
93 let step = finite_nice_value(nice_num(raw_step, true), raw_step);
94 let candidate_min = (data_min / step).floor() * step;
95 let candidate_max = (data_max / step).ceil() * step;
96 let nice_min = if candidate_min.is_finite() && candidate_min <= data_min {
97 candidate_min
98 } else {
99 data_min
100 };
101 let nice_max = if candidate_max.is_finite() && candidate_max >= data_max {
102 candidate_max
103 } else {
104 data_max
105 };
106 checked_plan(nice_min, nice_max, step, 4, data_min, data_max, false)
107}
108
109fn compute_log(data_min: f64, data_max: f64, target_count: usize) -> Result<TickPlan, TickError> {
110 let log_min = data_min.log10().floor();
111 let log_max = data_max.log10().ceil();
112 let decades = log_max - log_min;
113 if !(log_min.is_finite() && log_max.is_finite() && decades.is_finite() && decades > 0.0) {
114 return Err(TickError::InvalidRange);
115 }
116 let divisor = ((target_count as f64) - 1.0).max(1.0);
117 let decade_step = (decades / divisor).ceil().max(1.0);
118 let candidate_min = 10f64.powf(log_min);
119 let candidate_max = 10f64.powf(log_max);
120 let nice_min = if candidate_min.is_finite() && candidate_min > 0.0 {
121 candidate_min
122 } else {
123 data_min
124 };
125 let nice_max = if candidate_max.is_finite() {
126 candidate_max
127 } else {
128 data_max
129 };
130 checked_plan(nice_min, nice_max, decade_step, 8, data_min, data_max, true)
131}
132
133impl AxisOptions {
134 pub fn compute_nice_ticks(
135 scale: AxisScale,
136 data_min: f64,
137 data_max: f64,
138 target_count: usize,
139 ) -> Result<TickPlan, TickError> {
140 if target_count == 0 {
141 return Err(TickError::ZeroTargetCount);
142 }
143 if !(data_min.is_finite() && data_max.is_finite() && data_min < data_max) {
144 return Err(TickError::InvalidRange);
145 }
146 if scale == AxisScale::Logarithmic && data_min <= 0.0 {
147 return Err(TickError::NonPositiveLog);
148 }
149 match scale {
150 AxisScale::Linear => compute_linear(data_min, data_max, target_count),
151 AxisScale::Logarithmic => compute_log(data_min, data_max, target_count),
152 }
153 }
154
155 pub fn auto_ticks(
156 &mut self,
157 data_min: f64,
158 data_max: f64,
159 target_count: usize,
160 ) -> Result<(), TickError> {
161 let plan =
162 AxisOptions::compute_nice_ticks(self.scale.clone(), data_min, data_max, target_count)?;
163 self.min = plan.min;
164 self.max = plan.max;
165 self.major_spacing = plan.major_spacing;
166 self.minor_count = plan.minor_count;
167 Ok(())
168 }
169}
170
171impl Config {
172 pub fn auto_ticks_all(
173 &mut self,
174 top_x: (f64, f64),
175 bottom_x: (f64, f64),
176 left_y: (f64, f64),
177 right_y: (f64, f64),
178 target_count: usize,
179 ) -> Result<(), TickError> {
180 let plan_top = AxisOptions::compute_nice_ticks(
182 self.top_x.scale.clone(),
183 top_x.0,
184 top_x.1,
185 target_count,
186 )?;
187 let plan_bottom = AxisOptions::compute_nice_ticks(
188 self.bottom_x.scale.clone(),
189 bottom_x.0,
190 bottom_x.1,
191 target_count,
192 )?;
193 let plan_left = AxisOptions::compute_nice_ticks(
194 self.left_y.scale.clone(),
195 left_y.0,
196 left_y.1,
197 target_count,
198 )?;
199 let plan_right = AxisOptions::compute_nice_ticks(
200 self.right_y.scale.clone(),
201 right_y.0,
202 right_y.1,
203 target_count,
204 )?;
205
206 apply_plan(&mut self.top_x, plan_top);
208 apply_plan(&mut self.bottom_x, plan_bottom);
209 apply_plan(&mut self.left_y, plan_left);
210 apply_plan(&mut self.right_y, plan_right);
211 Ok(())
212 }
213}
214
215fn apply_plan(axis: &mut AxisOptions, p: TickPlan) {
216 axis.min = p.min;
217 axis.max = p.max;
218 axis.major_spacing = p.major_spacing;
219 axis.minor_count = p.minor_count;
220}
221
222#[cfg(test)]
225mod tests {
226 use super::*;
227 use crate::default::default_config;
228
229 #[test]
231 fn linear_plan_contains_range() {
232 let p = AxisOptions::compute_nice_ticks(AxisScale::Linear, 0.3, 9.7, 6).unwrap();
233 assert!(p.min <= 0.3);
234 assert!(p.max >= 9.7);
235 }
236
237 #[test]
239 fn linear_plan_non_degenerate() {
240 let p = AxisOptions::compute_nice_ticks(AxisScale::Linear, -50.0, 50.0, 5).unwrap();
241 assert!(p.max > p.min);
242 assert!(p.major_spacing > 0.0);
243 }
244
245 #[test]
246 fn log_plan_contains_range() {
247 let p = AxisOptions::compute_nice_ticks(AxisScale::Logarithmic, 0.003, 250.0, 5).unwrap();
248 assert!(p.min <= 0.003);
249 assert!(p.max >= 250.0);
250 assert!(p.max > p.min);
251 assert!(p.major_spacing > 0.0);
252 }
253
254 #[test]
255 fn log_rejects_non_positive() {
256 let e = AxisOptions::compute_nice_ticks(AxisScale::Logarithmic, 0.0, 100.0, 5).unwrap_err();
257 assert_eq!(e, TickError::NonPositiveLog);
258 }
259
260 #[test]
261 fn rejects_invalid_range() {
262 let e = AxisOptions::compute_nice_ticks(AxisScale::Linear, 5.0, 5.0, 5).unwrap_err();
263 assert_eq!(e, TickError::InvalidRange);
264 }
265
266 #[test]
267 fn rejects_zero_target_count() {
268 let e = AxisOptions::compute_nice_ticks(AxisScale::Linear, 0.0, 10.0, 0).unwrap_err();
269 assert_eq!(e, TickError::ZeroTargetCount);
270 }
271
272 #[test]
273 fn rejects_every_non_finite_bound_and_reversed_ranges() {
274 for (data_min, data_max) in [
275 (f64::NAN, 1.0),
276 (1.0, f64::NAN),
277 (f64::NEG_INFINITY, 1.0),
278 (1.0, f64::NEG_INFINITY),
279 (f64::INFINITY, 1.0),
280 (1.0, f64::INFINITY),
281 (2.0, 1.0),
282 ] {
283 assert_eq!(
284 AxisOptions::compute_nice_ticks(AxisScale::Logarithmic, data_min, data_max, 5),
285 Err(TickError::InvalidRange),
286 "range ({data_min:?}, {data_max:?})"
287 );
288 }
289 }
290
291 #[test]
292 fn target_count_then_range_then_log_domain_define_error_priority() {
293 assert_eq!(
294 AxisOptions::compute_nice_ticks(AxisScale::Logarithmic, f64::NAN, 0.0, 0),
295 Err(TickError::ZeroTargetCount)
296 );
297 assert_eq!(
298 AxisOptions::compute_nice_ticks(AxisScale::Logarithmic, -1.0, -2.0, 5),
299 Err(TickError::InvalidRange)
300 );
301 assert_eq!(
302 AxisOptions::compute_nice_ticks(AxisScale::Logarithmic, -1.0, 2.0, 5),
303 Err(TickError::NonPositiveLog)
304 );
305 }
306
307 #[test]
308 fn extreme_finite_ranges_produce_finite_ordered_plans_or_fail_cleanly() {
309 for (scale, data_min, data_max) in [
310 (AxisScale::Linear, 0.0, f64::from_bits(1)),
311 (AxisScale::Linear, 0.0, f64::MAX),
312 (AxisScale::Logarithmic, f64::from_bits(1), f64::MAX),
313 ] {
314 let plan = AxisOptions::compute_nice_ticks(scale, data_min, data_max, 5).unwrap();
315 assert!(plan.min.is_finite());
316 assert!(plan.max.is_finite());
317 assert!(plan.major_spacing.is_finite());
318 assert!(plan.min < plan.max);
319 assert!(plan.min <= data_min);
320 assert!(plan.max >= data_max);
321 }
322
323 assert_eq!(
324 AxisOptions::compute_nice_ticks(AxisScale::Linear, -f64::MAX, f64::MAX, 5),
325 Err(TickError::InvalidRange)
326 );
327 }
328
329 #[test]
331 fn auto_ticks_err_preserves_state() {
332 let mut cfg = default_config();
333 let before = cfg.top_x.clone();
334 let r = cfg.top_x.auto_ticks(-f64::MAX, f64::MAX, 5);
335 assert!(r.is_err());
336 assert_eq!(cfg.top_x, before);
337 }
338
339 #[test]
341 fn auto_ticks_all_err_preserves_all() {
342 let mut cfg = default_config();
343 let before = cfg.clone();
344 let r = cfg.auto_ticks_all((0.0, 10.0), (0.0, 10.0), (f64::NAN, 5.0), (0.0, 10.0), 5);
345 assert!(r.is_err());
346 assert_eq!(cfg, before);
347 }
348
349 #[test]
351 fn compute_nice_ticks_pure() {
352 let a = AxisOptions::compute_nice_ticks(AxisScale::Linear, 0.0, 100.0, 6).unwrap();
353 let b = AxisOptions::compute_nice_ticks(AxisScale::Linear, 0.0, 100.0, 6).unwrap();
354 assert_eq!(a, b);
355 }
356}