#[must_use]
pub fn value_to_y(value: f64, max: f64, height: f64) -> f64 {
let safe_max = if max > 0.0 { max } else { 1.0 };
let clamped = value.max(0.0);
height - (clamped / safe_max) * height
}
#[must_use]
#[allow(
dead_code,
reason = "inverse of value_to_y; kept as the named pair for Phases B/C consumers that will project crosshair-Y back to data space"
)]
pub fn y_to_value(y: f64, max: f64, height: f64) -> f64 {
let safe_max = if max > 0.0 { max } else { 1.0 };
let safe_height = if height > 0.0 { height } else { 1.0 };
(1.0 - (y / safe_height)) * safe_max
}
#[must_use]
pub fn index_to_x(index: usize, n_points: usize, width: f64) -> f64 {
if n_points <= 1 {
return 0.0;
}
#[allow(
clippy::cast_precision_loss,
reason = "indices and point counts are bounded well below 2^53"
)]
{
(index as f64 / (n_points - 1) as f64) * width
}
}
#[cfg(test)]
#[allow(clippy::float_cmp, reason = "exact-value tests with simple inputs")]
mod tests {
use super::*;
#[test]
fn value_to_y_inverts_at_endpoints() {
assert_eq!(value_to_y(0.0, 10.0, 200.0), 200.0);
assert_eq!(value_to_y(10.0, 10.0, 200.0), 0.0);
assert_eq!(value_to_y(5.0, 10.0, 200.0), 100.0);
}
#[test]
fn value_to_y_handles_zero_max() {
assert_eq!(value_to_y(0.0, 0.0, 200.0), 200.0);
}
#[test]
fn value_to_y_clamps_negative_to_zero() {
assert_eq!(value_to_y(-5.0, 10.0, 200.0), 200.0);
}
#[test]
fn y_to_value_roundtrips() {
let y = value_to_y(7.5, 10.0, 200.0);
let back = y_to_value(y, 10.0, 200.0);
assert!((back - 7.5).abs() < 1e-9);
}
#[test]
fn index_to_x_spans_zero_to_width() {
assert_eq!(index_to_x(0, 5, 100.0), 0.0);
assert_eq!(index_to_x(4, 5, 100.0), 100.0);
assert_eq!(index_to_x(2, 5, 100.0), 50.0);
}
#[test]
fn index_to_x_handles_single_point() {
assert_eq!(index_to_x(0, 1, 100.0), 0.0);
}
}