damascene_core/plot/
decimate.rs1#![warn(missing_docs)]
12
13use crate::plot::series::Sample;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum Decimation {
18 MinMax,
21}
22
23pub fn minmax(samples: &[Sample], x_window: (f64, f64), buckets: usize) -> Vec<Sample> {
29 let lo = x_window.0.min(x_window.1);
30 let hi = x_window.0.max(x_window.1);
31 let span = hi - lo;
32 if buckets == 0 || !span.is_finite() || span <= 0.0 || samples.len() <= buckets * 2 {
33 return samples.to_vec();
34 }
35
36 let mut cols: Vec<Option<(Sample, Sample)>> = vec![None; buckets];
38 for &s in samples {
39 if !s.x.is_finite() || !s.y.is_finite() || s.x < lo || s.x > hi {
40 continue;
41 }
42 let frac = (s.x - lo) / span;
43 let b = ((frac * buckets as f64) as usize).min(buckets - 1);
44 match &mut cols[b] {
45 None => cols[b] = Some((s, s)),
46 Some((mn, mx)) => {
47 if s.y < mn.y {
48 *mn = s;
49 }
50 if s.y > mx.y {
51 *mx = s;
52 }
53 }
54 }
55 }
56
57 let mut out = Vec::with_capacity(buckets * 2);
58 for (mn, mx) in cols.into_iter().flatten() {
59 let (first, second) = if mn.x <= mx.x { (mn, mx) } else { (mx, mn) };
62 out.push(first);
63 if second.x != first.x || second.y != first.y {
64 out.push(second);
65 }
66 }
67 out
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 fn series(n: usize) -> Vec<Sample> {
75 (0..n)
76 .map(|i| Sample::new(i as f64, (i as f64 * 0.1).sin()))
77 .collect()
78 }
79
80 #[test]
81 fn within_budget_is_unchanged() {
82 let s = series(10);
83 assert_eq!(minmax(&s, (0.0, 9.0), 8), s);
84 }
85
86 #[test]
87 fn reduces_to_envelope_within_budget() {
88 let s = series(10_000);
89 let out = minmax(&s, (0.0, 9_999.0), 100);
90 assert!(out.len() <= 200, "≤ 2 per bucket, got {}", out.len());
91 assert!(
92 out.len() > 100,
93 "keeps a useful envelope, got {}",
94 out.len()
95 );
96 assert!(out.windows(2).all(|w| w[0].x <= w[1].x));
98 }
99
100 #[test]
101 fn preserves_spikes() {
102 let mut s: Vec<Sample> = (0..1000).map(|i| Sample::new(i as f64, 0.0)).collect();
104 s[500].y = 99.0;
105 let out = minmax(&s, (0.0, 999.0), 50);
106 assert!(
107 out.iter().any(|p| p.y == 99.0),
108 "the spike must survive decimation"
109 );
110 }
111
112 #[test]
113 fn drops_out_of_window_samples() {
114 let s = series(1000);
115 let out = minmax(&s, (400.0, 600.0), 50);
116 assert!(out.iter().all(|p| p.x >= 400.0 && p.x <= 600.0));
117 }
118
119 #[test]
120 fn degenerate_window_returns_input() {
121 let s = series(1000);
122 assert_eq!(minmax(&s, (5.0, 5.0), 50), s);
123 }
124}