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 norm_x = if x_range.abs() > f64::EPSILON {
(data_x - self.data_x.start) / x_range
} else {
0.5
};
let norm_y = if y_range.abs() > f64::EPSILON {
(data_y - self.data_y.start) / y_range
} else {
0.5
};
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 + (norm_x as f32) * screen_width;
let screen_y = if self.y_inverted {
self.screen_y.start + (1.0 - norm_y as f32) * screen_height
} else {
self.screen_y.start + (norm_y as f32) * screen_height
};
(screen_x, screen_y)
}
#[inline]
pub fn screen_to_data(&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 norm_x = (screen_x - self.screen_x.start) / screen_width;
let norm_y = if self.y_inverted {
1.0 - (screen_y - self.screen_y.start) / screen_height
} else {
(screen_y - self.screen_y.start) / screen_height
};
let data_x = self.data_x.start + (norm_x as f64) * (self.data_x.end - self.data_x.start);
let data_y = self.data_y.start + (norm_y as f64) * (self.data_y.end - self.data_y.start);
(data_x, data_y)
}
#[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);
}
}