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 quadrilateral_grid_coords(points: &[[f64; 2]], dim0: usize, dim1: usize) -> Vec<[f64; 2]> {
debug_assert!(dim0 >= 2 && dim1 >= 2);
debug_assert_eq!(points.len(), dim0 * dim1);
let gw = dim1 + 1; let mut grid = vec![[0.0_f64; 2]; (dim0 + 1) * gw];
let p = |r: usize, c: usize| points[r * dim1 + c];
let inner = |r: usize, c: usize| -> [f64; 2] {
let (a, b, d, e) = (p(r, c), p(r, c + 1), p(r + 1, c), p(r + 1, c + 1));
[
(a[0] + b[0] + d[0] + e[0]) / 4.0,
(a[1] + b[1] + d[1] + e[1]) / 4.0,
]
};
for r in 0..dim0 - 1 {
for c in 0..dim1 - 1 {
grid[(r + 1) * gw + (c + 1)] = inner(r, c);
}
}
for r in 0..dim0 - 1 {
let il = inner(r, 0);
grid[(r + 1) * gw] = [p(r, 0)[0] + p(r + 1, 0)[0] - il[0], il[1]];
let ir = inner(r, dim1 - 2);
grid[(r + 1) * gw + dim1] = [p(r, dim1 - 1)[0] + p(r + 1, dim1 - 1)[0] - ir[0], ir[1]];
}
for c in 0..dim1 - 1 {
let it = inner(0, c);
grid[c + 1] = [it[0], p(0, c)[1] + p(0, c + 1)[1] - it[1]];
let ib = inner(dim0 - 2, c);
grid[dim0 * gw + (c + 1)] = [ib[0], p(dim0 - 1, c)[1] + p(dim0 - 1, c + 1)[1] - ib[1]];
}
let corner = |pr: usize, pc: usize, ir: usize, ic: usize| -> [f64; 2] {
let (pp, ii) = (p(pr, pc), inner(ir, ic));
[2.0 * pp[0] - ii[0], 2.0 * pp[1] - ii[1]]
};
grid[0] = corner(0, 0, 0, 0);
grid[dim1] = corner(0, dim1 - 1, 0, dim1 - 2);
grid[dim0 * gw + dim1] = corner(dim0 - 1, dim1 - 1, dim0 - 2, dim1 - 2);
grid[dim0 * gw] = corner(dim0 - 1, 0, dim0 - 2, 0);
grid
}
fn quadrilateral_grid_as_triangles(
points: &[[f64; 2]],
dim0: usize,
dim1: usize,
) -> (Vec<[f64; 2]>, Vec<[u32; 3]>) {
let nbpoints = dim0 * dim1;
let grid = quadrilateral_grid_coords(points, dim0, dim1);
let gw = dim1 + 1;
let mut coords = vec![[0.0_f64; 2]; 4 * nbpoints];
for r in 0..dim0 {
for c in 0..dim1 {
let k = r * dim1 + c;
coords[4 * k] = grid[r * gw + c];
coords[4 * k + 1] = grid[(r + 1) * gw + c];
coords[4 * k + 2] = grid[r * gw + (c + 1)];
coords[4 * k + 3] = grid[(r + 1) * gw + (c + 1)];
}
}
let mut indices = Vec::with_capacity(2 * nbpoints);
for k in 0..nbpoints {
let b = u32::try_from(4 * k).expect("vertex index fits in u32");
indices.push([b, b + 1, b + 2]);
indices.push([b + 1, b + 2, b + 3]);
}
(coords, indices)
}
fn arrange_irregular_grid_points(
x: &[f64],
y: &[f64],
) -> Option<(Vec<[f64; 2]>, usize, usize, bool)> {
let nbpoints = x.len();
if nbpoints < 2 {
return None;
}
let grid = detect_regular_grid(x, y)?;
let (mut s0, mut s1) = grid.shape; let order = grid.order;
if nbpoints != s0 * s1 {
match order {
GridMajorOrder::Row => s0 = nbpoints.div_ceil(s1),
GridMajorOrder::Column => s1 = nbpoints.div_ceil(s0),
}
}
if s0 < 2 || s1 < 2 {
let row_order = s0 == 1;
let line: Vec<[f64; 2]> = (0..nbpoints)
.map(|i| {
if row_order {
[x[i], y[i]]
} else {
[y[i], x[i]]
}
})
.collect();
let mut points = Vec::with_capacity(2 * nbpoints);
points.extend_from_slice(&line);
for i in 0..nbpoints {
let (dx, dy) = if i + 1 < nbpoints {
(line[i + 1][0] - line[i][0], line[i + 1][1] - line[i][1])
} else {
(line[i][0] - line[i - 1][0], line[i][1] - line[i - 1][1])
};
points.push([line[i][0] + dy, line[i][1] - dx]);
}
return Some((points, 2, nbpoints, !row_order));
}
let total = s0 * s1;
let mut points = vec![[0.0_f64; 2]; total];
match order {
GridMajorOrder::Row => {
for i in 0..nbpoints {
points[i] = [x[i], y[i]];
}
if nbpoints != total {
let index = (nbpoints / s1) * s1; let pad = total - nbpoints;
let last_y = y[nbpoints - 1];
for j in 0..pad {
points[nbpoints + j] = [x[index - pad + j], last_y];
}
}
Some((points, s0, s1, false))
}
GridMajorOrder::Column => {
for i in 0..nbpoints {
points[i] = [y[i], x[i]];
}
if nbpoints != total {
let index = (nbpoints / s0) * s0; let pad = total - nbpoints;
let last_x = x[nbpoints - 1];
for j in 0..pad {
points[nbpoints + j] = [y[index - pad + j], last_x];
}
}
Some((points, s1, s0, true))
}
}
}
#[must_use]
pub fn irregular_grid_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 point"
);
let nbpoints = x.len();
let (points, dim0, dim1, swap_xy) = arrange_irregular_grid_points(x, y)?;
let (mut coords, mut indices) = quadrilateral_grid_as_triangles(&points, dim0, dim1);
coords.truncate(4 * nbpoints);
indices.truncate(2 * nbpoints);
let (vx, vy): (Vec<f64>, Vec<f64>) = if swap_xy {
coords.iter().map(|c| (c[1], c[0])).unzip()
} else {
coords.iter().map(|c| (c[0], c[1])).unzip()
};
let mut vcolors = Vec::with_capacity(4 * nbpoints);
for &c in colors {
vcolors.extend_from_slice(&[c; 4]);
}
Some(Triangles::new(vx, vy, indices, vcolors))
}
#[must_use]
pub fn irregular_grid_pick(mesh: &Triangles, px: f64, py: f64) -> Option<usize> {
for (t, tri) in mesh.indices.iter().enumerate() {
let v = |i: usize| [mesh.x[i], mesh.y[i]];
let (a, b, c) = (v(tri[0] as usize), v(tri[1] as usize), v(tri[2] as usize));
if barycentric(a, b, c, [px, py]).is_some() {
return Some(t / 2);
}
}
None
}
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, Default)]
pub struct ScatterLineProfile {
pub points: Vec<[f64; 2]>,
pub values: Vec<Option<f64>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ProfileAxis {
X,
Y,
}
impl ScatterLineProfile {
#[must_use]
pub fn dominant_axis_curve(&self) -> (Vec<f64>, Vec<f64>, ProfileAxis) {
let (Some(&[x0, y0]), Some(&[xn, yn])) = (self.points.first(), self.points.last()) else {
return (Vec::new(), Vec::new(), ProfileAxis::X);
};
let axis = if (xn - x0).abs() > (yn - y0).abs() {
ProfileAxis::X
} else {
ProfileAxis::Y
};
let coords = self
.points
.iter()
.map(|&[x, y]| match axis {
ProfileAxis::X => x,
ProfileAxis::Y => y,
})
.collect();
let value = self.values.iter().map(|v| v.unwrap_or(f64::NAN)).collect();
(coords, value, axis)
}
}
pub fn scatter_line_profile(
x: &[f64],
y: &[f64],
values: &[f64],
start: (f64, f64),
end: (f64, f64),
n_points: usize,
) -> ScatterLineProfile {
let tri = delaunay(x, y);
let mut points = Vec::with_capacity(n_points);
let mut profile = Vec::with_capacity(n_points);
for i in 0..n_points {
let t = if n_points <= 1 {
0.0
} else {
i as f64 / (n_points - 1) as f64
};
let px = start.0 + (end.0 - start.0) * t;
let py = start.1 + (end.1 - start.1) * t;
points.push([px, py]);
profile.push(interpolate(&tri, x, y, values, px, py));
}
ScatterLineProfile {
points,
values: profile,
}
}
#[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 cell(&self, x: f64, y: f64) -> Option<(usize, usize)> {
grid_cell(self.shape, self.origin, self.scale, x, y)
}
}
fn grid_cell(
shape: (usize, usize),
origin: (f64, f64),
scale: (f64, f64),
x: f64,
y: f64,
) -> Option<(usize, usize)> {
let (sx, sy) = scale;
if sx == 0.0 || sy == 0.0 {
return None;
}
let cf = (x - origin.0) / sx;
let rf = (y - origin.1) / sy;
if !cf.is_finite() || !rf.is_finite() || cf < 0.0 || rf < 0.0 {
return None;
}
let col = cf.floor() as usize;
let row = rf.floor() as usize;
if row < shape.0 && col < shape.1 {
Some((row, col))
} else {
None
}
}
#[must_use]
pub fn regular_grid_pick(
image: &GridImage,
order: GridMajorOrder,
point_count: usize,
x: f64,
y: f64,
) -> Option<usize> {
let (row, col) = image.cell(x, y)?;
let (rows, cols) = image.shape;
let index = match order {
GridMajorOrder::Row => row * cols + col,
GridMajorOrder::Column => row + col * rows,
};
if index < point_count {
Some(index)
} 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 pick(&self, x: &[f64], y: &[f64], px: f64, py: f64) -> Option<Vec<usize>> {
let (row, col) = grid_cell(self.shape, self.origin, self.scale, px, py)?;
let (ox, oy) = self.origin;
let (sx, sy) = self.scale;
let x_lo = ox + sx * col as f64;
let x_hi = ox + sx * (col + 1) as f64;
let y_lo = oy + sy * row as f64;
let y_hi = oy + sy * (row + 1) as f64;
let indices: Vec<usize> = x
.iter()
.zip(y.iter())
.enumerate()
.filter(|&(_, (&xi, &yi))| xi >= x_lo && xi < x_hi && yi >= y_lo && yi < y_hi)
.map(|(i, _)| i)
.collect();
if indices.is_empty() {
None
} else {
Some(indices)
}
}
}
#[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 scatter_line_profile_interpolates_affine_field_along_line() {
let x = [0.0, 2.0, 0.0];
let y = [0.0, 0.0, 2.0];
let values = [0.0, 2.0, 4.0];
let prof = scatter_line_profile(&x, &y, &values, (0.0, 0.0), (1.0, 1.0), 3);
assert_eq!(prof.points, vec![[0.0, 0.0], [0.5, 0.5], [1.0, 1.0]]);
let got: Vec<f64> = prof
.values
.iter()
.map(|v| v.expect("inside hull"))
.collect();
for (g, want) in got.iter().zip([0.0, 1.5, 3.0]) {
assert!((g - want).abs() < 1e-9, "got {g}, want {want}");
}
}
#[test]
fn scatter_line_profile_outside_hull_is_none() {
let x = [0.0, 2.0, 0.0];
let y = [0.0, 0.0, 2.0];
let values = [0.0, 2.0, 4.0];
let prof = scatter_line_profile(&x, &y, &values, (5.0, 5.0), (9.0, 9.0), 4);
assert!(prof.values.iter().all(Option::is_none), "{:?}", prof.values);
}
#[test]
fn scatter_line_profile_too_few_points_yields_no_values() {
let x = [0.0, 1.0];
let y = [0.0, 1.0];
let values = [1.0, 2.0];
let prof = scatter_line_profile(&x, &y, &values, (0.0, 0.0), (1.0, 1.0), 2);
assert_eq!(prof.points.len(), 2);
assert!(prof.values.iter().all(Option::is_none));
}
#[test]
fn dominant_axis_curve_picks_the_wider_span_with_nan_gaps() {
let prof = ScatterLineProfile {
points: vec![[0.0, 0.0], [3.0, 4.0], [6.0, 8.0]],
values: vec![Some(1.0), None, Some(2.0)],
};
let (coords, value, axis) = prof.dominant_axis_curve();
assert_eq!(axis, ProfileAxis::Y);
assert_eq!(coords, vec![0.0, 4.0, 8.0]);
assert_eq!(value[0], 1.0);
assert!(value[1].is_nan(), "out-of-hull sample maps to NaN");
assert_eq!(value[2], 2.0);
}
#[test]
fn dominant_axis_curve_x_wider_plots_against_x() {
let prof = ScatterLineProfile {
points: vec![[0.0, 0.0], [10.0, 2.0]],
values: vec![Some(1.0), Some(3.0)],
};
let (coords, _value, axis) = prof.dominant_axis_curve();
assert_eq!(axis, ProfileAxis::X);
assert_eq!(coords, vec![0.0, 10.0]);
}
#[test]
fn dominant_axis_curve_equal_span_ties_to_y() {
let prof = ScatterLineProfile {
points: vec![[0.0, 0.0], [5.0, 5.0]],
values: vec![Some(1.0), Some(2.0)],
};
let (coords, _value, axis) = prof.dominant_axis_curve();
assert_eq!(axis, ProfileAxis::Y);
assert_eq!(coords, vec![0.0, 5.0]);
}
#[test]
fn dominant_axis_curve_empty_profile_is_empty() {
let prof = ScatterLineProfile::default();
let (coords, value, axis) = prof.dominant_axis_curve();
assert!(coords.is_empty() && value.is_empty());
assert_eq!(axis, ProfileAxis::X);
}
#[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 regular_grid_pick_row_major_maps_cell_to_index() {
let image = GridImage {
data: vec![0.0; 6],
shape: (2, 3),
origin: (0.0, 0.0),
scale: (1.0, 1.0),
};
assert_eq!(
regular_grid_pick(&image, GridMajorOrder::Row, 6, 2.5, 0.5),
Some(2)
);
assert_eq!(
regular_grid_pick(&image, GridMajorOrder::Row, 6, 0.5, 1.5),
Some(3)
);
assert_eq!(
regular_grid_pick(&image, GridMajorOrder::Row, 6, 3.5, 0.5),
None
);
}
#[test]
fn regular_grid_pick_column_major_inverts_transposed_placement() {
let image = GridImage {
data: vec![0.0; 6],
shape: (2, 3),
origin: (0.0, 0.0),
scale: (1.0, 1.0),
};
assert_eq!(
regular_grid_pick(&image, GridMajorOrder::Column, 6, 2.5, 1.5),
Some(5)
);
assert_eq!(
regular_grid_pick(&image, GridMajorOrder::Column, 6, 1.5, 0.5),
Some(2)
);
}
#[test]
fn regular_grid_pick_none_past_last_point() {
let image = GridImage {
data: vec![0.0; 6],
shape: (2, 3),
origin: (0.0, 0.0),
scale: (1.0, 1.0),
};
assert_eq!(
regular_grid_pick(&image, GridMajorOrder::Row, 5, 2.5, 1.5),
None
);
assert_eq!(
regular_grid_pick(&image, GridMajorOrder::Row, 5, 1.5, 1.5),
Some(4)
);
}
#[test]
fn regular_grid_pick_handles_negative_scale() {
let image = GridImage {
data: vec![0.0; 3],
shape: (1, 3),
origin: (11.0, -0.5),
scale: (-2.0, 1.0),
};
assert_eq!(
regular_grid_pick(&image, GridMajorOrder::Row, 3, 10.0, 0.0),
Some(0)
);
assert_eq!(
regular_grid_pick(&image, GridMajorOrder::Row, 3, 6.0, 0.0),
Some(2)
);
assert_eq!(
regular_grid_pick(&image, GridMajorOrder::Row, 3, 12.0, 0.0),
None
);
}
#[test]
fn binned_statistic_pick_returns_points_in_bin() {
let x = [0.0, 0.5, 1.5, 2.0];
let y = [0.0, 0.5, 1.5, 2.0];
let v = [10.0, 30.0, 5.0, 7.0];
let bs = binned_statistic(&x, &y, &v, 2, 2).expect("binned");
assert_eq!(bs.origin, (0.0, 0.0));
assert_eq!(bs.scale, (1.0, 1.0));
assert_eq!(bs.pick(&x, &y, 0.5, 0.5), Some(vec![0, 1]));
assert_eq!(bs.pick(&x, &y, 1.5, 1.5), Some(vec![2]));
}
#[test]
fn binned_statistic_pick_none_off_grid_or_empty_bin() {
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.pick(&x, &y, 1.5, 0.5), None);
assert_eq!(bs.pick(&x, &y, 2.5, 0.5), None);
assert_eq!(bs.pick(&x, &y, -0.5, 0.5), None);
}
#[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]);
}
#[test]
fn quadrilateral_grid_coords_unit_2x2_offsets_by_half() {
let points = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
let grid = quadrilateral_grid_coords(&points, 2, 2);
let expect = [
[-0.5, -0.5],
[0.5, -0.5],
[1.5, -0.5],
[-0.5, 0.5],
[0.5, 0.5],
[1.5, 0.5],
[-0.5, 1.5],
[0.5, 1.5],
[1.5, 1.5],
];
assert_eq!(grid.len(), 9);
for (got, want) in grid.iter().zip(&expect) {
assert!((got[0] - want[0]).abs() < 1e-12, "x: {got:?} vs {want:?}");
assert!((got[1] - want[1]).abs() < 1e-12, "y: {got:?} vs {want:?}");
}
}
#[test]
fn irregular_grid_triangles_builds_one_cell_per_point() {
let x = [0.0, 1.0, 0.0, 1.0];
let y = [0.0, 0.0, 1.0, 1.0];
let colors = [Color32::RED, Color32::GREEN, Color32::BLUE, Color32::WHITE];
let mesh = irregular_grid_triangles(&x, &y, &colors).expect("buildable grid");
assert_eq!(mesh.x.len(), 16);
assert_eq!(mesh.colors.len(), 16);
assert_eq!(mesh.indices.len(), 8);
for (k, &c) in colors.iter().enumerate() {
for v in 0..4 {
assert_eq!(mesh.colors[4 * k + v], c, "point {k} vertex {v}");
}
}
}
#[test]
fn irregular_grid_pick_maps_cursor_to_owning_cell() {
let x = [0.0, 1.0, 0.0, 1.0];
let y = [0.0, 0.0, 1.0, 1.0];
let mesh = irregular_grid_triangles(&x, &y, &[Color32::RED; 4]).expect("buildable grid");
assert_eq!(irregular_grid_pick(&mesh, 0.0, 0.0), Some(0));
assert_eq!(irregular_grid_pick(&mesh, 1.0, 0.0), Some(1));
assert_eq!(irregular_grid_pick(&mesh, 0.0, 1.0), Some(2));
assert_eq!(irregular_grid_pick(&mesh, 1.0, 1.0), Some(3));
assert_eq!(irregular_grid_pick(&mesh, 10.0, 10.0), None);
}
#[test]
fn irregular_grid_single_line_builds_one_cell_per_point_and_picks() {
let x = [0.0, 1.0, 2.0, 3.0];
let y = [0.0, 0.0, 0.0, 0.0];
let mesh = irregular_grid_triangles(&x, &y, &[Color32::RED; 4]).expect("buildable line");
assert_eq!(mesh.x.len(), 16, "4 points -> 16 vertices");
assert_eq!(mesh.indices.len(), 8, "4 points -> 8 triangles");
assert_eq!(irregular_grid_pick(&mesh, 1.0, 0.0), Some(1));
assert_eq!(irregular_grid_pick(&mesh, 2.0, 0.0), Some(2));
}
#[test]
fn irregular_grid_triangles_none_for_too_few_points() {
assert!(irregular_grid_triangles(&[0.0], &[0.0], &[Color32::RED]).is_none());
}
}