1#[must_use]
35pub fn lttb(x: &[f64], y: &[f64], count: usize) -> Vec<usize> {
36 let length = x.len().min(y.len());
37 if length <= count || count < 3 {
38 return (0..length).collect();
39 }
40
41 let mut kept = Vec::with_capacity(count);
42 kept.push(0);
43
44 let bucket = (length - 2) as f64 / (count - 2) as f64;
45 let mut previous = 0;
46
47 for index in 0..count - 2 {
48 let start = 1 + (index as f64 * bucket).floor() as usize;
49 let end = (1 + ((index + 1) as f64 * bucket).floor() as usize).min(length - 1);
50 let ahead = if index < count - 3 {
51 (1 + ((index + 2) as f64 * bucket).floor() as usize).min(length)
52 } else {
53 length
54 };
55
56 let (mean_x, mean_y) = mean_of(x, y, end..ahead).unwrap_or((x[length - 1], y[length - 1]));
57
58 let picked = (start..end)
59 .max_by(|left, right| {
60 area(x[previous], y[previous], x[*left], y[*left], mean_x, mean_y).total_cmp(&area(
61 x[previous],
62 y[previous],
63 x[*right],
64 y[*right],
65 mean_x,
66 mean_y,
67 ))
68 })
69 .unwrap_or(start);
70
71 kept.push(picked);
72 previous = picked;
73 }
74
75 kept.push(length - 1);
76 kept
77}
78
79#[must_use]
91pub fn lttb_log(x: &[f64], y: &[f64], count: usize) -> Vec<usize> {
92 let length = x.len().min(y.len());
93 if length <= count || count < 3 {
94 return (0..length).collect();
95 }
96
97 let (first, last) = (positive(x[0]), x[length - 1]);
98 if !last.is_finite() || last <= first {
99 return lttb(x, y, count);
100 }
101
102 let (low, span) = (first.ln(), last.ln() - first.ln());
103 if span <= 0.0 {
104 return lttb(x, y, count);
105 }
106
107 let buckets = count - 2;
108 let edge = |bucket: usize| (low + (bucket as f64 / buckets as f64) * span).exp();
109
110 let mut kept = Vec::with_capacity(count);
111 kept.push(0);
112 let mut previous = 0;
113
114 for bucket in 0..buckets {
115 let start = x[..length]
116 .partition_point(|at| *at < edge(bucket))
117 .min(length - 1);
118 let end = x[..length]
119 .partition_point(|at| *at < edge(bucket + 1))
120 .clamp(start + 1, length);
121 let ahead = if bucket < buckets - 1 {
122 x[..length]
123 .partition_point(|at| *at < edge(bucket + 2).min(last))
124 .clamp(end, length)
125 } else {
126 length
127 };
128
129 let (mean_x, mean_y) = mean_of(x, y, end..ahead).unwrap_or((x[length - 1], y[length - 1]));
130 let (previous_x, mean_x) = (positive(x[previous]).ln(), positive(mean_x).ln());
132
133 let picked = (start..end)
134 .max_by(|left, right| {
135 let of = |bin: usize| {
136 area(
137 previous_x,
138 y[previous],
139 positive(x[bin]).ln(),
140 y[bin],
141 mean_x,
142 mean_y,
143 )
144 };
145 of(*left).total_cmp(&of(*right))
146 })
147 .unwrap_or(start);
148
149 kept.push(picked);
150 previous = picked;
151 }
152
153 kept.push(length - 1);
154 kept.dedup();
155 kept
156}
157
158#[must_use]
164pub fn decimate(length: usize, count: usize) -> Vec<usize> {
165 if length == 0 || count == 0 {
166 return Vec::new();
167 }
168
169 let stride = (length as f64 / count as f64).ceil() as usize;
170 (0..length).step_by(stride.max(1)).collect()
171}
172
173#[must_use]
180pub fn extrema(values: &[f64], buckets: usize) -> Vec<usize> {
181 if values.is_empty() || buckets == 0 {
182 return Vec::new();
183 }
184
185 let span = ((values.len() as f64 / buckets as f64).ceil() as usize).max(1);
186 let mut kept = Vec::with_capacity(buckets * 2);
187
188 for (bucket, samples) in values.chunks(span).enumerate() {
189 let (mut lowest, mut highest) = (0, 0);
192 for (index, value) in samples.iter().enumerate() {
193 if *value < samples[lowest] {
194 lowest = index;
195 }
196 if *value > samples[highest] {
197 highest = index;
198 }
199 }
200
201 let base = bucket * span;
202 kept.push(base + lowest.min(highest));
203 if lowest != highest {
204 kept.push(base + lowest.max(highest));
205 }
206 }
207
208 kept
209}
210
211#[must_use]
219pub fn above_floor(values: &[f64], db_below_peak: f64) -> Vec<usize> {
220 let peak = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
221 if peak.is_nan() || peak <= 0.0 {
222 return Vec::new();
223 }
224
225 let floor = peak * 10.0_f64.powf(-db_below_peak / 20.0);
226 values
227 .iter()
228 .enumerate()
229 .filter(|(_, value)| **value >= floor)
230 .map(|(index, _)| index)
231 .collect()
232}
233
234fn positive(at: f64) -> f64 {
240 at.max(f64::MIN_POSITIVE)
241}
242
243fn area(x0: f64, y0: f64, x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
245 ((x0 - x2) * (y1 - y0) - (x0 - x1) * (y2 - y0)).abs()
246}
247
248fn mean_of(x: &[f64], y: &[f64], range: std::ops::Range<usize>) -> Option<(f64, f64)> {
250 let count = range.len();
251 if count == 0 {
252 return None;
253 }
254
255 let sum_x: f64 = x[range.clone()].iter().sum();
256 let sum_y: f64 = y[range].iter().sum();
257
258 Some((sum_x / count as f64, sum_y / count as f64))
259}
260
261#[cfg(test)]
262mod tests {
263 use std::f64::consts::TAU;
264
265 use super::*;
266
267 fn run(count: usize, f: impl Fn(f64) -> f64) -> (Vec<f64>, Vec<f64>) {
269 (0..count)
270 .map(|index| (index as f64, f(index as f64)))
271 .unzip()
272 }
273
274 #[test]
275 fn lttb_keeps_the_endpoints_and_the_count_asked_for() {
276 let (x, y) = run(1_000, |at| (at * 0.01).sin());
277
278 let kept = lttb(&x, &y, 100);
279
280 assert_eq!(kept.len(), 100);
281 assert_eq!(kept[0], 0);
282 assert_eq!(kept[99], 999);
283 }
284
285 #[test]
286 fn lttb_answers_strictly_increasing_indices() {
287 let (x, y) = run(1_000, |at| (at * 0.05).sin());
288
289 let kept = lttb(&x, &y, 137);
290
291 assert!(kept.windows(2).all(|pair| pair[0] < pair[1]));
292 }
293
294 #[test]
295 fn lttb_keeps_a_spike_a_stride_would_miss() {
296 let (x, y) = run(100, |at| if (at - 53.0).abs() < 0.5 { 100.0 } else { 0.0 });
297
298 let kept = lttb(&x, &y, 20);
299
300 assert!(
301 kept.contains(&53),
302 "the spike at 53 was thinned away: {kept:?}"
303 );
304 assert!(
305 !decimate(100, 20).contains(&53),
306 "a stride of five steps straight over it"
307 );
308 }
309
310 #[test]
311 fn asking_for_more_than_there_is_keeps_everything() {
312 let (x, y) = run(10, |at| at);
313
314 assert_eq!(lttb(&x, &y, 50), (0..10).collect::<Vec<_>>());
315 assert_eq!(lttb(&x, &y, 2), (0..10).collect::<Vec<_>>());
316 assert_eq!(lttb_log(&x, &y, 50), (0..10).collect::<Vec<_>>());
317 assert!(lttb(&[], &[], 10).is_empty());
318 }
319
320 #[test]
321 fn log_buckets_spend_points_on_the_low_decades() {
322 let x: Vec<f64> = (1..=10_000).map(|bin| f64::from(bin) * 1_000.0).collect();
325 let y: Vec<f64> = x.iter().map(|at| (at.ln() * 0.5).sin()).collect();
326
327 let linear = lttb(&x, &y, 100);
328 let logarithmic = lttb_log(&x, &y, 100);
329
330 let below = |kept: &[usize]| kept.iter().filter(|bin| x[**bin] < 1e6).count();
331
332 assert!(below(&logarithmic) > below(&linear));
333 assert!(logarithmic.len() <= 100);
334 assert_eq!(logarithmic[0], 0);
335 assert_eq!(logarithmic[logarithmic.len() - 1], 9_999);
336 }
337
338 #[test]
339 fn log_buckets_answer_increasing_indices() {
340 let x: Vec<f64> = (1..=5_000).map(|bin| f64::from(bin) * 1_000.0).collect();
341 let y: Vec<f64> = x.iter().map(|at| 1_000.0 / at.sqrt()).collect();
342
343 let kept = lttb_log(&x, &y, 50);
344
345 assert!(kept.windows(2).all(|pair| pair[0] < pair[1]), "{kept:?}");
346 }
347
348 #[test]
349 fn an_axis_with_no_logarithmic_span_falls_back_to_linear_buckets() {
350 let x = vec![5.0; 100];
351 let y: Vec<f64> = (0..100).map(f64::from).collect();
352
353 assert_eq!(lttb_log(&x, &y, 10), lttb(&x, &y, 10));
354 }
355
356 #[test]
357 fn decimation_strides_and_never_overruns() {
358 assert_eq!(decimate(10, 5), vec![0, 2, 4, 6, 8]);
359 assert_eq!(decimate(100, 10).len(), 10);
360 assert!(decimate(0, 10).is_empty());
361 assert!(decimate(10, 0).is_empty());
362 }
363
364 #[test]
365 fn extrema_keep_both_ends_of_every_bucket() {
366 let (_, y) = run(100, |at| (at * 0.1 * TAU).sin());
367
368 let kept = extrema(&y, 10);
369
370 assert!(kept.len() <= 20);
371 assert!(kept.windows(2).all(|pair| pair[0] < pair[1]), "{kept:?}");
372
373 let highest = y.iter().copied().fold(f64::NEG_INFINITY, f64::max);
374 let lowest = y.iter().copied().fold(f64::INFINITY, f64::min);
375 assert!(kept.iter().any(|index| (y[*index] - highest).abs() < 1e-12));
376 assert!(kept.iter().any(|index| (y[*index] - lowest).abs() < 1e-12));
377 }
378
379 #[test]
380 fn a_flat_bucket_contributes_one_sample() {
381 assert_eq!(extrema(&[1.0, 1.0, 1.0, 1.0], 2), vec![0, 2]);
382 assert!(extrema(&[], 4).is_empty());
383 assert!(extrema(&[1.0], 0).is_empty());
384 }
385
386 #[test]
387 fn the_floor_keeps_what_is_within_the_stated_decibels() {
388 let mut values = vec![1.0; 100];
389 values[50] = 100.0;
390
391 assert_eq!(above_floor(&values, 40.0).len(), 100);
393 assert_eq!(above_floor(&values, 20.0), vec![50]);
395 }
396
397 #[test]
398 fn a_run_with_no_positive_sample_has_no_floor() {
399 assert!(above_floor(&[0.0, 0.0], 40.0).is_empty());
400 assert!(above_floor(&[-1.0, -2.0], 40.0).is_empty());
401 assert!(above_floor(&[], 40.0).is_empty());
402 }
403}