#![warn(missing_docs)]
use crate::Rect;
use crate::plot::scale::Scale;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AxisView {
pub min: f64,
pub max: f64,
}
impl AxisView {
pub fn new(min: f64, max: f64) -> Self {
Self { min, max }
}
fn forward(self, scale: Scale) -> (f64, f64) {
(scale.forward(self.min), scale.forward(self.max))
}
fn zoomed(self, factor: f64, t: f64, scale: Scale) -> Self {
let (fa, fb) = self.forward(scale);
let anchor = fa + t * (fb - fa);
Self {
min: scale.inverse(anchor + (fa - anchor) * factor),
max: scale.inverse(anchor + (fb - anchor) * factor),
}
}
fn panned(self, frac: f64, scale: Scale) -> Self {
let (fa, fb) = self.forward(scale);
let d = frac * (fb - fa);
Self {
min: scale.inverse(fa + d),
max: scale.inverse(fb + d),
}
}
fn fraction(self, v: f64, scale: Scale) -> f64 {
let (fa, fb) = self.forward(scale);
if fb == fa {
0.0
} else {
(scale.forward(v) - fa) / (fb - fa)
}
}
fn at_fraction(self, t: f64, scale: Scale) -> f64 {
let (fa, fb) = self.forward(scale);
scale.inverse(fa + t * (fb - fa))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PlotView {
pub x: AxisView,
pub y: AxisView,
}
impl PlotView {
pub fn new(x: AxisView, y: AxisView) -> Self {
Self { x, y }
}
pub fn fit(x: (f64, f64), y: (f64, f64), pad: f64) -> Self {
Self {
x: pad_axis(x, pad),
y: pad_axis(y, pad),
}
}
pub fn project(self, p: (f64, f64), x: Scale, y: Scale, rect: Rect) -> (f32, f32) {
let fx = self.x.fraction(p.0, x);
let fy = self.y.fraction(p.1, y);
(
rect.x + (fx as f32) * rect.w,
rect.y + ((1.0 - fy) as f32) * rect.h,
)
}
pub fn unproject(self, s: (f32, f32), x: Scale, y: Scale, rect: Rect) -> (f64, f64) {
let tx = if rect.w != 0.0 {
((s.0 - rect.x) / rect.w) as f64
} else {
0.0
};
let ty = if rect.h != 0.0 {
1.0 - ((s.1 - rect.y) / rect.h) as f64
} else {
0.0
};
(self.x.at_fraction(tx, x), self.y.at_fraction(ty, y))
}
pub fn pan_pixels(self, d: (f32, f32), x: Scale, y: Scale, rect: Rect) -> Self {
let frac_x = if rect.w != 0.0 {
-(d.0 as f64) / rect.w as f64
} else {
0.0
};
let frac_y = if rect.h != 0.0 {
(d.1 as f64) / rect.h as f64
} else {
0.0
};
Self {
x: self.x.panned(frac_x, x),
y: self.y.panned(frac_y, y),
}
}
pub fn zoom_about(
self,
factor: (f64, f64),
anchor: (f32, f32),
x: Scale,
y: Scale,
rect: Rect,
) -> Self {
let tx = if rect.w != 0.0 {
((anchor.0 - rect.x) / rect.w) as f64
} else {
0.5
};
let ty = if rect.h != 0.0 {
1.0 - ((anchor.1 - rect.y) / rect.h) as f64
} else {
0.5
};
Self {
x: self.x.zoomed(factor.0, tx, x),
y: self.y.zoomed(factor.1, ty, y),
}
}
pub fn with_y(self, y: AxisView) -> Self {
Self { y, ..self }
}
}
fn pad_axis((min, max): (f64, f64), pad: f64) -> AxisView {
if !min.is_finite() || !max.is_finite() || max <= min {
let c = if min.is_finite() { min } else { 0.0 };
return AxisView::new(c - 0.5, c + 0.5);
}
let m = (max - min) * pad;
AxisView::new(min - m, max + m)
}
#[cfg(test)]
mod tests {
use super::*;
const RECT: Rect = Rect {
x: 0.0,
y: 0.0,
w: 200.0,
h: 100.0,
};
fn lin_view() -> PlotView {
PlotView::new(AxisView::new(0.0, 100.0), AxisView::new(0.0, 50.0))
}
#[test]
fn project_corners_and_center() {
let v = lin_view();
let (xs, ys) = (Scale::linear(), Scale::linear());
assert_eq!(v.project((0.0, 0.0), xs, ys, RECT), (0.0, 100.0));
assert_eq!(v.project((100.0, 50.0), xs, ys, RECT), (200.0, 0.0));
assert_eq!(v.project((50.0, 25.0), xs, ys, RECT), (100.0, 50.0));
}
#[test]
fn project_unproject_roundtrip() {
let v = lin_view();
let (xs, ys) = (Scale::linear(), Scale::linear());
let p = (37.5, 12.25);
let s = v.project(p, xs, ys, RECT);
let back = v.unproject(s, xs, ys, RECT);
assert!((back.0 - p.0).abs() < 1e-4, "x {back:?}");
assert!((back.1 - p.1).abs() < 1e-4, "y {back:?}");
}
#[test]
fn zoom_keeps_anchor_data_fixed() {
let v = lin_view();
let (xs, ys) = (Scale::linear(), Scale::linear());
let anchor = (150.0, 25.0); let before = v.unproject(anchor, xs, ys, RECT);
let zoomed = v.zoom_about((0.5, 0.5), anchor, xs, ys, RECT);
let after = zoomed.unproject(anchor, xs, ys, RECT);
assert!((before.0 - after.0).abs() < 1e-6, "x {before:?} {after:?}");
assert!((before.1 - after.1).abs() < 1e-6, "y {before:?} {after:?}");
assert!(zoomed.x.max - zoomed.x.min < 100.0);
}
#[test]
fn pan_shifts_window_opposite_to_drag_x() {
let v = lin_view();
let (xs, ys) = (Scale::linear(), Scale::linear());
let panned = v.pan_pixels((200.0, 0.0), xs, ys, RECT);
assert!((panned.x.min - -100.0).abs() < 1e-6);
assert!((panned.x.max - 0.0).abs() < 1e-6);
}
#[test]
fn pan_down_reveals_higher_values_at_top() {
let v = lin_view();
let (xs, ys) = (Scale::linear(), Scale::linear());
let panned = v.pan_pixels((0.0, 100.0), xs, ys, RECT);
assert!((panned.y.min - 50.0).abs() < 1e-6);
assert!((panned.y.max - 100.0).abs() < 1e-6);
}
#[test]
fn log_zoom_anchor_fixed() {
let v = PlotView::new(AxisView::new(1.0, 1000.0), AxisView::new(0.0, 1.0));
let (xs, ys) = (Scale::log(), Scale::linear());
let anchor = (50.0, 50.0);
let before = v.unproject(anchor, xs, ys, RECT);
let zoomed = v.zoom_about((0.5, 1.0), anchor, xs, ys, RECT);
let after = zoomed.unproject(anchor, xs, ys, RECT);
assert!(
(before.0 - after.0).abs() / before.0 < 1e-6,
"{before:?} {after:?}"
);
}
#[test]
fn fit_adds_padding_and_handles_degenerate() {
let v = PlotView::fit((0.0, 100.0), (5.0, 5.0), 0.1);
assert!((v.x.min - -10.0).abs() < 1e-9);
assert!((v.x.max - 110.0).abs() < 1e-9);
assert_eq!(v.y, AxisView::new(4.5, 5.5));
}
}