use flow_utils::KernelDensity2D;
use ndarray::Array2;
#[derive(Clone, Debug)]
pub struct ContourData {
pub contours: Vec<ContourPath>,
pub outliers: Vec<(f64, f64)>,
}
pub type ContourPath = Vec<(f64, f64)>;
pub fn calculate_contours(
data: &[(f32, f32)],
level_count: u32,
smoothing: f64,
draw_outliers: bool,
_x_range: (f32, f32),
_y_range: (f32, f32),
) -> Result<ContourData, flow_utils::KdeError> {
if data.len() < 3 {
return Ok(ContourData {
contours: Vec::new(),
outliers: data
.iter()
.map(|&(x, y)| (x as f64, y as f64))
.collect(),
});
}
let data_x: Vec<f64> = data.iter().map(|(x, _)| *x as f64).collect();
let data_y: Vec<f64> = data.iter().map(|(_, y)| *y as f64).collect();
let n_grid = 128usize;
let kde = KernelDensity2D::estimate(&data_x, &data_y, smoothing, n_grid)?;
let max_density = kde
.z
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max)
.max(1e-300);
let levels: Vec<f64> = if level_count <= 1 {
vec![0.1 * max_density]
} else {
(1..=level_count as usize)
.map(|i| {
let t = i as f64 / (level_count as f64 + 1.0);
let frac = 0.01 + 0.98 * t;
frac * max_density
})
.collect()
};
let mut contours = Vec::with_capacity(levels.len());
for &threshold in &levels {
let paths = marching_squares_contours(&kde.x, &kde.y, &kde.z, threshold);
contours.extend(paths);
}
let outliers = if draw_outliers {
let min_threshold = levels.first().copied().unwrap_or(0.01 * max_density);
data.iter()
.filter(|&&(x, y)| {
let d = kde.density_at(x as f64, y as f64);
d < min_threshold
})
.map(|&(x, y)| (x as f64, y as f64))
.collect()
} else {
Vec::new()
};
Ok(ContourData {
contours,
outliers,
})
}
fn marching_squares_contours(
x: &[f64],
y: &[f64],
z: &Array2<f64>,
threshold: f64,
) -> Vec<ContourPath> {
let nx = x.len();
let ny = y.len();
if nx < 2 || ny < 2 {
return Vec::new();
}
let mut segments = Vec::new();
for i in 0..nx - 1 {
for j in 0..ny - 1 {
let v00 = z[[i, j]];
let v10 = z[[i + 1, j]];
let v01 = z[[i, j + 1]];
let v11 = z[[i + 1, j + 1]];
let c00 = v00 >= threshold;
let c10 = v10 >= threshold;
let c01 = v01 >= threshold;
let c11 = v11 >= threshold;
let case = (c00 as u8) | ((c10 as u8) << 1) | ((c01 as u8) << 2) | ((c11 as u8) << 3);
if case == 0 || case == 15 {
continue; }
let lerp = |a: f64, b: f64, va: f64, vb: f64| -> f64 {
if (va >= threshold) == (vb >= threshold) {
return a; }
let t = (threshold - va) / (vb - va).max(1e-300);
a + t * (b - a)
};
let x0 = x[i];
let x1 = x[i + 1];
let y0 = y[j];
let y1 = y[j + 1];
let bottom = (lerp(x0, x1, v00, v10), y0);
let right = (x1, lerp(y0, y1, v10, v11));
let top = (lerp(x1, x0, v11, v01), y1);
let left = (x0, lerp(y1, y0, v01, v00));
match case {
1 => segments.push((left, bottom)),
2 => segments.push((bottom, right)),
3 => segments.push((left, right)),
4 => segments.push((top, right)),
5 => {
segments.push((left, bottom));
segments.push((top, right));
}
6 => segments.push((bottom, top)),
7 => segments.push((left, top)),
8 => segments.push((top, left)),
9 => segments.push((bottom, top)),
10 => {
segments.push((bottom, left));
segments.push((top, right));
}
11 => segments.push((bottom, top)),
12 => segments.push((right, left)),
13 => segments.push((right, bottom)),
14 => segments.push((top, left)),
_ => {}
}
}
}
chain_segments_into_paths(segments)
}
fn chain_segments_into_paths(segments: Vec<((f64, f64), (f64, f64))>) -> Vec<ContourPath> {
const EQ_EPS: f64 = 1e-10;
fn points_eq(a: (f64, f64), b: (f64, f64)) -> bool {
(a.0 - b.0).abs() < EQ_EPS && (a.1 - b.1).abs() < EQ_EPS
}
let mut used = vec![false; segments.len()];
let mut paths = Vec::new();
for start_idx in 0..segments.len() {
if used[start_idx] {
continue;
}
let (p0, p1) = segments[start_idx];
used[start_idx] = true;
let mut path = vec![p0, p1];
loop {
let mut found = false;
for (idx, &(a, b)) in segments.iter().enumerate() {
if used[idx] {
continue;
}
let last = *path.last().unwrap();
if points_eq(last, a) {
path.push(b);
used[idx] = true;
found = true;
break;
} else if points_eq(last, b) {
path.push(a);
used[idx] = true;
found = true;
break;
}
}
if !found {
break;
}
if path.len() >= 3 && points_eq(path.first().copied().unwrap(), *path.last().unwrap()) {
path.pop(); break;
}
}
if path.len() >= 2 {
if path.len() >= 3 && points_eq(path[0], path[path.len() - 1]) {
path.pop();
}
paths.push(path);
}
}
paths
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_contour_empty_data() {
let data: Vec<(f32, f32)> = vec![];
let result = calculate_contours(&data, 5, 1.0, false, (0.0, 100.0), (0.0, 100.0));
assert!(result.is_ok());
let contour_data = result.unwrap();
assert!(contour_data.contours.is_empty());
assert!(contour_data.outliers.is_empty());
}
#[test]
fn test_contour_small_data() {
let data = vec![(1.0f32, 2.0f32), (2.0, 3.0), (3.0, 4.0)];
let result = calculate_contours(&data, 3, 1.0, false, (0.0, 10.0), (0.0, 10.0));
assert!(result.is_ok());
}
#[test]
fn test_contour_clustered_data() {
let mut data: Vec<(f32, f32)> = Vec::new();
for xi in 0..20 {
for yi in 0..20 {
data.push((100.0 + xi as f32 * 2.0, 100.0 + yi as f32 * 2.0));
}
}
let result = calculate_contours(&data, 5, 0.8, false, (0.0, 200.0), (0.0, 200.0));
assert!(result.is_ok());
let contour_data = result.unwrap();
assert!(
!contour_data.contours.is_empty(),
"dense cluster should produce at least one contour path"
);
}
}