use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct StickyEdges {
pub(super) y_zero_baseline: bool,
pub(super) all_edges: bool,
pub(super) by_construction: bool,
}
impl StickyEdges {
pub(super) const NONE: Self = Self {
y_zero_baseline: false,
all_edges: false,
by_construction: false,
};
const ZERO_BASELINE: Self = Self {
y_zero_baseline: true,
all_edges: false,
by_construction: false,
};
const ALL_EDGES: Self = Self {
y_zero_baseline: false,
all_edges: true,
by_construction: false,
};
pub(super) const BY_CONSTRUCTION: Self = Self {
y_zero_baseline: false,
all_edges: false,
by_construction: true,
};
fn union(self, other: Self) -> Self {
Self {
y_zero_baseline: self.y_zero_baseline || other.y_zero_baseline,
all_edges: self.all_edges || other.all_edges,
by_construction: self.by_construction && other.by_construction,
}
}
}
fn sticky_edges_of(series_type: &SeriesType) -> StickyEdges {
match series_type {
SeriesType::Bar { .. } | SeriesType::Histogram { .. } => StickyEdges::ZERO_BASELINE,
SeriesType::Heatmap { .. } | SeriesType::Contour { .. } => StickyEdges::ALL_EDGES,
SeriesType::Pie { .. } | SeriesType::Radar { .. } | SeriesType::Polar { .. } => {
StickyEdges::BY_CONSTRUCTION
}
SeriesType::Computed { data } if data.pins_zero_baseline() => StickyEdges::ZERO_BASELINE,
_ => StickyEdges::NONE,
}
}
#[derive(Clone, Copy, Default)]
struct AttachedErrors<'a> {
x: Option<&'a ErrorValues>,
y: Option<&'a ErrorValues>,
}
impl<'a> AttachedErrors<'a> {
fn of(series: &'a PlotSeries) -> Self {
Self {
x: series.x_errors.as_ref(),
y: series.y_errors.as_ref(),
}
}
}
#[derive(Debug, Clone, Copy)]
struct BoundsAccumulator {
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
x_scale: AxisScale,
y_scale: AxisScale,
x_rejected_by_scale: bool,
y_rejected_by_scale: bool,
}
impl BoundsAccumulator {
fn new(x_scale: AxisScale, y_scale: AxisScale) -> Self {
Self {
x_min: f64::INFINITY,
x_max: f64::NEG_INFINITY,
y_min: f64::INFINITY,
y_max: f64::NEG_INFINITY,
x_scale,
y_scale,
x_rejected_by_scale: false,
y_rejected_by_scale: false,
}
}
fn from_bounds(bounds: (f64, f64, f64, f64), x_scale: AxisScale, y_scale: AxisScale) -> Self {
Self {
x_min: bounds.0,
x_max: bounds.1,
y_min: bounds.2,
y_max: bounds.3,
x_scale,
y_scale,
x_rejected_by_scale: false,
y_rejected_by_scale: false,
}
}
fn axis_with_no_representable_data(&self) -> Option<(&'static str, &'static str)> {
if self.x_rejected_by_scale && !(self.x_min.is_finite() && self.x_max.is_finite()) {
return Some(("x", "xscale"));
}
if self.y_rejected_by_scale && !(self.y_min.is_finite() && self.y_max.is_finite()) {
return Some(("y", "yscale"));
}
None
}
fn bounds(&self) -> (f64, f64, f64, f64) {
(self.x_min, self.x_max, self.y_min, self.y_max)
}
fn finite_bounds(&self) -> Option<(f64, f64, f64, f64)> {
(self.x_min.is_finite()
&& self.x_max.is_finite()
&& self.y_min.is_finite()
&& self.y_max.is_finite())
.then(|| self.bounds())
}
fn include_x(&mut self, x: f64) {
if self.x_scale.is_valid_value(x) {
self.x_min = self.x_min.min(x);
self.x_max = self.x_max.max(x);
} else if x.is_finite() {
self.x_rejected_by_scale = true;
}
}
fn include_y(&mut self, y: f64) {
if self.y_scale.is_valid_value(y) {
self.y_min = self.y_min.min(y);
self.y_max = self.y_max.max(y);
} else if y.is_finite() {
self.y_rejected_by_scale = true;
}
}
fn include_point(&mut self, x: f64, y: f64) {
self.include_x(x);
self.include_y(y);
}
fn include_x_span(&mut self, a: f64, b: f64) {
self.include_x(a);
self.include_x(b);
}
fn include_y_span(&mut self, a: f64, b: f64) {
self.include_y(a);
self.include_y(b);
}
fn include_plot_data<T: crate::plots::traits::PlotData + ?Sized>(&mut self, data: &T) {
let ((x_min, x_max), (y_min, y_max)) = crate::plots::traits::PlotData::data_bounds(data);
self.include_x_span(x_min, x_max);
self.include_y_span(y_min, y_max);
}
fn add_points_with_errors(
&mut self,
x: &[f64],
y: &[f64],
x_errors: Option<ErrorValuesRef<'_>>,
y_errors: Option<ErrorValuesRef<'_>>,
) {
for (index, (&x_value, &y_value)) in x.iter().zip(y.iter()).enumerate() {
if x_value.is_finite() {
match finite_error_at(x_errors, index) {
Some((lower, upper)) => self.include_x_span(x_value - lower, x_value + upper),
None => self.include_x(x_value),
}
}
if y_value.is_finite() {
match finite_error_at(y_errors, index) {
Some((lower, upper)) => self.include_y_span(y_value - lower, y_value + upper),
None => self.include_y(y_value),
}
}
}
}
fn add_bars(&mut self, category_count: usize, values: &[f64]) {
self.include_x_span(-0.5, category_count as f64 - 0.5);
for &value in values {
if value.is_finite() {
self.include_y_span(value.min(0.0), value.max(0.0));
}
}
}
fn add_histogram(&mut self, data: &crate::plots::histogram::HistogramData) {
if let (Some(&first), Some(&last)) = (data.bin_edges.first(), data.bin_edges.last()) {
self.include_x_span(first, last);
}
self.include_y(0.0);
for &count in &data.counts {
if count > 0.0 {
self.include_y(count);
}
}
}
fn add_box_plot(
&mut self,
data: &[f64],
config: &crate::plots::boxplot::BoxPlotConfig,
) -> Result<()> {
if data.is_empty() {
return Err(PlottingError::EmptyDataSet);
}
let (lo, hi) = crate::plots::boxplot::category_slot_span(config.x_center());
self.include_x_span(lo, hi);
for &value in data {
self.include_y(value);
}
Ok(())
}
fn add_computed_series(&mut self, series_type: &SeriesType) {
match series_type {
SeriesType::Heatmap { data } => self.include_plot_data(data.as_ref()),
SeriesType::Boxen { data } => self.include_plot_data(data.as_ref()),
SeriesType::Computed { data } => self.include_plot_data(data.as_ref()),
SeriesType::Kde { data } => {
self.add_points_with_errors(&data.x, &data.y, None, None);
self.include_y(0.0);
}
SeriesType::Ecdf { data } => {
self.add_points_with_errors(&data.x, &data.y, None, None);
self.include_y(0.0);
}
SeriesType::Violin { data } => {
if data.kde.x.is_empty() {
self.include_y_span(data.range.0, data.range.1);
} else {
for &value in &data.kde.x {
self.include_y(value);
}
}
let (lo, hi) = crate::plots::boxplot::category_slot_span(data.config.x_center());
self.include_x_span(lo, hi);
}
SeriesType::Quiver { data } => {
for arrow in &data.arrows {
for (x, y) in [
arrow.start,
arrow.end,
arrow.head[0],
arrow.head[1],
arrow.head[2],
] {
self.include_point(x, y);
}
}
}
SeriesType::Contour { data } => {
for &x in &data.x {
self.include_x(x);
}
for &y in &data.y {
self.include_y(y);
}
}
SeriesType::Pie { .. } => {
self.include_x_span(0.0, 1.0);
self.include_y_span(0.0, 1.0);
}
SeriesType::Radar { .. } => {
let radius = crate::plots::polar::radar::RADAR_BOUNDS_RADIUS;
self.include_x_span(-radius, radius);
self.include_y_span(-radius, radius);
}
SeriesType::Polar { data } => {
let label_margin = data.bounds_radius();
self.x_min = -label_margin;
self.x_max = label_margin;
self.y_min = -label_margin;
self.y_max = label_margin;
}
SeriesType::Line { .. }
| SeriesType::Scatter { .. }
| SeriesType::Bar { .. }
| SeriesType::ErrorBars { .. }
| SeriesType::ErrorBarsXY { .. }
| SeriesType::Histogram { .. }
| SeriesType::BoxPlot { .. } => {
unreachable!("data-carrying series are accumulated from their resolved values")
}
}
}
fn include_annotations(&mut self, annotations: &[Annotation]) {
for annotation in annotations {
match annotation {
Annotation::Text { x, y, .. } => self.include_point(*x, *y),
Annotation::Arrow { x1, y1, x2, y2, .. } => {
self.include_point(*x1, *y1);
self.include_point(*x2, *y2);
}
Annotation::HLine { y, .. } => self.include_y(*y),
Annotation::VLine { x, .. } => self.include_x(*x),
Annotation::Rectangle {
x,
y,
width,
height,
..
} => {
self.include_point(*x, *y);
self.include_point(*x + *width, *y + *height);
}
Annotation::FillBetween { x, y1, y2, .. } => {
for ((&x_value, &y1_value), &y2_value) in x.iter().zip(y1).zip(y2) {
self.include_point(x_value, y1_value);
self.include_y(y2_value);
}
}
Annotation::HSpan { x_min, x_max, .. } => self.include_x_span(*x_min, *x_max),
Annotation::VSpan { y_min, y_max, .. } => self.include_y_span(*y_min, *y_max),
}
}
}
}
fn finite_error_at(errors: Option<ErrorValuesRef<'_>>, index: usize) -> Option<(f64, f64)> {
errors
.and_then(|errors| errors.bounds_at(index))
.filter(|(lower, upper)| lower.is_finite() && upper.is_finite())
}
trait SeriesBoundsSource {
fn accumulate_bounds(&self, acc: &mut BoundsAccumulator) -> Result<()>;
}
impl<T: SeriesBoundsSource + ?Sized> SeriesBoundsSource for &T {
fn accumulate_bounds(&self, acc: &mut BoundsAccumulator) -> Result<()> {
(**self).accumulate_bounds(acc)
}
}
impl SeriesBoundsSource for PlotSeries {
fn accumulate_bounds(&self, acc: &mut BoundsAccumulator) -> Result<()> {
let attached = AttachedErrors::of(self);
match &self.series_type {
SeriesType::Line { x_data, y_data } | SeriesType::Scatter { x_data, y_data } => {
acc.add_points_with_errors(
&x_data.resolve_cow(0.0),
&y_data.resolve_cow(0.0),
attached.x.map(ErrorValuesRef::from),
attached.y.map(ErrorValuesRef::from),
);
}
SeriesType::Bar {
categories, values, ..
} => acc.add_bars(categories.len(), &values.resolve_cow(0.0)),
SeriesType::ErrorBars {
x_data,
y_data,
y_errors,
} => {
let y_errors = y_errors.resolve_cow(0.0);
acc.add_points_with_errors(
&x_data.resolve_cow(0.0),
&y_data.resolve_cow(0.0),
attached.x.map(ErrorValuesRef::from),
Some(effective_error_values(attached.y, &y_errors)),
);
}
SeriesType::ErrorBarsXY {
x_data,
y_data,
x_errors,
y_errors,
} => {
let x_errors = x_errors.resolve_cow(0.0);
let y_errors = y_errors.resolve_cow(0.0);
acc.add_points_with_errors(
&x_data.resolve_cow(0.0),
&y_data.resolve_cow(0.0),
Some(effective_error_values(attached.x, &x_errors)),
Some(effective_error_values(attached.y, &y_errors)),
);
}
SeriesType::Histogram { .. } => {
if let Ok(data) = self.series_type.histogram_data_at(0.0) {
acc.add_histogram(&data);
}
}
SeriesType::BoxPlot { data, config } => {
acc.add_box_plot(&data.resolve_cow(0.0), config)?
}
series_type => acc.add_computed_series(series_type),
}
Ok(())
}
}
impl ResolvedSeries<'_> {
fn accumulate_bounds_with(
&self,
acc: &mut BoundsAccumulator,
attached: AttachedErrors<'_>,
) -> Result<()> {
match self {
ResolvedSeries::Line { x, y } | ResolvedSeries::Scatter { x, y } => acc
.add_points_with_errors(
x,
y,
attached.x.map(ErrorValuesRef::from),
attached.y.map(ErrorValuesRef::from),
),
ResolvedSeries::Bar { categories, values } => acc.add_bars(categories.len(), values),
ResolvedSeries::ErrorBars { x, y, y_errors } => acc.add_points_with_errors(
x,
y,
attached.x.map(ErrorValuesRef::from),
Some(effective_error_values(attached.y, y_errors)),
),
ResolvedSeries::ErrorBarsXY {
x,
y,
x_errors,
y_errors,
} => acc.add_points_with_errors(
x,
y,
Some(effective_error_values(attached.x, x_errors)),
Some(effective_error_values(attached.y, y_errors)),
),
ResolvedSeries::Histogram { data } => acc.add_histogram(data),
ResolvedSeries::BoxPlot { data, config } => acc.add_box_plot(data, config)?,
ResolvedSeries::Other(series_type) => acc.add_computed_series(series_type),
}
Ok(())
}
}
impl SeriesBoundsSource for ResolvedSeries<'_> {
fn accumulate_bounds(&self, acc: &mut BoundsAccumulator) -> Result<()> {
self.accumulate_bounds_with(acc, AttachedErrors::default())
}
}
impl SeriesBoundsSource for (&PlotSeries, &ResolvedSeries<'_>) {
fn accumulate_bounds(&self, acc: &mut BoundsAccumulator) -> Result<()> {
let (series, resolved) = *self;
resolved.accumulate_bounds_with(acc, AttachedErrors::of(series))
}
}
impl Plot {
pub(super) fn sticky_edges_for_series(series_list: &[PlotSeries]) -> StickyEdges {
series_list
.iter()
.map(|series| sticky_edges_of(&series.series_type))
.reduce(StickyEdges::union)
.unwrap_or(StickyEdges::BY_CONSTRUCTION)
}
pub(super) fn sticky_edges(&self) -> StickyEdges {
Self::sticky_edges_for_series(&self.series_mgr.series)
}
fn normalize_degenerate_bounds(&self, bounds: (f64, f64, f64, f64)) -> (f64, f64, f64, f64) {
let (x_min, x_max) =
crate::axes::expand_degenerate_range(bounds.0, bounds.1, &self.layout.x_scale);
let (y_min, y_max) =
crate::axes::expand_degenerate_range(bounds.2, bounds.3, &self.layout.y_scale);
(x_min, x_max, y_min, y_max)
}
fn accumulate_series_bounds<S: SeriesBoundsSource>(
&self,
series: impl IntoIterator<Item = S>,
) -> Result<BoundsAccumulator> {
if let Some(err) = self.pending_ingestion_error() {
return Err(err);
}
let mut acc = BoundsAccumulator::new(self.layout.x_scale, self.layout.y_scale);
for source in series {
source.accumulate_bounds(&mut acc)?;
}
Ok(acc)
}
fn finish_bounds(&self, mut acc: BoundsAccumulator) -> Result<(f64, f64, f64, f64)> {
acc.include_annotations(&self.annotations);
if let Some(bounds) = acc.finite_bounds() {
return Ok(self.normalize_degenerate_bounds(bounds));
}
if let Some((axis, setter)) = acc.axis_with_no_representable_data() {
let shared = crate::axes::scale::LOG_SCALE_REQUIRES_POSITIVE;
return Err(PlottingError::InvalidInput(format!(
"Invalid {axis}-axis range: {shared} \
(no sample can be placed on the logarithmic {axis} axis because every \
{axis} value is zero or negative. Remove `.{setter}(AxisScale::Log)`, use \
`.{setter}(AxisScale::SymLog {{ linthresh }})`, or supply positive data.)"
)));
}
Ok(self.empty_cartesian_bounds())
}
pub(super) fn expand_bounds_with_annotations(
&self,
bounds: (f64, f64, f64, f64),
) -> (f64, f64, f64, f64) {
let mut acc =
BoundsAccumulator::from_bounds(bounds, self.layout.x_scale, self.layout.y_scale);
acc.include_annotations(&self.annotations);
self.normalize_degenerate_bounds(acc.bounds())
}
pub(super) fn calculate_data_bounds(&self) -> Result<(f64, f64, f64, f64)> {
self.calculate_data_bounds_for_series(&self.series_mgr.series)
}
pub(super) fn calculate_data_bounds_for_series(
&self,
series_list: &[PlotSeries],
) -> Result<(f64, f64, f64, f64)> {
let acc = self.accumulate_series_bounds(series_list)?;
self.finish_bounds(acc)
}
pub(super) fn calculate_data_bounds_from_resolved<'frame, 'data>(
&self,
resolved_series: impl IntoIterator<Item = &'frame ResolvedSeries<'data>>,
) -> Result<(f64, f64, f64, f64)>
where
'data: 'frame,
{
let acc = self.accumulate_series_bounds(resolved_series)?;
self.finish_bounds(acc)
}
pub(super) fn calculate_data_bounds_for_frame(
&self,
series_list: &[PlotSeries],
resolved_series: &[ResolvedSeries<'_>],
) -> Result<(f64, f64, f64, f64)> {
self.calculate_data_bounds_for_pairs(series_list.iter().zip(resolved_series))
}
pub(super) fn calculate_data_bounds_for_pairs<'frame, 'data>(
&self,
pairs: impl IntoIterator<Item = (&'frame PlotSeries, &'frame ResolvedSeries<'data>)>,
) -> Result<(f64, f64, f64, f64)>
where
'data: 'frame,
{
let acc = self.accumulate_series_bounds(pairs)?;
self.finish_bounds(acc)
}
pub(super) fn effective_frame_bounds(
&self,
resolved_series: &[ResolvedSeries<'_>],
) -> Result<(f64, f64, f64, f64)> {
if resolved_series.is_empty() {
return Ok(self.empty_cartesian_bounds());
}
self.calculate_data_bounds_for_frame(&self.series_mgr.series, resolved_series)
.map(|bounds| self.apply_manual_axis_limits(bounds))
}
pub(super) fn inset_bounds_from_resolved(
&self,
resolved: &ResolvedSeries<'_>,
) -> Result<(f64, f64, f64, f64)> {
let acc = self.accumulate_series_bounds(std::iter::once(resolved))?;
Ok(match acc.finite_bounds() {
Some(bounds) => self.normalize_degenerate_bounds(bounds),
None => self.empty_cartesian_bounds(),
})
}
}
#[cfg(test)]
mod bounds_tests {
use super::*;
fn assert_all_views_agree(plot: &Plot, what: &str) -> (f64, f64, f64, f64) {
let series = plot.snapshot_series(0.0);
let frame = plot.resolve_frame(0.0).expect("frame should resolve");
let all = plot
.calculate_data_bounds()
.unwrap_or_else(|e| panic!("{what}: whole-plot bounds failed: {e}"));
let listed = plot
.calculate_data_bounds_for_series(&series)
.unwrap_or_else(|e| panic!("{what}: per-series bounds failed: {e}"));
let resolved = plot
.calculate_data_bounds_from_resolved(&frame.series)
.unwrap_or_else(|e| panic!("{what}: resolved bounds failed: {e}"));
let paired = plot
.calculate_data_bounds_for_frame(&plot.series_mgr.series, &frame.series)
.unwrap_or_else(|e| panic!("{what}: paired bounds failed: {e}"));
assert_eq!(all, listed, "{what}: series-list view diverged");
assert_eq!(all, paired, "{what}: paired frame view diverged");
if plot
.series_mgr
.series
.iter()
.all(|s| s.x_errors.is_none() && s.y_errors.is_none())
{
assert_eq!(all, resolved, "{what}: resolved view diverged");
}
all
}
fn grid() -> Vec<Vec<f64>> {
vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]]
}
#[test]
fn every_plot_type_agrees_across_all_bounds_views() {
let x = [1.0, 2.0, 3.0];
let y = [10.0, 20.0, 30.0];
assert_all_views_agree(&Plot::new().line(&x, &y).into_plot(), "line");
assert_all_views_agree(&Plot::new().scatter(&x, &y).into_plot(), "scatter");
assert_all_views_agree(
&Plot::new().bar(&["a", "b"], &[1.0, 2.0]).into_plot(),
"bar",
);
assert_all_views_agree(
&Plot::new()
.histogram(&[1.0, 2.0, 2.0, 3.0, 4.0])
.into_plot(),
"histogram",
);
assert_all_views_agree(
&Plot::new().boxplot(&[1.0, 2.0, 3.0, 4.0]).into_plot(),
"boxplot",
);
assert_all_views_agree(
&Plot::new().error_bars(&x, &y, &[1.0, 1.0, 1.0]).into_plot(),
"error_bars",
);
assert_all_views_agree(
&Plot::new()
.error_bars_xy(&x, &y, &[0.5, 0.5, 0.5], &[1.0, 1.0, 1.0])
.into_plot(),
"error_bars_xy",
);
assert_all_views_agree(&Plot::new().heatmap(&grid()).into_plot(), "heatmap");
}
#[test]
fn annotations_reach_every_bounds_view() {
let plot = Plot::new()
.line(&[0.0, 1.0], &[0.0, 1.0])
.into_plot()
.axvspan(-5.0, 7.0);
let (x_min, x_max, ..) = assert_all_views_agree(&plot, "hspan-annotated line");
assert!(x_min <= -5.0, "hspan lower edge clipped: {x_min}");
assert!(x_max >= 7.0, "hspan upper edge clipped: {x_max}");
}
#[test]
fn attached_y_error_whiskers_are_inside_the_bounds() {
let plot = Plot::new()
.line(&[0.0, 1.0, 2.0], &[10.0, 10.0, 10.0])
.with_yerr(&[2.0, 2.0, 2.0])
.into_plot();
let (_, _, y_min, y_max) = plot
.calculate_data_bounds()
.expect("bounds should resolve for a line with attached y errors");
assert!(y_min <= 8.0, "lower whisker clipped: y_min = {y_min}");
assert!(y_max >= 12.0, "upper whisker clipped: y_max = {y_max}");
}
#[test]
fn attached_x_error_whiskers_are_inside_the_bounds() {
let plot = Plot::new()
.scatter(&[5.0], &[0.0])
.with_xerr(&[3.0])
.into_plot();
let (x_min, x_max, ..) = plot
.calculate_data_bounds()
.expect("bounds should resolve for a scatter with attached x errors");
assert!(x_min <= 2.0, "lower whisker clipped: x_min = {x_min}");
assert!(x_max >= 8.0, "upper whisker clipped: x_max = {x_max}");
}
#[test]
fn attached_asymmetric_errors_override_the_dedicated_series_values() {
let plot = Plot::new()
.error_bars(&[0.0], &[0.0], &[0.25])
.with_yerr_asymmetric(&[0.5], &[1.5])
.into_plot();
let (_, _, y_min, y_max) = plot
.calculate_data_bounds()
.expect("bounds should resolve for overridden error bars");
assert!(y_min <= -0.5, "override lower ignored: y_min = {y_min}");
assert!(y_max >= 1.5, "override upper ignored: y_max = {y_max}");
}
#[test]
fn attached_errors_are_folded_in_on_the_resolved_frame_path() {
let plot = Plot::new()
.line(&[0.0, 1.0], &[10.0, 10.0])
.with_yerr(&[4.0, 4.0])
.into_plot();
let frame = plot.resolve_frame(0.0).expect("frame should resolve");
let paired = plot
.calculate_data_bounds_for_frame(&plot.series_mgr.series, &frame.series)
.expect("paired bounds should resolve");
assert!(paired.2 <= 6.0, "lower whisker clipped: {}", paired.2);
assert!(paired.3 >= 14.0, "upper whisker clipped: {}", paired.3);
let effective = plot
.effective_frame_bounds(&frame.series)
.expect("frame bounds should resolve");
assert!(effective.2 <= 6.0, "lower whisker clipped: {}", effective.2);
assert!(
effective.3 >= 14.0,
"upper whisker clipped: {}",
effective.3
);
}
#[test]
fn insets_do_not_inherit_plot_level_annotations() {
let plot = Plot::new()
.line(&[0.0, 1.0], &[0.0, 1.0])
.into_plot()
.axvspan(-100.0, 100.0);
let frame = plot.resolve_frame(0.0).expect("frame should resolve");
let inset = plot
.inset_bounds_from_resolved(&frame.series[0])
.expect("inset bounds should resolve");
assert_eq!(inset, (0.0, 1.0, 0.0, 1.0));
}
#[test]
fn sticky_edges_are_declared_once_per_plot_type() {
let bars = Plot::new().bar(&["a"], &[1.0]).into_plot().sticky_edges();
assert!(bars.y_zero_baseline);
assert!(!bars.all_edges);
let heatmap = Plot::new().heatmap(&grid()).into_plot().sticky_edges();
assert!(heatmap.all_edges);
let line = Plot::new()
.line(&[0.0, 1.0], &[0.0, 1.0])
.into_plot()
.sticky_edges();
assert_eq!(line, StickyEdges::NONE);
let mixed = Plot::new()
.pie(&[1.0, 2.0])
.into_plot()
.line(&[0.0, 1.0], &[0.0, 1.0])
.into_plot()
.sticky_edges();
assert!(!mixed.by_construction);
}
#[test]
fn sticky_edges_of_a_seriesless_plot_pin_the_default_axes() {
let empty = Plot::new().sticky_edges();
assert_eq!(empty, StickyEdges::BY_CONSTRUCTION);
assert!(empty.by_construction);
let plot = Plot::new();
assert_eq!(
plot.apply_autoscale_margins((0.0, 1.0, 0.0, 1.0)),
(0.0, 1.0, 0.0, 1.0),
"an empty plot must not grow a margin band around nothing"
);
}
}