pub fn lttb(points: &[(f64, f64)], threshold: usize) -> Vec<(f64, f64)> {
let n = points.len();
if n == 0 || threshold == 0 {
return Vec::new();
}
if threshold >= n {
return points.to_vec();
}
if threshold == 1 {
return vec![points[0]];
}
if threshold == 2 {
return vec![points[0], points[n - 1]];
}
let bucket_count = threshold - 2;
let bucket_size = (n - 2) as f64 / bucket_count as f64;
let mut sampled = Vec::with_capacity(threshold);
sampled.push(points[0]);
let mut anchor_index = 0usize;
for i in 0..bucket_count {
let range_start = (i as f64 * bucket_size) as usize + 1;
let range_end = (((i + 1) as f64 * bucket_size) as usize + 1).min(n);
let next_start = (((i + 1) as f64) * bucket_size) as usize + 1;
let next_end = (((i + 2) as f64) * bucket_size) as usize + 1;
let next_end = next_end.min(n);
let next_anchor = if next_start < next_end {
let (sum_x, sum_y) = points[next_start..next_end].iter().fold((0.0, 0.0), |(sx, sy), &(x, y)| (sx + x, sy + y));
let len = (next_end - next_start) as f64;
(sum_x / len, sum_y / len)
} else {
points[n - 1]
};
let (anchor_x, anchor_y) = points[anchor_index];
let mut best_area = -1.0_f64;
let mut best_index = range_start.min(n - 1);
for j in range_start..range_end {
let (px, py) = points[j];
let area = ((anchor_x - next_anchor.0) * (py - anchor_y) - (anchor_x - px) * (next_anchor.1 - anchor_y)).abs();
if area > best_area {
best_area = area;
best_index = j;
}
}
sampled.push(points[best_index]);
anchor_index = best_index;
}
sampled.push(points[n - 1]);
sampled
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn threshold_at_or_above_len_is_identity() {
let points: Vec<(f64, f64)> = (0..10).map(|i| (i as f64, (i as f64).sin())).collect();
assert_eq!(lttb(&points, 10), points);
assert_eq!(lttb(&points, 50), points);
}
#[test]
fn empty_and_degenerate_thresholds_never_panic() {
assert_eq!(lttb(&[], 10), Vec::<(f64, f64)>::new());
let points = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 4.0)];
assert_eq!(lttb(&points, 0), Vec::<(f64, f64)>::new());
assert_eq!(lttb(&points, 1), vec![(0.0, 0.0)]);
assert_eq!(lttb(&points, 2), vec![(0.0, 0.0), (2.0, 4.0)]);
}
#[test]
fn keeps_first_and_last_point_verbatim() {
let points: Vec<(f64, f64)> = (0..1000).map(|i| (i as f64, ((i as f64) * 0.01).sin() * 10.0)).collect();
let out = lttb(&points, 50);
assert_eq!(out.len(), 50);
assert_eq!(out[0], points[0]);
assert_eq!(out[out.len() - 1], points[points.len() - 1]);
}
#[test]
fn a_synthetic_spike_survives_1000_to_50_downsample() {
const N: usize = 1000;
const SPIKE_INDEX: usize = 417; const SPIKE_Y: f64 = 500.0;
let points: Vec<(f64, f64)> = (0..N)
.map(|i| {
let baseline = ((i as f64) * 0.02).sin() * 5.0;
if i == SPIKE_INDEX { (i as f64, SPIKE_Y) } else { (i as f64, baseline) }
})
.collect();
let out = lttb(&points, 50);
assert_eq!(out.len(), 50);
assert!(
out.iter().any(|&(x, y)| x == SPIKE_INDEX as f64 && y == SPIKE_Y),
"the spike point must be one of the 50 selected points, got: {out:?}"
);
}
#[test]
fn deterministic_across_repeated_calls() {
let points: Vec<(f64, f64)> = (0..733).map(|i| (i as f64, ((i as f64) * 0.037).cos() * 3.0 + (i as f64 % 17.0))).collect();
let a = lttb(&points, 40);
let b = lttb(&points, 40);
assert_eq!(a, b);
}
#[test]
fn output_length_matches_threshold_when_downsampling() {
let points: Vec<(f64, f64)> = (0..200).map(|i| (i as f64, i as f64 * 0.5)).collect();
for threshold in [3usize, 10, 51, 100] {
let out = lttb(&points, threshold);
assert_eq!(out.len(), threshold, "threshold={threshold}");
assert_eq!(out[0], points[0]);
assert_eq!(*out.last().unwrap(), *points.last().unwrap());
}
}
}