Use the actions-rs actions to set things up
1 |
/*!
|
|
2 |
|
|
3 |
A module for managing axes
|
|
4 |
|
|
5 |
*/
|
|
6 |
|
|
7 |
#[derive(Debug, Clone)]
|
|
8 |
pub struct Range { |
|
9 |
pub lower: f64, |
|
10 |
pub upper: f64, |
|
11 |
}
|
|
12 |
|
|
13 |
impl Range { |
|
14 | 1 |
pub fn new(lower: f64, upper: f64) -> Range { |
15 |
Range { lower, upper } |
|
16 |
} |
|
17 |
|
|
18 | 1 |
pub(crate) fn is_valid(&self) -> bool { |
19 | 1 |
self.lower < self.upper |
20 |
} |
|
21 |
}
|
|
22 |
|
|
23 |
#[derive(Debug)]
|
|
24 |
pub struct ContinuousAxis { |
|
25 |
range: Range, |
|
26 |
ticks: Vec<f64>, |
|
27 |
label: String, |
|
28 |
}
|
|
29 |
|
|
30 |
impl ContinuousAxis { |
|
31 |
/// Constructs a new ContinuousAxis |
|
32 | 1 |
pub fn new(lower: f64, upper: f64, max_ticks: usize) -> ContinuousAxis { |
33 |
ContinuousAxis { |
|
34 | 1 |
range: Range::new(lower, upper), |
35 | 1 |
ticks: calculate_ticks(lower, upper, max_ticks), |
36 | 1 |
label: "".into(), |
37 |
} |
|
38 |
} |
|
39 |
|
|
40 | 1 |
pub fn max(&self) -> f64 { |
41 | 1 |
self.range.upper |
42 |
} |
|
43 |
|
|
44 | 1 |
pub fn min(&self) -> f64 { |
45 | 1 |
self.range.lower |
46 |
} |
|
47 |
|
|
48 | 1 |
pub fn label<S>(mut self, l: S) -> Self |
49 |
where |
|
50 |
S: Into<String>, |
|
51 |
{ |
|
52 | 1 |
self.label = l.into(); |
53 | 1 |
self |
54 |
} |
|
55 |
|
|
56 | 1 |
pub fn get_label(&self) -> &str { |
57 | 1 |
self.label.as_ref() |
58 |
} |
|
59 |
|
|
60 |
/// Get the positions of the ticks on the axis |
|
61 | 1 |
pub fn ticks(&self) -> &Vec<f64> { |
62 | 1 |
&self.ticks |
63 |
} |
|
64 |
}
|
|
65 |
|
|
66 |
#[derive(Debug)]
|
|
67 |
pub struct CategoricalAxis { |
|
68 |
ticks: Vec<String>, |
|
69 |
label: String, |
|
70 |
}
|
|
71 |
|
|
72 |
impl CategoricalAxis { |
|
73 |
/// Constructs a new ContinuousAxis |
|
74 |
pub fn new(ticks: &[String]) -> CategoricalAxis { |
|
75 |
CategoricalAxis { |
|
76 |
ticks: ticks.into(), |
|
77 |
label: "".into(), |
|
78 |
} |
|
79 |
} |
|
80 |
|
|
81 |
pub fn label<S>(mut self, l: S) -> Self |
|
82 |
where |
|
83 |
S: Into<String>, |
|
84 |
{ |
|
85 |
self.label = l.into(); |
|
86 |
self |
|
87 |
} |
|
88 |
|
|
89 |
pub fn get_label(&self) -> &str { |
|
90 |
self.label.as_ref() |
|
91 |
} |
|
92 |
|
|
93 |
/// Get the positions of the ticks on the axis |
|
94 |
pub fn ticks(&self) -> &Vec<String> { |
|
95 |
&self.ticks |
|
96 |
} |
|
97 |
}
|
|
98 |
|
|
99 |
/// The base units for the step sizes
|
|
100 |
/// They should be within one order of magnitude, e.g. [1,10)
|
|
101 |
const BASE_STEPS: [u32; 4] = [1, 2, 4, 5]; |
|
102 |
|
|
103 |
#[derive(Debug, Clone)]
|
|
104 |
struct TickSteps { |
|
105 |
next: f64, |
|
106 |
}
|
|
107 |
|
|
108 |
impl TickSteps { |
|
109 | 1 |
fn start_at(start: f64) -> TickSteps { |
110 | 1 |
let start_options = TickSteps::scaled_steps(start); |
111 | 1 |
let overflow = start_options[0] * 10.0; |
112 | 1 |
let curr = start_options.iter().find(|&step| step >= &start); |
113 |
|
|
114 |
TickSteps { |
|
115 | 1 |
next: *curr.unwrap_or(&overflow), |
116 |
} |
|
117 |
} |
|
118 |
|
|
119 | 1 |
fn scaled_steps(curr: f64) -> Vec<f64> { |
120 | 1 |
let power = curr.log10().floor(); |
121 | 1 |
let base_step_scale = 10f64.powf(power); |
122 | 1 |
BASE_STEPS |
123 |
.iter() |
|
124 |
.map(|&s| (f64::from(s) * base_step_scale)) |
|
125 |
.collect() |
|
126 |
} |
|
127 |
}
|
|
128 |
|
|
129 |
impl Iterator for TickSteps { |
|
130 |
type Item = f64; |
|
131 |
|
|
132 | 1 |
fn next(&mut self) -> Option<f64> { |
133 | 1 |
let curr = self.next; // cache the value we're currently on |
134 | 1 |
let curr_steps = TickSteps::scaled_steps(self.next); |
135 | 1 |
let overflow = curr_steps[0] * 10.0; |
136 | 1 |
self.next = *curr_steps.iter().find(|&s| s > &curr).unwrap_or(&overflow); |
137 | 1 |
Some(curr) |
138 |
} |
|
139 |
}
|
|
140 |
|
|
141 | 1 |
fn generate_ticks(min: f64, max: f64, step_size: f64) -> Vec<f64> { |
142 |
// "fix" just makes sure there are no floating-point errors |
|
143 | 1 |
fn fix(x: f64) -> f64 { |
144 |
const PRECISION: f64 = 100_000_f64; |
|
145 | 1 |
(x * PRECISION).round() / PRECISION |
146 |
} |
|
147 |
|
|
148 | 1 |
let mut ticks: Vec<f64> = vec![]; |
149 | 1 |
if min <= 0.0 { |
150 | 1 |
if max >= 0.0 { |
151 |
// standard spanning axis |
|
152 | 1 |
ticks.extend( |
153 | 1 |
(1..) |
154 | 1 |
.map(|n| -1.0 * fix(f64::from(n) * step_size)) |
155 | 1 |
.take_while(|&v| v >= min) |
156 |
.collect::<Vec<f64>>() |
|
157 |
.iter() |
|
158 |
.rev(), |
|
159 |
); |
|
160 | 1 |
ticks.push(0.0); |
161 | 1 |
ticks.extend( |
162 | 1 |
(1..) |
163 | 1 |
.map(|n| fix(f64::from(n) * step_size)) |
164 | 1 |
.take_while(|&v| v <= max), |
165 |
); |
|
166 |
} else { |
|
167 |
// entirely negative axis |
|
168 | 1 |
ticks.extend( |
169 | 1 |
(0..) |
170 | 1 |
.map(|n| -1.0 * fix((f64::from(n) * step_size) - max)) |
171 | 1 |
.take_while(|&v| v >= min) |
172 |
.collect::<Vec<f64>>() |
|
173 |
.iter() |
|
174 |
.rev(), |
|
175 |
); |
|
176 |
} |
|
177 |
} else { |
|
178 |
// entirely positive axis |
|
179 | 1 |
ticks.extend( |
180 | 1 |
(0..) |
181 | 1 |
.map(|n| fix((f64::from(n) * step_size) + min)) |
182 | 1 |
.take_while(|&v| v <= max), |
183 |
); |
|
184 |
} |
|
185 | 1 |
ticks |
186 |
}
|
|
187 |
|
|
188 |
/// Given a range and a step size, work out how many ticks will be displayed
|
|
189 | 1 |
fn number_of_ticks(min: f64, max: f64, step_size: f64) -> usize { |
190 | 1 |
generate_ticks(min, max, step_size).len() |
191 |
}
|
|
192 |
|
|
193 |
/// Given a range of values, and a maximum number of ticks, calulate the step between the ticks
|
|
194 | 1 |
fn calculate_tick_step_for_range(min: f64, max: f64, max_ticks: usize) -> f64 { |
195 | 1 |
let range = max - min; |
196 | 1 |
let min_tick_step = range / max_ticks as f64; |
197 |
// Get the first entry which is our smallest possible tick step size |
|
198 | 1 |
let smallest_valid_step = TickSteps::start_at(min_tick_step) |
199 | 1 |
.find(|&s| number_of_ticks(min, max, s) <= max_ticks) |
200 |
.expect("ERROR: We've somehow run out of tick step options!"); |
|
201 |
// Count how many ticks that relates to |
|
202 | 1 |
let actual_num_ticks = number_of_ticks(min, max, smallest_valid_step); |
203 |
|
|
204 |
// Create a new TickStep iterator, starting at the correct lower bound |
|
205 | 1 |
let tick_steps = TickSteps::start_at(smallest_valid_step); |
206 |
// Get all the possible tick step sizes that give just as many ticks |
|
207 | 1 |
let step_options = tick_steps.take_while(|&s| number_of_ticks(min, max, s) == actual_num_ticks); |
208 |
// Get the largest tick step size from the list |
|
209 | 1 |
step_options.fold(-1. / 0., f64::max) |
210 |
}
|
|
211 |
|
|
212 |
/// Given an axis range, calculate the sensible places to place the ticks
|
|
213 | 1 |
fn calculate_ticks(min: f64, max: f64, max_ticks: usize) -> Vec<f64> { |
214 | 1 |
let tick_step = calculate_tick_step_for_range(min, max, max_ticks); |
215 | 1 |
generate_ticks(min, max, tick_step) |
216 |
}
|
|
217 |
|
|
218 |
#[cfg(test)]
|
|
219 |
mod tests { |
|
220 |
use super::*; |
|
221 |
|
|
222 |
#[test] |
|
223 | 1 |
fn test_tick_step_generator() { |
224 | 1 |
let t = TickSteps::start_at(1.0); |
225 | 1 |
let ts: Vec<_> = t.take(7).collect(); |
226 | 1 |
assert_eq!(ts, [1.0, 2.0, 4.0, 5.0, 10.0, 20.0, 40.0]); |
227 |
|
|
228 | 1 |
let t = TickSteps::start_at(100.0); |
229 | 1 |
let ts: Vec<_> = t.take(5).collect(); |
230 | 1 |
assert_eq!(ts, [100.0, 200.0, 400.0, 500.0, 1000.0]); |
231 |
|
|
232 | 1 |
let t = TickSteps::start_at(3.0); |
233 | 1 |
let ts: Vec<_> = t.take(5).collect(); |
234 | 1 |
assert_eq!(ts, [4.0, 5.0, 10.0, 20.0, 40.0]); |
235 |
|
|
236 | 1 |
let t = TickSteps::start_at(8.0); |
237 | 1 |
let ts: Vec<_> = t.take(3).collect(); |
238 | 1 |
assert_eq!(ts, [10.0, 20.0, 40.0]); |
239 |
} |
|
240 |
|
|
241 |
#[test] |
|
242 | 1 |
fn test_number_of_ticks() { |
243 | 1 |
assert_eq!(number_of_ticks(-7.93, 15.58, 4.0), 5); |
244 | 1 |
assert_eq!(number_of_ticks(-7.93, 15.58, 5.0), 5); |
245 | 1 |
assert_eq!(number_of_ticks(0.0, 15.0, 4.0), 4); |
246 | 1 |
assert_eq!(number_of_ticks(0.0, 15.0, 5.0), 4); |
247 | 1 |
assert_eq!(number_of_ticks(5.0, 21.0, 4.0), 5); |
248 | 1 |
assert_eq!(number_of_ticks(5.0, 21.0, 5.0), 4); |
249 | 1 |
assert_eq!(number_of_ticks(-8.0, 15.58, 4.0), 6); |
250 | 1 |
assert_eq!(number_of_ticks(-8.0, 15.58, 5.0), 5); |
251 |
} |
|
252 |
|
|
253 |
#[test] |
|
254 | 1 |
fn test_calculate_tick_step_for_range() { |
255 | 1 |
assert_eq!(calculate_tick_step_for_range(0.0, 3.0, 6), 1.0); |
256 | 1 |
assert_eq!(calculate_tick_step_for_range(0.0, 6.0, 6), 2.0); |
257 | 1 |
assert_eq!(calculate_tick_step_for_range(0.0, 11.0, 6), 2.0); |
258 | 1 |
assert_eq!(calculate_tick_step_for_range(0.0, 14.0, 6), 4.0); |
259 | 1 |
assert_eq!(calculate_tick_step_for_range(0.0, 15.0, 6), 5.0); |
260 | 1 |
assert_eq!(calculate_tick_step_for_range(-1.0, 5.0, 6), 2.0); |
261 | 1 |
assert_eq!(calculate_tick_step_for_range(-7.93, 15.58, 6), 5.0); |
262 | 1 |
assert_eq!(calculate_tick_step_for_range(0.0, 0.06, 6), 0.02); |
263 |
} |
|
264 |
|
|
265 |
#[test] |
|
266 | 1 |
fn test_calculate_ticks() { |
267 |
macro_rules! assert_approx_eq { |
|
268 |
($a:expr, $b:expr) => {{ |
|
269 |
let (a, b) = (&$a, &$b); |
|
270 |
assert!( |
|
271 |
(*a - *b).abs() < 1.0e-6, |
|
272 |
"{} is not approximately equal to {}", |
|
273 |
*a, |
|
274 |
*b |
|
275 |
); |
|
276 |
}}; |
|
277 |
} |
|
278 |
|
|
279 | 1 |
for (prod, want) in calculate_ticks(0.0, 1.0, 6) |
280 |
.iter() |
|
281 | 1 |
.zip([0.0, 0.2, 0.4, 0.6, 0.8, 1.0].iter()) |
282 |
{ |
|
283 | 1 |
assert_approx_eq!(prod, want); |
284 |
} |
|
285 | 1 |
for (prod, want) in calculate_ticks(0.0, 2.0, 6) |
286 |
.iter() |
|
287 | 1 |
.zip([0.0, 0.4, 0.8, 1.2, 1.6, 2.0].iter()) |
288 |
{ |
|
289 | 1 |
assert_approx_eq!(prod, want); |
290 |
} |
|
291 | 1 |
assert_eq!(calculate_ticks(0.0, 3.0, 6), [0.0, 1.0, 2.0, 3.0]); |
292 | 1 |
assert_eq!(calculate_ticks(0.0, 4.0, 6), [0.0, 1.0, 2.0, 3.0, 4.0]); |
293 | 1 |
assert_eq!(calculate_ticks(0.0, 5.0, 6), [0.0, 1.0, 2.0, 3.0, 4.0, 5.0]); |
294 | 1 |
assert_eq!(calculate_ticks(0.0, 6.0, 6), [0.0, 2.0, 4.0, 6.0]); |
295 | 1 |
assert_eq!(calculate_ticks(0.0, 7.0, 6), [0.0, 2.0, 4.0, 6.0]); |
296 | 1 |
assert_eq!(calculate_ticks(0.0, 8.0, 6), [0.0, 2.0, 4.0, 6.0, 8.0]); |
297 | 1 |
assert_eq!(calculate_ticks(0.0, 9.0, 6), [0.0, 2.0, 4.0, 6.0, 8.0]); |
298 | 1 |
assert_eq!( |
299 | 1 |
calculate_ticks(0.0, 10.0, 6), |
300 |
[0.0, 2.0, 4.0, 6.0, 8.0, 10.0] |
|
301 |
); |
|
302 | 1 |
assert_eq!( |
303 | 1 |
calculate_ticks(0.0, 11.0, 6), |
304 |
[0.0, 2.0, 4.0, 6.0, 8.0, 10.0] |
|
305 |
); |
|
306 | 1 |
assert_eq!(calculate_ticks(0.0, 12.0, 6), [0.0, 4.0, 8.0, 12.0]); |
307 | 1 |
assert_eq!(calculate_ticks(0.0, 13.0, 6), [0.0, 4.0, 8.0, 12.0]); |
308 | 1 |
assert_eq!(calculate_ticks(0.0, 14.0, 6), [0.0, 4.0, 8.0, 12.0]); |
309 | 1 |
assert_eq!(calculate_ticks(0.0, 15.0, 6), [0.0, 5.0, 10.0, 15.0]); |
310 | 1 |
assert_eq!(calculate_ticks(0.0, 16.0, 6), [0.0, 4.0, 8.0, 12.0, 16.0]); |
311 | 1 |
assert_eq!(calculate_ticks(0.0, 17.0, 6), [0.0, 4.0, 8.0, 12.0, 16.0]); |
312 | 1 |
assert_eq!(calculate_ticks(0.0, 18.0, 6), [0.0, 4.0, 8.0, 12.0, 16.0]); |
313 | 1 |
assert_eq!(calculate_ticks(0.0, 19.0, 6), [0.0, 4.0, 8.0, 12.0, 16.0]); |
314 | 1 |
assert_eq!( |
315 | 1 |
calculate_ticks(0.0, 20.0, 6), |
316 |
[0.0, 4.0, 8.0, 12.0, 16.0, 20.0] |
|
317 |
); |
|
318 | 1 |
assert_eq!( |
319 | 1 |
calculate_ticks(0.0, 21.0, 6), |
320 |
[0.0, 4.0, 8.0, 12.0, 16.0, 20.0] |
|
321 |
); |
|
322 | 1 |
assert_eq!( |
323 | 1 |
calculate_ticks(0.0, 22.0, 6), |
324 |
[0.0, 4.0, 8.0, 12.0, 16.0, 20.0] |
|
325 |
); |
|
326 | 1 |
assert_eq!( |
327 | 1 |
calculate_ticks(0.0, 23.0, 6), |
328 |
[0.0, 4.0, 8.0, 12.0, 16.0, 20.0] |
|
329 |
); |
|
330 | 1 |
assert_eq!(calculate_ticks(0.0, 24.0, 6), [0.0, 5.0, 10.0, 15.0, 20.0]); |
331 | 1 |
assert_eq!( |
332 | 1 |
calculate_ticks(0.0, 25.0, 6), |
333 |
[0.0, 5.0, 10.0, 15.0, 20.0, 25.0] |
|
334 |
); |
|
335 | 1 |
assert_eq!( |
336 | 1 |
calculate_ticks(0.0, 26.0, 6), |
337 |
[0.0, 5.0, 10.0, 15.0, 20.0, 25.0] |
|
338 |
); |
|
339 | 1 |
assert_eq!( |
340 | 1 |
calculate_ticks(0.0, 27.0, 6), |
341 |
[0.0, 5.0, 10.0, 15.0, 20.0, 25.0] |
|
342 |
); |
|
343 | 1 |
assert_eq!( |
344 | 1 |
calculate_ticks(0.0, 28.0, 6), |
345 |
[0.0, 5.0, 10.0, 15.0, 20.0, 25.0] |
|
346 |
); |
|
347 | 1 |
assert_eq!( |
348 | 1 |
calculate_ticks(0.0, 29.0, 6), |
349 |
[0.0, 5.0, 10.0, 15.0, 20.0, 25.0] |
|
350 |
); |
|
351 | 1 |
assert_eq!(calculate_ticks(0.0, 30.0, 6), [0.0, 10.0, 20.0, 30.0]); |
352 | 1 |
assert_eq!(calculate_ticks(0.0, 31.0, 6), [0.0, 10.0, 20.0, 30.0]); |
353 |
//... |
|
354 | 1 |
assert_eq!(calculate_ticks(0.0, 40.0, 6), [0.0, 10.0, 20.0, 30.0, 40.0]); |
355 | 1 |
assert_eq!( |
356 | 1 |
calculate_ticks(0.0, 50.0, 6), |
357 |
[0.0, 10.0, 20.0, 30.0, 40.0, 50.0] |
|
358 |
); |
|
359 | 1 |
assert_eq!(calculate_ticks(0.0, 60.0, 6), [0.0, 20.0, 40.0, 60.0]); |
360 | 1 |
assert_eq!(calculate_ticks(0.0, 70.0, 6), [0.0, 20.0, 40.0, 60.0]); |
361 | 1 |
assert_eq!(calculate_ticks(0.0, 80.0, 6), [0.0, 20.0, 40.0, 60.0, 80.0]); |
362 | 1 |
assert_eq!(calculate_ticks(0.0, 90.0, 6), [0.0, 20.0, 40.0, 60.0, 80.0]); |
363 | 1 |
assert_eq!( |
364 | 1 |
calculate_ticks(0.0, 100.0, 6), |
365 |
[0.0, 20.0, 40.0, 60.0, 80.0, 100.0] |
|
366 |
); |
|
367 | 1 |
assert_eq!( |
368 | 1 |
calculate_ticks(0.0, 110.0, 6), |
369 |
[0.0, 20.0, 40.0, 60.0, 80.0, 100.0] |
|
370 |
); |
|
371 | 1 |
assert_eq!(calculate_ticks(0.0, 120.0, 6), [0.0, 40.0, 80.0, 120.0]); |
372 | 1 |
assert_eq!(calculate_ticks(0.0, 130.0, 6), [0.0, 40.0, 80.0, 120.0]); |
373 | 1 |
assert_eq!(calculate_ticks(0.0, 140.0, 6), [0.0, 40.0, 80.0, 120.0]); |
374 | 1 |
assert_eq!(calculate_ticks(0.0, 150.0, 6), [0.0, 50.0, 100.0, 150.0]); |
375 |
//... |
|
376 | 1 |
assert_eq!( |
377 | 1 |
calculate_ticks(0.0, 3475.0, 6), |
378 |
[0.0, 1000.0, 2000.0, 3000.0] |
|
379 |
); |
|
380 |
|
|
381 | 1 |
assert_eq!(calculate_ticks(-11.0, -4.0, 6), [-10.0, -8.0, -6.0, -4.0]); |
382 |
|
|
383 |
// test rounding |
|
384 | 1 |
assert_eq!(calculate_ticks(1.0, 1.5, 6), [1.0, 1.1, 1.2, 1.3, 1.4, 1.5]); |
385 | 1 |
assert_eq!(calculate_ticks(0.0, 1.0, 6), [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]); |
386 | 1 |
assert_eq!(calculate_ticks(0.0, 0.3, 4), [0.0, 0.1, 0.2, 0.3]); |
387 |
} |
|
388 |
}
|
Read our documentation on viewing source code .