1#![warn(missing_docs)]
19
20use crate::Rect;
21use crate::plot::scale::Scale;
22
23#[derive(Clone, Copy, Debug, PartialEq)]
28pub struct AxisView {
29 pub min: f64,
31 pub max: f64,
33}
34
35impl AxisView {
36 pub fn new(min: f64, max: f64) -> Self {
38 Self { min, max }
39 }
40
41 fn forward(self, scale: Scale) -> (f64, f64) {
43 (scale.forward(self.min), scale.forward(self.max))
44 }
45
46 fn zoomed(self, factor: f64, t: f64, scale: Scale) -> Self {
51 let (fa, fb) = self.forward(scale);
52 let anchor = fa + t * (fb - fa);
53 Self {
54 min: scale.inverse(anchor + (fa - anchor) * factor),
55 max: scale.inverse(anchor + (fb - anchor) * factor),
56 }
57 }
58
59 fn panned(self, frac: f64, scale: Scale) -> Self {
62 let (fa, fb) = self.forward(scale);
63 let d = frac * (fb - fa);
64 Self {
65 min: scale.inverse(fa + d),
66 max: scale.inverse(fb + d),
67 }
68 }
69
70 fn fraction(self, v: f64, scale: Scale) -> f64 {
74 let (fa, fb) = self.forward(scale);
75 if fb == fa {
76 0.0
77 } else {
78 (scale.forward(v) - fa) / (fb - fa)
79 }
80 }
81
82 fn at_fraction(self, t: f64, scale: Scale) -> f64 {
85 let (fa, fb) = self.forward(scale);
86 scale.inverse(fa + t * (fb - fa))
87 }
88}
89
90#[derive(Clone, Copy, Debug, PartialEq)]
101pub struct PlotView {
102 pub x: AxisView,
104 pub y: AxisView,
106}
107
108impl PlotView {
109 pub fn new(x: AxisView, y: AxisView) -> Self {
111 Self { x, y }
112 }
113
114 pub fn fit(x: (f64, f64), y: (f64, f64), pad: f64) -> Self {
119 Self {
120 x: pad_axis(x, pad),
121 y: pad_axis(y, pad),
122 }
123 }
124
125 pub fn project(self, p: (f64, f64), x: Scale, y: Scale, rect: Rect) -> (f32, f32) {
128 let fx = self.x.fraction(p.0, x);
129 let fy = self.y.fraction(p.1, y);
130 (
131 rect.x + (fx as f32) * rect.w,
132 rect.y + ((1.0 - fy) as f32) * rect.h,
133 )
134 }
135
136 pub fn unproject(self, s: (f32, f32), x: Scale, y: Scale, rect: Rect) -> (f64, f64) {
139 let tx = if rect.w != 0.0 {
140 ((s.0 - rect.x) / rect.w) as f64
141 } else {
142 0.0
143 };
144 let ty = if rect.h != 0.0 {
145 1.0 - ((s.1 - rect.y) / rect.h) as f64
146 } else {
147 0.0
148 };
149 (self.x.at_fraction(tx, x), self.y.at_fraction(ty, y))
150 }
151
152 pub fn pan_pixels(self, d: (f32, f32), x: Scale, y: Scale, rect: Rect) -> Self {
156 let frac_x = if rect.w != 0.0 {
157 -(d.0 as f64) / rect.w as f64
158 } else {
159 0.0
160 };
161 let frac_y = if rect.h != 0.0 {
162 (d.1 as f64) / rect.h as f64
163 } else {
164 0.0
165 };
166 Self {
167 x: self.x.panned(frac_x, x),
168 y: self.y.panned(frac_y, y),
169 }
170 }
171
172 pub fn zoom_about(
177 self,
178 factor: (f64, f64),
179 anchor: (f32, f32),
180 x: Scale,
181 y: Scale,
182 rect: Rect,
183 ) -> Self {
184 let tx = if rect.w != 0.0 {
185 ((anchor.0 - rect.x) / rect.w) as f64
186 } else {
187 0.5
188 };
189 let ty = if rect.h != 0.0 {
190 1.0 - ((anchor.1 - rect.y) / rect.h) as f64
191 } else {
192 0.5
193 };
194 Self {
195 x: self.x.zoomed(factor.0, tx, x),
196 y: self.y.zoomed(factor.1, ty, y),
197 }
198 }
199
200 pub fn with_y(self, y: AxisView) -> Self {
204 Self { y, ..self }
205 }
206
207 pub fn with_x(self, x: AxisView) -> Self {
211 Self { x, ..self }
212 }
213}
214
215fn pad_axis((min, max): (f64, f64), pad: f64) -> AxisView {
218 if !min.is_finite() || !max.is_finite() || max <= min {
219 let c = if min.is_finite() { min } else { 0.0 };
221 return AxisView::new(c - 0.5, c + 0.5);
222 }
223 let m = (max - min) * pad;
224 AxisView::new(min - m, max + m)
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 const RECT: Rect = Rect {
232 x: 0.0,
233 y: 0.0,
234 w: 200.0,
235 h: 100.0,
236 };
237
238 fn lin_view() -> PlotView {
239 PlotView::new(AxisView::new(0.0, 100.0), AxisView::new(0.0, 50.0))
240 }
241
242 #[test]
243 fn project_corners_and_center() {
244 let v = lin_view();
245 let (xs, ys) = (Scale::linear(), Scale::linear());
246 assert_eq!(v.project((0.0, 0.0), xs, ys, RECT), (0.0, 100.0));
248 assert_eq!(v.project((100.0, 50.0), xs, ys, RECT), (200.0, 0.0));
250 assert_eq!(v.project((50.0, 25.0), xs, ys, RECT), (100.0, 50.0));
252 }
253
254 #[test]
255 fn project_unproject_roundtrip() {
256 let v = lin_view();
257 let (xs, ys) = (Scale::linear(), Scale::linear());
258 let p = (37.5, 12.25);
259 let s = v.project(p, xs, ys, RECT);
260 let back = v.unproject(s, xs, ys, RECT);
261 assert!((back.0 - p.0).abs() < 1e-4, "x {back:?}");
262 assert!((back.1 - p.1).abs() < 1e-4, "y {back:?}");
263 }
264
265 #[test]
266 fn zoom_keeps_anchor_data_fixed() {
267 let v = lin_view();
268 let (xs, ys) = (Scale::linear(), Scale::linear());
269 let anchor = (150.0, 25.0); let before = v.unproject(anchor, xs, ys, RECT);
271 let zoomed = v.zoom_about((0.5, 0.5), anchor, xs, ys, RECT);
272 let after = zoomed.unproject(anchor, xs, ys, RECT);
273 assert!((before.0 - after.0).abs() < 1e-6, "x {before:?} {after:?}");
274 assert!((before.1 - after.1).abs() < 1e-6, "y {before:?} {after:?}");
275 assert!(zoomed.x.max - zoomed.x.min < 100.0);
277 }
278
279 #[test]
280 fn pan_shifts_window_opposite_to_drag_x() {
281 let v = lin_view();
282 let (xs, ys) = (Scale::linear(), Scale::linear());
283 let panned = v.pan_pixels((200.0, 0.0), xs, ys, RECT);
286 assert!((panned.x.min - -100.0).abs() < 1e-6);
287 assert!((panned.x.max - 0.0).abs() < 1e-6);
288 }
289
290 #[test]
291 fn pan_down_reveals_higher_values_at_top() {
292 let v = lin_view();
293 let (xs, ys) = (Scale::linear(), Scale::linear());
294 let panned = v.pan_pixels((0.0, 100.0), xs, ys, RECT);
296 assert!((panned.y.min - 50.0).abs() < 1e-6);
297 assert!((panned.y.max - 100.0).abs() < 1e-6);
298 }
299
300 #[test]
301 fn log_zoom_anchor_fixed() {
302 let v = PlotView::new(AxisView::new(1.0, 1000.0), AxisView::new(0.0, 1.0));
303 let (xs, ys) = (Scale::log(), Scale::linear());
304 let anchor = (50.0, 50.0);
305 let before = v.unproject(anchor, xs, ys, RECT);
306 let zoomed = v.zoom_about((0.5, 1.0), anchor, xs, ys, RECT);
307 let after = zoomed.unproject(anchor, xs, ys, RECT);
308 assert!(
309 (before.0 - after.0).abs() / before.0 < 1e-6,
310 "{before:?} {after:?}"
311 );
312 }
313
314 #[test]
315 fn fit_adds_padding_and_handles_degenerate() {
316 let v = PlotView::fit((0.0, 100.0), (5.0, 5.0), 0.1);
317 assert!((v.x.min - -10.0).abs() < 1e-9);
318 assert!((v.x.max - 110.0).abs() < 1e-9);
319 assert_eq!(v.y, AxisView::new(4.5, 5.5));
321 }
322}