1#![warn(missing_docs)]
11
12use crate::plot::scale::Scale;
13use crate::plot::series::SeriesBounds;
14use crate::plot::spec::{Mark, PlotSpec};
15use crate::plot::view::{AxisView, PlotView};
16use crate::tree::Rect;
17
18pub const FIT_PADDING: f64 = 0.05;
22
23const GUTTER_BOTTOM: f32 = 28.0;
28const MARGIN_TOP: f32 = 10.0;
29const MARGIN_RIGHT: f32 = 12.0;
30
31pub const GUTTER_LEFT_MIN: f32 = 40.0;
34const Y_TICK_LABEL_SIZE: f32 = 11.0;
37const Y_LABEL_GAP: f32 = 12.0;
40const Y_TICK_TARGET: usize = 6;
42
43pub fn left_gutter(spec: &PlotSpec, view: &PlotView) -> f32 {
47 let ys = spec.y.scale;
48 let mut widest = 0.0_f32;
49 for t in ys.ticks((view.y.min, view.y.max), Y_TICK_TARGET) {
50 let w = crate::text::metrics::line_width(
51 &t.label,
52 Y_TICK_LABEL_SIZE,
53 crate::tree::FontWeight::default(),
54 false,
55 );
56 widest = widest.max(w);
57 }
58 (widest + Y_LABEL_GAP).max(GUTTER_LEFT_MIN)
59}
60
61pub fn data_rect(node_rect: Rect, gutter_left: f32) -> Rect {
65 let x = node_rect.x + gutter_left;
66 let y = node_rect.y + MARGIN_TOP;
67 let w = (node_rect.w - gutter_left - MARGIN_RIGHT).max(0.0);
68 let h = (node_rect.h - MARGIN_TOP - GUTTER_BOTTOM).max(0.0);
69 Rect::new(x, y, w, h)
70}
71
72pub fn data_bounds(spec: &PlotSpec) -> SeriesBounds {
75 let mut bounds = SeriesBounds::default();
76 for mark in &spec.marks {
77 bounds = bounds.union(series_of(mark).bounds());
78 }
79 bounds
80}
81
82fn series_of(mark: &Mark) -> &crate::plot::series::SeriesHandle {
84 match mark {
85 Mark::Line(m) => &m.series,
86 Mark::Scatter(m) => &m.series,
87 }
88}
89
90pub fn autofit(bounds: SeriesBounds, xs: Scale, ys: Scale) -> PlotView {
94 PlotView::fit(
95 bounds.x.unwrap_or((0.0, 1.0)),
96 bounds.y.unwrap_or((0.0, 1.0)),
97 FIT_PADDING,
98 xs,
99 ys,
100 )
101}
102
103pub fn visible_y(spec: &PlotSpec, x: AxisView) -> Option<(f64, f64)> {
107 let (lo, hi) = (x.min.min(x.max), x.min.max(x.max));
108 let mut acc: Option<(f64, f64)> = None;
109 for mark in &spec.marks {
110 let (samples, _) = series_of(mark).snapshot();
111 for s in samples.iter() {
112 if s.x.is_finite() && s.y.is_finite() && s.x >= lo && s.x <= hi {
113 acc = Some(match acc {
114 Some((ylo, yhi)) => (ylo.min(s.y), yhi.max(s.y)),
115 None => (s.y, s.y),
116 });
117 }
118 }
119 }
120 acc
121}
122
123pub fn pad_y(span: (f64, f64), scale: Scale) -> AxisView {
127 AxisView::fit(span, FIT_PADDING, scale)
128}
129
130pub fn resolve_view(
140 spec: &PlotSpec,
141 persisted: Option<PlotView>,
142 autoscale_x: bool,
143 autoscale_y: bool,
144) -> PlotView {
145 let bounds = data_bounds(spec);
146 let fit = autofit(bounds, spec.x.scale, spec.y.scale);
147 let mut view = persisted.unwrap_or(fit);
148 if autoscale_x && bounds.x.is_some() {
149 view = view.with_x(fit.x);
150 }
151 if autoscale_y && let Some(span) = visible_y(spec, view.x) {
152 view = view.with_y(pad_y(span, spec.y.scale));
153 }
154 view
155}
156
157pub fn x_scale(spec: &PlotSpec) -> Scale {
160 spec.x.scale
161}
162
163pub fn y_scale(spec: &PlotSpec) -> Scale {
165 spec.y.scale
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::plot::series::{Sample, SeriesHandle};
172 use crate::plot::spec::line;
173
174 fn spec_with(samples: Vec<Sample>) -> PlotSpec {
175 let h = SeriesHandle::new(samples);
176 PlotSpec::new().add_mark(line(&h))
177 }
178
179 #[test]
180 fn data_rect_insets_gutters() {
181 let g = 52.0;
182 let r = data_rect(Rect::new(0.0, 0.0, 200.0, 100.0), g);
183 assert_eq!(r.x, g);
184 assert_eq!(r.y, MARGIN_TOP);
185 assert_eq!(r.w, 200.0 - g - MARGIN_RIGHT);
186 assert_eq!(r.h, 100.0 - MARGIN_TOP - GUTTER_BOTTOM);
187 }
188
189 #[test]
190 fn data_rect_clamps_to_nonnegative() {
191 let r = data_rect(Rect::new(0.0, 0.0, 10.0, 10.0), 52.0);
192 assert_eq!(r.w, 0.0);
193 assert_eq!(r.h, 0.0);
194 }
195
196 #[test]
197 fn left_gutter_grows_for_wide_labels_and_floors() {
198 let wide = spec_with(vec![Sample::new(0.0, 0.0), Sample::new(1.0, 1_000_000.0)]);
201 let narrow = spec_with(vec![Sample::new(0.0, 0.0), Sample::new(1.0, 9.0)]);
202 let view_w = resolve_view(&wide, None, true, true);
203 let view_n = resolve_view(&narrow, None, true, true);
204 let g_wide = left_gutter(&wide, &view_w);
205 let g_narrow = left_gutter(&narrow, &view_n);
206 assert!(
207 g_wide > g_narrow,
208 "wide labels widen the gutter: {g_wide} vs {g_narrow}"
209 );
210 assert!(
211 g_narrow >= GUTTER_LEFT_MIN,
212 "floored at the minimum: {g_narrow}"
213 );
214 }
215
216 #[test]
217 fn data_bounds_union_over_marks() {
218 let a = SeriesHandle::new(vec![Sample::new(0.0, 1.0), Sample::new(5.0, 3.0)]);
219 let b = SeriesHandle::new(vec![Sample::new(-2.0, 0.0), Sample::new(3.0, 9.0)]);
220 let spec = PlotSpec::new().line(&a).line(&b);
221 let bounds = data_bounds(&spec);
222 assert_eq!(bounds.x, Some((-2.0, 5.0)));
223 assert_eq!(bounds.y, Some((0.0, 9.0)));
224 }
225
226 #[test]
227 fn autofit_pads_the_window() {
228 let bounds = SeriesBounds {
229 x: Some((0.0, 100.0)),
230 y: Some((0.0, 10.0)),
231 };
232 let v = autofit(bounds, Scale::linear(), Scale::linear());
233 assert!(v.x.min < 0.0 && v.x.max > 100.0);
234 assert!(v.y.min < 0.0 && v.y.max > 10.0);
235 }
236
237 #[test]
238 fn visible_y_only_counts_in_window() {
239 let spec = spec_with(vec![
240 Sample::new(0.0, 1.0),
241 Sample::new(5.0, 100.0), Sample::new(1.0, 2.0),
243 ]);
244 let span = visible_y(&spec, AxisView::new(-0.5, 1.5)).unwrap();
245 assert_eq!(span, (1.0, 2.0)); }
247
248 #[test]
249 fn resolve_view_autoscales_y_to_visible() {
250 let spec = spec_with(vec![Sample::new(0.0, 0.0), Sample::new(10.0, 1000.0)]);
251 let persisted = PlotView::new(AxisView::new(-1.0, 1.0), AxisView::new(-5.0, 5.0));
253 let v = resolve_view(&spec, Some(persisted), false, true);
254 assert!(v.y.max < 100.0, "y autoscaled to visible: {:?}", v.y);
255 }
256
257 #[test]
258 fn resolve_view_autofits_when_unpersisted() {
259 let spec = spec_with(vec![Sample::new(0.0, 0.0), Sample::new(4.0, 8.0)]);
260 let v = resolve_view(&spec, None, true, true);
261 assert!(v.x.min < 0.0 && v.x.max > 4.0);
262 }
263}
264
265#[cfg(test)]
266mod log_fit_tests {
267 use super::*;
268 use crate::plot::series::{Sample, SeriesHandle};
269 use crate::plot::spec::line;
270 use crate::tree::Rect;
271
272 #[test]
277 fn log_y_autofit_keeps_marks_spread_and_ticks_sane() {
278 let samples: Vec<Sample> = (0..149)
280 .map(|i| {
281 let x = f64::from(i) * (36000.0 / 148.0);
282 let y = 65536.0 * (1.0f64 / 65536.0).powf(f64::from(i) / 148.0);
283 Sample::new(x, y)
284 })
285 .collect();
286 let h = SeriesHandle::new(samples);
287 let spec = PlotSpec::new()
288 .x(Scale::linear())
289 .y(Scale::log())
290 .add_mark(line(&h));
291
292 let view = resolve_view(&spec, None, true, true);
293 assert!(
294 view.y.min > 0.0,
295 "log-y window stays positive: {:?}",
296 view.y
297 );
298
299 let rect = Rect::new(0.0, 0.0, 400.0, 300.0);
302 let (xs, ys) = (Scale::linear(), Scale::log());
303 let top = view.project((0.0, 65536.0), xs, ys, rect).1;
304 let bottom = view.project((36000.0, 1.0), xs, ys, rect).1;
305 assert!(
306 (bottom - top).abs() > rect.h * 0.8,
307 "marks span the rect: {top} .. {bottom}"
308 );
309
310 let ticks = ys.ticks((view.y.min, view.y.max), 6);
312 let values: Vec<f64> = ticks.iter().map(|t| t.value).collect();
313 assert_eq!(values, vec![1.0, 10.0, 100.0, 1000.0, 10000.0, 100000.0]);
314 assert!(ticks.iter().all(|t| t.label != "0"), "labels: {ticks:?}");
315 }
316}
317
318#[cfg(test)]
319mod x_autoscale_tests {
320 use super::*;
321 use crate::plot::Sample;
322 use crate::plot::series::SeriesHandle;
323
324 fn spec_of(h: &SeriesHandle) -> PlotSpec {
325 PlotSpec::new().line(h)
326 }
327
328 #[test]
331 fn resolve_view_x_autoscale_tracks_growing_data() {
332 let h = SeriesHandle::new(vec![Sample::new(0.0, 0.0), Sample::new(1.0, 1.0)]);
333 let spec = spec_of(&h);
334 let first = resolve_view(&spec, None, true, true);
335 assert!(first.x.max < 2.0, "seeded around the initial extent");
336
337 h.append(&[Sample::new(100.0, 5.0)]);
339 let next = resolve_view(&spec, Some(first), true, true);
340 assert!(
341 next.x.max > 100.0,
342 "x window follows the data: {:?}",
343 next.x
344 );
345 }
346
347 #[test]
350 fn resolve_view_manual_x_holds_the_window() {
351 let h = SeriesHandle::new(vec![Sample::new(0.0, 0.0), Sample::new(1.0, 1.0)]);
352 let spec = spec_of(&h);
353 let first = resolve_view(&spec, None, false, true);
354 h.append(&[Sample::new(100.0, 5.0)]);
355 let next = resolve_view(&spec, Some(first), false, true);
356 assert_eq!(next.x, first.x, "manual x window is sticky");
357 }
358}