use crate::axes::AxisScale;
use std::ops::Range;
#[derive(Debug, Clone)]
pub struct CoordinateTransform {
pub data_x: Range<f64>,
pub data_y: Range<f64>,
pub screen_x: Range<f32>,
pub screen_y: Range<f32>,
pub y_inverted: bool,
}
impl CoordinateTransform {
pub fn new(
data_x: Range<f64>,
data_y: Range<f64>,
screen_x: Range<f32>,
screen_y: Range<f32>,
) -> Self {
Self {
data_x,
data_y,
screen_x,
screen_y,
y_inverted: true,
}
}
pub fn new_non_inverted(
data_x: Range<f64>,
data_y: Range<f64>,
screen_x: Range<f32>,
screen_y: Range<f32>,
) -> Self {
Self {
data_x,
data_y,
screen_x,
screen_y,
y_inverted: false,
}
}
pub fn from_plot_area(
area_x: f32,
area_y: f32,
area_width: f32,
area_height: f32,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
) -> Self {
Self::new(
x_min..x_max,
y_min..y_max,
area_x..(area_x + area_width),
area_y..(area_y + area_height),
)
}
#[inline]
pub fn data_to_screen(&self, data_x: f64, data_y: f64) -> (f32, f32) {
let x_range = self.data_x.end - self.data_x.start;
let y_range = self.data_y.end - self.data_y.start;
let normalized_x = if x_range.abs() > f64::EPSILON {
crate::axes::scale::linear_normalized_position_with_range(
data_x,
self.data_x.start,
self.data_x.end,
x_range,
)
} else {
0.5
};
let normalized_y = if y_range.abs() > f64::EPSILON {
crate::axes::scale::linear_normalized_position_with_range(
data_y,
self.data_y.start,
self.data_y.end,
y_range,
)
} else {
0.5
};
self.normalized_to_screen(normalized_x, normalized_y)
}
#[inline]
pub fn data_to_screen_scaled(
&self,
data_x: f64,
data_y: f64,
x_scale: &AxisScale,
y_scale: &AxisScale,
) -> (f32, f32) {
let normalized_x = x_scale.normalized_position(data_x, self.data_x.start, self.data_x.end);
let normalized_y = y_scale.normalized_position(data_y, self.data_y.start, self.data_y.end);
self.normalized_to_screen(normalized_x, normalized_y)
}
#[inline]
pub fn screen_to_data(&self, screen_x: f32, screen_y: f32) -> (f64, f64) {
self.screen_to_data_scaled(screen_x, screen_y, &AxisScale::Linear, &AxisScale::Linear)
}
#[inline]
pub fn screen_to_data_scaled(
&self,
screen_x: f32,
screen_y: f32,
x_scale: &AxisScale,
y_scale: &AxisScale,
) -> (f64, f64) {
let (normalized_x, normalized_y) = self.screen_to_normalized(screen_x, screen_y);
let data_x =
x_scale.inverse_normalized_position(normalized_x, self.data_x.start, self.data_x.end);
let data_y =
y_scale.inverse_normalized_position(normalized_y, self.data_y.start, self.data_y.end);
(data_x, data_y)
}
#[inline]
fn normalized_to_screen(&self, normalized_x: f64, normalized_y: f64) -> (f32, f32) {
let screen_width = self.screen_x.end - self.screen_x.start;
let screen_height = self.screen_y.end - self.screen_y.start;
let screen_x = self.screen_x.start + normalized_x as f32 * screen_width;
let screen_y = if self.y_inverted {
self.screen_y.start + (1.0 - normalized_y as f32) * screen_height
} else {
self.screen_y.start + normalized_y as f32 * screen_height
};
(screen_x, screen_y)
}
#[inline]
fn screen_to_normalized(&self, screen_x: f32, screen_y: f32) -> (f64, f64) {
let screen_width = self.screen_x.end - self.screen_x.start;
let screen_height = self.screen_y.end - self.screen_y.start;
let normalized_x = (screen_x - self.screen_x.start) / screen_width;
let normalized_y = if self.y_inverted {
1.0 - (screen_y - self.screen_y.start) / screen_height
} else {
(screen_y - self.screen_y.start) / screen_height
};
(normalized_x as f64, normalized_y as f64)
}
#[inline]
pub fn contains_data(&self, data_x: f64, data_y: f64) -> bool {
data_x >= self.data_x.start
&& data_x <= self.data_x.end
&& data_y >= self.data_y.start
&& data_y <= self.data_y.end
}
#[inline]
pub fn contains_screen(&self, screen_x: f32, screen_y: f32) -> bool {
screen_x >= self.screen_x.start
&& screen_x <= self.screen_x.end
&& screen_y >= self.screen_y.start
&& screen_y <= self.screen_y.end
}
pub fn data_center(&self) -> (f64, f64) {
(
(self.data_x.start + self.data_x.end) / 2.0,
(self.data_y.start + self.data_y.end) / 2.0,
)
}
pub fn screen_center(&self) -> (f32, f32) {
(
(self.screen_x.start + self.screen_x.end) / 2.0,
(self.screen_y.start + self.screen_y.end) / 2.0,
)
}
pub fn screen_width(&self) -> f32 {
self.screen_x.end - self.screen_x.start
}
pub fn screen_height(&self) -> f32 {
self.screen_y.end - self.screen_y.start
}
pub fn data_width(&self) -> f64 {
self.data_x.end - self.data_x.start
}
pub fn data_height(&self) -> f64 {
self.data_y.end - self.data_y.start
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_data_to_screen_basic() {
let transform = CoordinateTransform::new(0.0..100.0, 0.0..100.0, 0.0..1000.0, 0.0..500.0);
let (x, y) = transform.data_to_screen(0.0, 0.0);
assert!((x - 0.0).abs() < f32::EPSILON);
assert!((y - 500.0).abs() < f32::EPSILON);
let (x, y) = transform.data_to_screen(100.0, 100.0);
assert!((x - 1000.0).abs() < f32::EPSILON);
assert!((y - 0.0).abs() < f32::EPSILON);
let (x, y) = transform.data_to_screen(50.0, 50.0);
assert!((x - 500.0).abs() < f32::EPSILON);
assert!((y - 250.0).abs() < f32::EPSILON);
}
#[test]
fn test_screen_to_data_basic() {
let transform = CoordinateTransform::new(0.0..100.0, 0.0..100.0, 0.0..1000.0, 0.0..500.0);
let (x, y) = transform.screen_to_data(0.0, 0.0);
assert!((x - 0.0).abs() < f64::EPSILON);
assert!((y - 100.0).abs() < f64::EPSILON);
let (x, y) = transform.screen_to_data(1000.0, 500.0);
assert!((x - 100.0).abs() < f64::EPSILON);
assert!((y - 0.0).abs() < f64::EPSILON); }
#[test]
fn test_roundtrip() {
let transform =
CoordinateTransform::new(-50.0..150.0, -10.0..90.0, 100.0..900.0, 50.0..550.0);
let test_points = [(0.0, 0.0), (100.0, 50.0), (-25.0, 45.0), (75.0, -5.0)];
let tolerance = 1e-4;
for (data_x, data_y) in test_points {
let (screen_x, screen_y) = transform.data_to_screen(data_x, data_y);
let (recovered_x, recovered_y) = transform.screen_to_data(screen_x, screen_y);
let x_tol = if data_x.abs() > 1.0 {
data_x.abs() * tolerance
} else {
tolerance
};
let y_tol = if data_y.abs() > 1.0 {
data_y.abs() * tolerance
} else {
tolerance
};
assert!(
(data_x - recovered_x).abs() < x_tol,
"X roundtrip failed: {} -> {} -> {} (tolerance: {})",
data_x,
screen_x,
recovered_x,
x_tol
);
assert!(
(data_y - recovered_y).abs() < y_tol,
"Y roundtrip failed: {} -> {} -> {} (tolerance: {})",
data_y,
screen_y,
recovered_y,
y_tol
);
}
}
#[test]
fn test_from_plot_area() {
let transform = CoordinateTransform::from_plot_area(
50.0, 50.0, 700.0, 500.0, 0.0, 100.0, 0.0, 100.0, );
assert!((transform.screen_x.start - 50.0).abs() < f32::EPSILON);
assert!((transform.screen_x.end - 750.0).abs() < f32::EPSILON);
assert!((transform.screen_y.start - 50.0).abs() < f32::EPSILON);
assert!((transform.screen_y.end - 550.0).abs() < f32::EPSILON);
let (x, y) = transform.data_to_screen(50.0, 50.0);
assert!((x - 400.0).abs() < f32::EPSILON); assert!((y - 300.0).abs() < f32::EPSILON); }
#[test]
fn test_non_inverted() {
let transform =
CoordinateTransform::new_non_inverted(0.0..100.0, 0.0..100.0, 0.0..100.0, 0.0..100.0);
let (_, y) = transform.data_to_screen(0.0, 0.0);
assert!((y - 0.0).abs() < f32::EPSILON);
let (_, y) = transform.data_to_screen(0.0, 100.0);
assert!((y - 100.0).abs() < f32::EPSILON);
}
#[test]
fn test_contains_data() {
let transform = CoordinateTransform::new(0.0..100.0, 0.0..100.0, 0.0..100.0, 0.0..100.0);
assert!(transform.contains_data(50.0, 50.0));
assert!(transform.contains_data(0.0, 0.0));
assert!(transform.contains_data(100.0, 100.0));
assert!(!transform.contains_data(-1.0, 50.0));
assert!(!transform.contains_data(50.0, 101.0));
}
#[test]
fn test_zero_range() {
let transform = CoordinateTransform::new(
50.0..50.0, 50.0..50.0, 0.0..100.0,
0.0..100.0,
);
let (x, y) = transform.data_to_screen(50.0, 50.0);
assert!((x - 50.0).abs() < f32::EPSILON); assert!((y - 50.0).abs() < f32::EPSILON);
}
#[test]
fn test_helper_methods() {
let transform = CoordinateTransform::new(0.0..200.0, 0.0..100.0, 50.0..850.0, 100.0..600.0);
assert!((transform.screen_width() - 800.0).abs() < f32::EPSILON);
assert!((transform.screen_height() - 500.0).abs() < f32::EPSILON);
assert!((transform.data_width() - 200.0).abs() < f64::EPSILON);
assert!((transform.data_height() - 100.0).abs() < f64::EPSILON);
let (cx, cy) = transform.data_center();
assert!((cx - 100.0).abs() < f64::EPSILON);
assert!((cy - 50.0).abs() < f64::EPSILON);
let (sx, sy) = transform.screen_center();
assert!((sx - 450.0).abs() < f32::EPSILON);
assert!((sy - 350.0).abs() < f32::EPSILON);
}
#[test]
fn test_scaled_transform_endpoints_midpoints_and_reversed_screen_ranges() {
let transform =
CoordinateTransform::new(1.0..100.0, -100.0..100.0, 700.0..100.0, 500.0..50.0);
let x_scale = AxisScale::Log;
let y_scale = AxisScale::symlog(1.0);
let (start_x, start_y) = transform.data_to_screen_scaled(1.0, -100.0, &x_scale, &y_scale);
assert!((start_x - 700.0).abs() < f32::EPSILON);
assert!((start_y - 50.0).abs() < f32::EPSILON);
let (mid_x, mid_y) = transform.data_to_screen_scaled(10.0, 0.0, &x_scale, &y_scale);
assert!((mid_x - 400.0).abs() < f32::EPSILON);
assert!((mid_y - 275.0).abs() < f32::EPSILON);
let (end_x, end_y) = transform.data_to_screen_scaled(100.0, 100.0, &x_scale, &y_scale);
assert!((end_x - 100.0).abs() < f32::EPSILON);
assert!((end_y - 500.0).abs() < f32::EPSILON);
}
#[test]
fn test_scaled_transform_roundtrips_reversed_data_and_screen_ranges() {
let transform =
CoordinateTransform::new(1000.0..1.0, 100.0..-100.0, 900.0..75.0, 40.0..640.0);
let x_scale = AxisScale::Log;
let y_scale = AxisScale::symlog(2.0);
let points = [(1000.0, 100.0), (100.0, 10.0), (10.0, 0.0), (1.0, -100.0)];
for (data_x, data_y) in points {
let (screen_x, screen_y) =
transform.data_to_screen_scaled(data_x, data_y, &x_scale, &y_scale);
let (recovered_x, recovered_y) =
transform.screen_to_data_scaled(screen_x, screen_y, &x_scale, &y_scale);
let x_tolerance = data_x.abs().max(1.0) * 1e-5;
let y_tolerance = data_y.abs().max(1.0) * 1e-5;
assert!((recovered_x - data_x).abs() <= x_tolerance);
assert!((recovered_y - data_y).abs() <= y_tolerance);
}
}
#[test]
fn test_scaled_linear_transform_uses_exact_epsilon_and_extreme_range_rules() {
let min = 1.0;
let max = min + f64::EPSILON;
let transform =
CoordinateTransform::new(min..max, -f64::MAX..f64::MAX, 10.0..210.0, 20.0..120.0);
let linear = AxisScale::Linear;
assert_eq!(
transform.data_to_screen_scaled(min, -f64::MAX, &linear, &linear),
(10.0, 120.0)
);
assert_eq!(
transform.data_to_screen_scaled(max, f64::MAX, &linear, &linear),
(210.0, 20.0)
);
assert_eq!(
transform.screen_to_data_scaled(10.0, 120.0, &linear, &linear),
(min, -f64::MAX)
);
assert_eq!(
transform.screen_to_data_scaled(210.0, 20.0, &linear, &linear),
(max, f64::MAX)
);
}
#[test]
fn test_linear_transform_preserves_exact_epsilon_degeneracy_semantics() {
let transform =
CoordinateTransform::new(1.0..1.0 + f64::EPSILON, 0.0..1.0, 10.0..210.0, 20.0..120.0);
assert_eq!(transform.data_to_screen(1.0, 0.5).0, 110.0);
assert_eq!(transform.data_to_screen(1.0 + f64::EPSILON, 0.5).0, 110.0);
}
}