use egui::Color32;
use crate::core::triangles::Triangles;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GridMajorOrder {
Row,
Column,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BinnedStatisticFunction {
Mean,
Count,
Sum,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Triangulation {
pub triangles: Vec<[usize; 3]>,
}
impl Triangulation {
#[must_use]
pub fn len(&self) -> usize {
self.triangles.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.triangles.is_empty()
}
}
fn orient2d(a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> f64 {
(b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
}
fn in_circumcircle(a: [f64; 2], b: [f64; 2], c: [f64; 2], p: [f64; 2]) -> bool {
let ax = a[0] - p[0];
let ay = a[1] - p[1];
let bx = b[0] - p[0];
let by = b[1] - p[1];
let cx = c[0] - p[0];
let cy = c[1] - p[1];
let a2 = ax * ax + ay * ay;
let b2 = bx * bx + by * by;
let c2 = cx * cx + cy * cy;
let det = ax * (by * c2 - b2 * cy) - ay * (bx * c2 - b2 * cx) + a2 * (bx * cy - by * cx);
det > 0.0
}
#[must_use]
pub fn delaunay(x: &[f64], y: &[f64]) -> Triangulation {
assert_eq!(x.len(), y.len(), "x and y must have the same length");
let pts: Vec<(usize, [f64; 2])> = x
.iter()
.zip(y)
.enumerate()
.filter(|&(_, (&xi, &yi))| xi.is_finite() && yi.is_finite())
.map(|(i, (&xi, &yi))| (i, [xi, yi]))
.collect();
if pts.len() < 3 {
return Triangulation { triangles: vec![] };
}
let p0 = pts[0].1;
let has_area = pts.iter().enumerate().any(|(i, &(_, pi))| {
pts[i + 1..]
.iter()
.any(|&(_, pj)| orient2d(p0, pi, pj).abs() > 0.0)
});
if !has_area {
return Triangulation { triangles: vec![] };
}
let (mut min_x, mut min_y, mut max_x, mut max_y) = (
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
);
for &(_, [px, py]) in &pts {
min_x = min_x.min(px);
min_y = min_y.min(py);
max_x = max_x.max(px);
max_y = max_y.max(py);
}
let dx = max_x - min_x;
let dy = max_y - min_y;
let d = dx.max(dy).max(f64::MIN_POSITIVE);
let mid_x = 0.5 * (min_x + max_x);
let mid_y = 0.5 * (min_y + max_y);
let st0 = [mid_x - 20.0 * d, mid_y - d];
let st1 = [mid_x, mid_y + 20.0 * d];
let st2 = [mid_x + 20.0 * d, mid_y - d];
let n = pts.len();
let mut verts: Vec<[f64; 2]> = pts.iter().map(|&(_, p)| p).collect();
verts.push(st0);
verts.push(st1);
verts.push(st2);
let s0 = n;
let s1 = n + 1;
let s2 = n + 2;
let mut tris: Vec<[usize; 3]> = vec![ccw(&verts, [s0, s1, s2])];
for ip in 0..n {
let p = verts[ip];
let mut bad: Vec<usize> = Vec::new();
for (ti, t) in tris.iter().enumerate() {
if in_circumcircle(verts[t[0]], verts[t[1]], verts[t[2]], p) {
bad.push(ti);
}
}
let mut boundary: Vec<[usize; 2]> = Vec::new();
for &bi in &bad {
let t = tris[bi];
for &(a, b) in &[(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
let shared = bad
.iter()
.any(|&oi| oi != bi && triangle_has_edge(&tris[oi], a, b));
if !shared {
boundary.push([a, b]);
}
}
}
bad.sort_unstable();
for &bi in bad.iter().rev() {
tris.swap_remove(bi);
}
for [a, b] in boundary {
tris.push(ccw(&verts, [a, b, ip]));
}
}
let original: Vec<usize> = pts.iter().map(|&(i, _)| i).collect();
let triangles: Vec<[usize; 3]> = tris
.into_iter()
.filter(|t| t.iter().all(|&v| v < n))
.map(|t| [original[t[0]], original[t[1]], original[t[2]]])
.collect();
Triangulation { triangles }
}
fn ccw(verts: &[[f64; 2]], t: [usize; 3]) -> [usize; 3] {
if orient2d(verts[t[0]], verts[t[1]], verts[t[2]]) < 0.0 {
[t[0], t[2], t[1]]
} else {
t
}
}
fn triangle_has_edge(t: &[usize; 3], a: usize, b: usize) -> bool {
let edges = [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])];
edges
.iter()
.any(|&(u, v)| (u == a && v == b) || (u == b && v == a))
}
#[must_use]
pub fn solid_triangles(x: &[f64], y: &[f64], colors: &[Color32]) -> Option<Triangles> {
assert_eq!(x.len(), y.len(), "x and y must have the same length");
assert_eq!(
colors.len(),
x.len(),
"colors must have one entry per vertex"
);
let tri = delaunay(x, y);
if tri.is_empty() {
return None;
}
let indices: Vec<[u32; 3]> = tri
.triangles
.iter()
.map(|t| {
[
u32::try_from(t[0]).expect("vertex index fits in u32"),
u32::try_from(t[1]).expect("vertex index fits in u32"),
u32::try_from(t[2]).expect("vertex index fits in u32"),
]
})
.collect();
Some(Triangles::new(
x.to_vec(),
y.to_vec(),
indices,
colors.to_vec(),
))
}
fn barycentric(a: [f64; 2], b: [f64; 2], c: [f64; 2], p: [f64; 2]) -> Option<[f64; 3]> {
let det = orient2d(a, b, c);
if det == 0.0 {
return None;
}
let wa = orient2d(b, c, p) / det;
let wb = orient2d(c, a, p) / det;
let wc = orient2d(a, b, p) / det;
let eps = -1e-9;
if wa >= eps && wb >= eps && wc >= eps {
Some([wa, wb, wc])
} else {
None
}
}
#[must_use]
pub fn interpolate(
tri: &Triangulation,
x: &[f64],
y: &[f64],
values: &[f64],
px: f64,
py: f64,
) -> Option<f64> {
let p = [px, py];
for t in &tri.triangles {
let a = [x[t[0]], y[t[0]]];
let b = [x[t[1]], y[t[1]]];
let c = [x[t[2]], y[t[2]]];
if let Some([wa, wb, wc]) = barycentric(a, b, c, p) {
return Some(wa * values[t[0]] + wb * values[t[1]] + wc * values[t[2]]);
}
}
None
}
#[derive(Clone, Debug, PartialEq)]
pub struct GridImage {
pub data: Vec<f64>,
pub shape: (usize, usize),
pub origin: (f64, f64),
pub scale: (f64, f64),
}
impl GridImage {
#[must_use]
pub fn get(&self, r: usize, c: usize) -> Option<f64> {
if r < self.shape.0 && c < self.shape.1 {
Some(self.data[r * self.shape.1 + c])
} else {
None
}
}
}
#[must_use]
pub fn irregular_grid_image(
x: &[f64],
y: &[f64],
values: &[f64],
rows: usize,
cols: usize,
) -> Option<GridImage> {
assert_eq!(x.len(), y.len(), "x and y must have the same length");
assert_eq!(
values.len(),
x.len(),
"values must have one entry per point"
);
if rows == 0 || cols == 0 {
return None;
}
let tri = delaunay(x, y);
if tri.is_empty() {
return None;
}
let (mut min_x, mut min_y, mut max_x, mut max_y) = (
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
);
for (&xi, &yi) in x.iter().zip(y) {
if xi.is_finite() && yi.is_finite() {
min_x = min_x.min(xi);
min_y = min_y.min(yi);
max_x = max_x.max(xi);
max_y = max_y.max(yi);
}
}
let sx = if cols > 0 {
(max_x - min_x) / cols as f64
} else {
1.0
};
let sy = if rows > 0 {
(max_y - min_y) / rows as f64
} else {
1.0
};
let mut data = vec![f64::NAN; rows * cols];
for r in 0..rows {
let py = min_y + (r as f64 + 0.5) * sy;
for c in 0..cols {
let px = min_x + (c as f64 + 0.5) * sx;
if let Some(v) = interpolate(&tri, x, y, values, px, py) {
data[r * cols + c] = v;
}
}
}
Some(GridImage {
data,
shape: (rows, cols),
origin: (min_x, min_y),
scale: (sx, sy),
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RegularGrid {
pub shape: (usize, usize),
pub order: GridMajorOrder,
}
fn get_z_line_length(array: &[f64]) -> usize {
if array.len() < 2 {
return 0;
}
let sign: Vec<i8> = array
.windows(2)
.map(|w| {
let d = w[1] - w[0];
if d > 0.0 {
1
} else if d < 0.0 {
-1
} else {
0
}
})
.collect();
if sign.is_empty() || sign[0] == 0 {
return 0;
}
let first = sign[0];
let beginnings: Vec<usize> = sign
.iter()
.enumerate()
.filter(|&(_, &s)| s == -first)
.map(|(i, _)| i + 1)
.collect();
if beginnings.is_empty() {
return 0;
}
let length = beginnings[0];
let uniform = beginnings.windows(2).all(|w| w[1] - w[0] == length);
if uniform { length } else { 0 }
}
fn guess_z_grid_shape(x: &[f64], y: &[f64]) -> Option<RegularGrid> {
let n = x.len();
let width = get_z_line_length(x);
if width != 0 {
let height = n.div_ceil(width);
return Some(RegularGrid {
shape: (height, width),
order: GridMajorOrder::Row,
});
}
let height = get_z_line_length(y);
if height != 0 {
let width = n.div_ceil(height);
return Some(RegularGrid {
shape: (height, width),
order: GridMajorOrder::Column,
});
}
None
}
fn is_monotonic(array: &[f64]) -> i8 {
if array.len() < 2 {
return 1;
}
let diffs: Vec<f64> = array.windows(2).map(|w| w[1] - w[0]).collect();
if diffs.iter().all(|&d| d >= 0.0) {
1
} else if diffs.iter().all(|&d| d <= 0.0) {
-1
} else {
0
}
}
#[must_use]
pub fn detect_regular_grid(x: &[f64], y: &[f64]) -> Option<RegularGrid> {
assert_eq!(x.len(), y.len(), "x and y must have the same length");
if x.is_empty() {
return None;
}
if let Some(grid) = guess_z_grid_shape(x, y) {
return Some(grid);
}
let y_monotonic = is_monotonic(y) != 0;
let x_monotonic = is_monotonic(x) != 0;
if x_monotonic || y_monotonic {
let (x_min, x_max) = min_max(x);
let (y_min, y_max) = min_max(y);
let shape = if !y_monotonic || (x_max - x_min) >= (y_max - y_min) {
(1, x.len())
} else {
(y.len(), 1)
};
Some(RegularGrid {
shape,
order: GridMajorOrder::Row, })
} else {
None
}
}
fn min_max(array: &[f64]) -> (f64, f64) {
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
for &v in array {
if v.is_finite() {
min = min.min(v);
max = max.max(v);
}
}
if min > max {
(f64::NAN, f64::NAN)
} else {
(min, max)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct BinnedStatistic {
pub mean: Vec<f64>,
pub count: Vec<u64>,
pub sum: Vec<f64>,
pub shape: (usize, usize),
pub origin: (f64, f64),
pub scale: (f64, f64),
}
impl BinnedStatistic {
#[must_use]
pub fn select(&self, func: BinnedStatisticFunction) -> Vec<f64> {
match func {
BinnedStatisticFunction::Mean => self.mean.clone(),
BinnedStatisticFunction::Count => self.count.iter().map(|&c| c as f64).collect(),
BinnedStatisticFunction::Sum => self.sum.clone(),
}
}
}
#[must_use]
pub fn binned_statistic(
x: &[f64],
y: &[f64],
values: &[f64],
rows: usize,
cols: usize,
) -> Option<BinnedStatistic> {
assert_eq!(x.len(), y.len(), "x and y must have the same length");
assert_eq!(
values.len(),
x.len(),
"values must have one entry per point"
);
if rows == 0 || cols == 0 {
return None;
}
let (x_min, x_max) = min_max(x);
let (y_min, y_max) = min_max(y);
if !x_min.is_finite() || !y_min.is_finite() {
return None; }
let sx = {
let span = x_max - x_min;
if span > 0.0 { span / cols as f64 } else { 1.0 }
};
let sy = {
let span = y_max - y_min;
if span > 0.0 { span / rows as f64 } else { 1.0 }
};
let mut count = vec![0u64; rows * cols];
let mut sum = vec![0.0f64; rows * cols];
for ((&xi, &yi), &vi) in x.iter().zip(y).zip(values) {
if !xi.is_finite() || !yi.is_finite() || !vi.is_finite() {
continue;
}
let mut c = ((xi - x_min) / sx).floor() as isize;
let mut r = ((yi - y_min) / sy).floor() as isize;
if c >= cols as isize {
c = cols as isize - 1;
}
if r >= rows as isize {
r = rows as isize - 1;
}
if c < 0 || r < 0 {
continue; }
let idx = r as usize * cols + c as usize;
count[idx] += 1;
sum[idx] += vi;
}
let mean: Vec<f64> = count
.iter()
.zip(&sum)
.map(|(&c, &s)| if c == 0 { f64::NAN } else { s / c as f64 })
.collect();
Some(BinnedStatistic {
mean,
count,
sum,
shape: (rows, cols),
origin: (x_min, y_min),
scale: (sx, sy),
})
}
#[derive(Clone, Debug, PartialEq)]
pub struct PointsViz {
pub x: Vec<f64>,
pub y: Vec<f64>,
pub values: Vec<f64>,
pub colors: Vec<Color32>,
pub alpha: Option<Vec<f64>>,
}
impl PointsViz {
#[must_use]
pub fn new(x: Vec<f64>, y: Vec<f64>, values: Vec<f64>, colors: Vec<Color32>) -> Self {
assert_eq!(x.len(), y.len(), "x and y must have the same length");
assert_eq!(
values.len(),
x.len(),
"values must have one entry per point"
);
assert_eq!(
colors.len(),
x.len(),
"colors must have one entry per point"
);
Self {
x,
y,
values,
colors,
alpha: None,
}
}
#[must_use]
pub fn with_alpha(mut self, alpha: Vec<f64>) -> Self {
assert_eq!(
alpha.len(),
self.x.len(),
"alpha must have one entry per point"
);
self.alpha = Some(alpha.into_iter().map(|a| a.clamp(0.0, 1.0)).collect());
self
}
#[must_use]
pub fn len(&self) -> usize {
self.x.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.x.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn delaunay_three_points_one_triangle() {
let tri = delaunay(&[0.0, 1.0, 0.0], &[0.0, 0.0, 1.0]);
assert_eq!(tri.len(), 1);
let mut refs = tri.triangles[0];
refs.sort_unstable();
assert_eq!(refs, [0, 1, 2]);
}
#[test]
fn delaunay_four_convex_points_two_triangles() {
let x = [0.0, 1.0, 1.0, 0.0];
let y = [0.0, 0.0, 1.0, 1.0];
let tri = delaunay(&x, &y);
assert_eq!(tri.len(), 2);
let mut seen = [false; 4];
for t in &tri.triangles {
for &v in t {
seen[v] = true;
}
}
assert!(seen.iter().all(|&s| s), "every input point referenced");
}
#[test]
fn delaunay_collinear_points_empty() {
let tri = delaunay(&[0.0, 1.0, 2.0, 3.0], &[0.0, 1.0, 2.0, 3.0]);
assert!(tri.is_empty(), "collinear input -> empty triangulation");
}
#[test]
fn delaunay_fewer_than_three_points_empty() {
assert!(delaunay(&[0.0, 1.0], &[0.0, 1.0]).is_empty());
assert!(delaunay(&[], &[]).is_empty());
}
#[test]
fn delaunay_ignores_non_finite_points() {
let x = [0.0, 1.0, 0.0, f64::NAN];
let y = [0.0, 0.0, 1.0, 5.0];
let tri = delaunay(&x, &y);
assert_eq!(tri.len(), 1);
for t in &tri.triangles {
assert!(
t.iter().all(|&v| v < 3),
"no triangle references the NaN point"
);
}
}
#[test]
fn delaunay_property_no_point_inside_circumcircle() {
let x = [0.0, 1.0, 2.0, 0.5, 1.5, 1.0];
let y = [0.0, 0.2, 0.0, 1.0, 1.1, 2.0];
let tri = delaunay(&x, &y);
assert!(!tri.is_empty());
for t in &tri.triangles {
let a = [x[t[0]], y[t[0]]];
let b = [x[t[1]], y[t[1]]];
let c = [x[t[2]], y[t[2]]];
let (a, b, c) = if orient2d(a, b, c) < 0.0 {
(a, c, b)
} else {
(a, b, c)
};
for i in 0..x.len() {
if i == t[0] || i == t[1] || i == t[2] {
continue;
}
let p = [x[i], y[i]];
assert!(
!in_circumcircle(a, b, c, p),
"point {i} inside circumcircle of triangle {t:?}"
);
}
}
}
#[test]
fn solid_triangles_colors_each_vertex() {
let x = [0.0, 1.0, 0.0];
let y = [0.0, 0.0, 1.0];
let colors = [Color32::RED, Color32::GREEN, Color32::BLUE];
let t = solid_triangles(&x, &y, &colors).expect("triangulable");
assert_eq!(t.indices.len(), 1);
assert_eq!(t.colors, colors);
assert_eq!(t.x, x);
assert_eq!(t.y, y);
}
#[test]
fn solid_triangles_none_for_collinear() {
let x = [0.0, 1.0, 2.0];
let y = [0.0, 1.0, 2.0];
let colors = [Color32::RED; 3];
assert!(solid_triangles(&x, &y, &colors).is_none());
}
#[test]
fn interpolate_at_vertices_returns_vertex_value() {
let x = [0.0, 1.0, 0.0];
let y = [0.0, 0.0, 1.0];
let values = [10.0, 20.0, 30.0];
let tri = delaunay(&x, &y);
for i in 0..3 {
let v = interpolate(&tri, &x, &y, &values, x[i], y[i]).expect("inside");
assert!((v - values[i]).abs() < 1e-9, "vertex {i}: {v}");
}
}
#[test]
fn interpolate_at_centroid_returns_mean() {
let x = [0.0, 1.0, 0.0];
let y = [0.0, 0.0, 1.0];
let values = [10.0, 20.0, 30.0];
let tri = delaunay(&x, &y);
let cx = (x[0] + x[1] + x[2]) / 3.0;
let cy = (y[0] + y[1] + y[2]) / 3.0;
let v = interpolate(&tri, &x, &y, &values, cx, cy).expect("inside");
let mean = (10.0 + 20.0 + 30.0) / 3.0;
assert!((v - mean).abs() < 1e-9, "centroid value {v} != mean {mean}");
}
#[test]
fn interpolate_outside_returns_none() {
let x = [0.0, 1.0, 0.0];
let y = [0.0, 0.0, 1.0];
let values = [10.0, 20.0, 30.0];
let tri = delaunay(&x, &y);
assert!(interpolate(&tri, &x, &y, &values, 5.0, 5.0).is_none());
assert!(interpolate(&tri, &x, &y, &values, -1.0, -1.0).is_none());
}
#[test]
fn irregular_grid_image_interpolates_inside_nan_outside() {
let x = [0.0, 4.0, 0.0];
let y = [0.0, 0.0, 4.0];
let values = [0.0, 4.0, 0.0];
let img = irregular_grid_image(&x, &y, &values, 4, 4).expect("triangulable");
assert_eq!(img.shape, (4, 4));
let v = img.get(0, 0).unwrap();
assert!((v - 0.5).abs() < 1e-9, "interior value {v}");
let outside = img.get(3, 3).unwrap();
assert!(
outside.is_nan(),
"exterior pixel should be NaN, got {outside}"
);
assert_eq!(img.origin, (0.0, 0.0));
assert_eq!(img.scale, (1.0, 1.0));
}
#[test]
fn irregular_grid_image_none_for_degenerate() {
assert!(
irregular_grid_image(&[0.0, 1.0, 2.0], &[0.0, 1.0, 2.0], &[1.0, 2.0, 3.0], 4, 4)
.is_none()
);
assert!(
irregular_grid_image(&[0.0, 1.0, 0.0], &[0.0, 0.0, 1.0], &[1.0, 2.0, 3.0], 0, 4)
.is_none()
);
}
fn grid_3x4_row_major() -> (Vec<f64>, Vec<f64>) {
let (rows, cols) = (3usize, 4usize);
let mut x = Vec::new();
let mut y = Vec::new();
for r in 0..rows {
for c in 0..cols {
x.push(c as f64);
y.push(r as f64);
}
}
(x, y)
}
#[test]
fn detect_regular_grid_row_major_3x4() {
let (x, y) = grid_3x4_row_major();
let grid = detect_regular_grid(&x, &y).expect("grid detected");
assert_eq!(grid.shape, (3, 4));
assert_eq!(grid.order, GridMajorOrder::Row);
}
#[test]
fn detect_regular_grid_column_major_3x4() {
let (rows, cols) = (3usize, 4usize);
let mut x = Vec::new();
let mut y = Vec::new();
for c in 0..cols {
for r in 0..rows {
x.push(c as f64);
y.push(r as f64);
}
}
let grid = detect_regular_grid(&x, &y).expect("grid detected");
assert_eq!(grid.shape, (3, 4));
assert_eq!(grid.order, GridMajorOrder::Column);
}
#[test]
fn detect_regular_grid_rejects_random_scatter() {
let x = [0.0, 1.0, 0.5, 2.0, 3.0, 1.0];
let y = [2.0, 0.0, 1.0, 3.0, 0.5, 4.0];
assert!(detect_regular_grid(&x, &y).is_none());
}
#[test]
fn detect_regular_grid_single_line_along_x() {
let x = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
let y = [2.0, 0.0, 1.0, 3.0, 0.5, 4.0];
let grid = detect_regular_grid(&x, &y).expect("line detected");
assert_eq!(grid.shape, (1, 6));
}
#[test]
fn binned_statistic_2x2_mean_count_sum() {
let x = [0.0, 0.5, 2.0];
let y = [0.0, 0.5, 2.0];
let v = [10.0, 30.0, 7.0];
let bs = binned_statistic(&x, &y, &v, 2, 2).expect("binned");
assert_eq!(bs.shape, (2, 2));
assert_eq!(bs.count[0], 2);
assert!((bs.sum[0] - 40.0).abs() < 1e-12);
assert!((bs.mean[0] - 20.0).abs() < 1e-12);
assert_eq!(bs.count[1], 0);
assert_eq!(bs.sum[1], 0.0);
assert!(bs.mean[1].is_nan(), "empty bin mean is NaN");
assert_eq!(bs.count[2], 0);
assert_eq!(bs.count[3], 1);
assert!((bs.sum[3] - 7.0).abs() < 1e-12);
assert!((bs.mean[3] - 7.0).abs() < 1e-12);
assert_eq!(bs.origin, (0.0, 0.0));
assert_eq!(bs.scale, (1.0, 1.0));
}
#[test]
fn binned_statistic_max_point_clamped_into_last_bin() {
let x = [0.0, 2.0];
let y = [0.0, 2.0];
let v = [1.0, 2.0];
let bs = binned_statistic(&x, &y, &v, 2, 2).expect("binned");
assert_eq!(bs.count[3], 1);
assert!((bs.sum[3] - 2.0).abs() < 1e-12);
assert_eq!(bs.count[0], 1);
}
#[test]
fn binned_statistic_select_returns_chosen_grid() {
let x = [0.2, 1.5];
let y = [0.2, 1.5];
let v = [10.0, 7.0];
let bs = binned_statistic(&x, &y, &v, 2, 2).expect("binned");
let counts = bs.select(BinnedStatisticFunction::Count);
assert_eq!(counts, vec![1.0, 0.0, 0.0, 1.0]);
let sums = bs.select(BinnedStatisticFunction::Sum);
assert_eq!(sums, vec![10.0, 0.0, 0.0, 7.0]);
let means = bs.select(BinnedStatisticFunction::Mean);
assert!((means[0] - 10.0).abs() < 1e-12);
assert!(means[1].is_nan());
}
#[test]
fn binned_statistic_none_for_empty_or_zero_shape() {
assert!(binned_statistic(&[], &[], &[], 2, 2).is_none());
assert!(binned_statistic(&[0.0], &[0.0], &[1.0], 0, 2).is_none());
}
#[test]
fn binned_statistic_skips_non_finite_value() {
let x = [0.2, 0.4];
let y = [0.2, 0.4];
let v = [10.0, f64::NAN];
let bs = binned_statistic(&x, &y, &v, 2, 2).expect("binned");
assert_eq!(bs.count[0], 1);
assert!((bs.sum[0] - 10.0).abs() < 1e-12);
}
#[test]
fn points_viz_default_no_alpha() {
let p = PointsViz::new(
vec![0.0, 1.0],
vec![0.0, 1.0],
vec![5.0, 6.0],
vec![Color32::RED, Color32::BLUE],
);
assert_eq!(p.len(), 2);
assert!(p.alpha.is_none());
}
#[test]
fn points_viz_with_alpha_clamps() {
let p = PointsViz::new(
vec![0.0, 1.0],
vec![0.0, 1.0],
vec![5.0, 6.0],
vec![Color32::RED, Color32::BLUE],
)
.with_alpha(vec![-0.5, 2.0]);
assert_eq!(p.alpha, Some(vec![0.0, 1.0]));
}
#[test]
#[should_panic(expected = "alpha must have one entry per point")]
fn points_viz_alpha_length_mismatch_panics() {
let _ = PointsViz::new(
vec![0.0, 1.0],
vec![0.0, 1.0],
vec![5.0, 6.0],
vec![Color32::RED, Color32::BLUE],
)
.with_alpha(vec![0.5]);
}
}