#![warn(missing_docs)]
use crate::plot::scale::Scale;
use crate::plot::series::SeriesBounds;
use crate::plot::spec::{Mark, PlotSpec};
use crate::plot::view::{AxisView, PlotView};
use crate::tree::Rect;
pub const FIT_PADDING: f64 = 0.05;
const GUTTER_BOTTOM: f32 = 28.0;
const MARGIN_TOP: f32 = 10.0;
const MARGIN_RIGHT: f32 = 12.0;
pub const GUTTER_LEFT_MIN: f32 = 40.0;
const Y_TICK_LABEL_SIZE: f32 = 11.0;
const Y_LABEL_GAP: f32 = 12.0;
const Y_TICK_TARGET: usize = 6;
pub fn left_gutter(spec: &PlotSpec, view: &PlotView) -> f32 {
let ys = spec.y.scale;
let mut widest = 0.0_f32;
for t in ys.ticks((view.y.min, view.y.max), Y_TICK_TARGET) {
let w = crate::text::metrics::line_width(
&t.label,
Y_TICK_LABEL_SIZE,
crate::tree::FontWeight::default(),
false,
);
widest = widest.max(w);
}
(widest + Y_LABEL_GAP).max(GUTTER_LEFT_MIN)
}
pub fn data_rect(node_rect: Rect, gutter_left: f32) -> Rect {
let x = node_rect.x + gutter_left;
let y = node_rect.y + MARGIN_TOP;
let w = (node_rect.w - gutter_left - MARGIN_RIGHT).max(0.0);
let h = (node_rect.h - MARGIN_TOP - GUTTER_BOTTOM).max(0.0);
Rect::new(x, y, w, h)
}
pub fn data_bounds(spec: &PlotSpec) -> SeriesBounds {
let mut bounds = SeriesBounds::default();
for mark in &spec.marks {
bounds = bounds.union(series_of(mark).bounds());
}
bounds
}
fn series_of(mark: &Mark) -> &crate::plot::series::SeriesHandle {
match mark {
Mark::Line(m) => &m.series,
Mark::Scatter(m) => &m.series,
}
}
pub fn autofit(bounds: SeriesBounds) -> PlotView {
PlotView::fit(
bounds.x.unwrap_or((0.0, 1.0)),
bounds.y.unwrap_or((0.0, 1.0)),
FIT_PADDING,
)
}
pub fn visible_y(spec: &PlotSpec, x: AxisView) -> Option<(f64, f64)> {
let (lo, hi) = (x.min.min(x.max), x.min.max(x.max));
let mut acc: Option<(f64, f64)> = None;
for mark in &spec.marks {
let (samples, _) = series_of(mark).snapshot();
for s in samples.iter() {
if s.x.is_finite() && s.y.is_finite() && s.x >= lo && s.x <= hi {
acc = Some(match acc {
Some((ylo, yhi)) => (ylo.min(s.y), yhi.max(s.y)),
None => (s.y, s.y),
});
}
}
}
acc
}
pub fn pad_y(span: (f64, f64)) -> AxisView {
let (lo, hi) = span;
if hi <= lo {
return AxisView::new(lo - 0.5, hi + 0.5);
}
let m = (hi - lo) * FIT_PADDING;
AxisView::new(lo - m, hi + m)
}
pub fn resolve_view(spec: &PlotSpec, persisted: Option<PlotView>, autoscale_y: bool) -> PlotView {
let bounds = data_bounds(spec);
let mut view = persisted.unwrap_or_else(|| autofit(bounds));
if autoscale_y {
if let Some(span) = visible_y(spec, view.x) {
view = view.with_y(pad_y(span));
}
}
view
}
pub fn x_scale(spec: &PlotSpec) -> Scale {
spec.x.scale
}
pub fn y_scale(spec: &PlotSpec) -> Scale {
spec.y.scale
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plot::series::{Sample, SeriesHandle};
use crate::plot::spec::line;
fn spec_with(samples: Vec<Sample>) -> PlotSpec {
let h = SeriesHandle::new(samples);
PlotSpec::new().add_mark(line(&h))
}
#[test]
fn data_rect_insets_gutters() {
let g = 52.0;
let r = data_rect(Rect::new(0.0, 0.0, 200.0, 100.0), g);
assert_eq!(r.x, g);
assert_eq!(r.y, MARGIN_TOP);
assert_eq!(r.w, 200.0 - g - MARGIN_RIGHT);
assert_eq!(r.h, 100.0 - MARGIN_TOP - GUTTER_BOTTOM);
}
#[test]
fn data_rect_clamps_to_nonnegative() {
let r = data_rect(Rect::new(0.0, 0.0, 10.0, 10.0), 52.0);
assert_eq!(r.w, 0.0);
assert_eq!(r.h, 0.0);
}
#[test]
fn left_gutter_grows_for_wide_labels_and_floors() {
let wide = spec_with(vec![Sample::new(0.0, 0.0), Sample::new(1.0, 1_000_000.0)]);
let narrow = spec_with(vec![Sample::new(0.0, 0.0), Sample::new(1.0, 9.0)]);
let view_w = resolve_view(&wide, None, true);
let view_n = resolve_view(&narrow, None, true);
let g_wide = left_gutter(&wide, &view_w);
let g_narrow = left_gutter(&narrow, &view_n);
assert!(
g_wide > g_narrow,
"wide labels widen the gutter: {g_wide} vs {g_narrow}"
);
assert!(
g_narrow >= GUTTER_LEFT_MIN,
"floored at the minimum: {g_narrow}"
);
}
#[test]
fn data_bounds_union_over_marks() {
let a = SeriesHandle::new(vec![Sample::new(0.0, 1.0), Sample::new(5.0, 3.0)]);
let b = SeriesHandle::new(vec![Sample::new(-2.0, 0.0), Sample::new(3.0, 9.0)]);
let spec = PlotSpec::new().line(&a).line(&b);
let bounds = data_bounds(&spec);
assert_eq!(bounds.x, Some((-2.0, 5.0)));
assert_eq!(bounds.y, Some((0.0, 9.0)));
}
#[test]
fn autofit_pads_the_window() {
let bounds = SeriesBounds {
x: Some((0.0, 100.0)),
y: Some((0.0, 10.0)),
};
let v = autofit(bounds);
assert!(v.x.min < 0.0 && v.x.max > 100.0);
assert!(v.y.min < 0.0 && v.y.max > 10.0);
}
#[test]
fn visible_y_only_counts_in_window() {
let spec = spec_with(vec![
Sample::new(0.0, 1.0),
Sample::new(5.0, 100.0), Sample::new(1.0, 2.0),
]);
let span = visible_y(&spec, AxisView::new(-0.5, 1.5)).unwrap();
assert_eq!(span, (1.0, 2.0)); }
#[test]
fn resolve_view_autoscales_y_to_visible() {
let spec = spec_with(vec![Sample::new(0.0, 0.0), Sample::new(10.0, 1000.0)]);
let persisted = PlotView::new(AxisView::new(-1.0, 1.0), AxisView::new(-5.0, 5.0));
let v = resolve_view(&spec, Some(persisted), true);
assert!(v.y.max < 100.0, "y autoscaled to visible: {:?}", v.y);
}
#[test]
fn resolve_view_autofits_when_unpersisted() {
let spec = spec_with(vec![Sample::new(0.0, 0.0), Sample::new(4.0, 8.0)]);
let v = resolve_view(&spec, None, true);
assert!(v.x.min < 0.0 && v.x.max > 4.0);
}
}