use crate::charts::boxplot::BoxStats;
use crate::colormap::Colormap;
use crate::decimate::DecimateMethod;
use crate::primitives::Color;
use crate::series::{Categories, Series};
use crate::theme::{LineStyle, Marker};
#[derive(Debug, Clone)]
pub enum Artist {
Line(LineArtist),
Scatter(ScatterArtist),
Bar(BarArtist),
Histogram(HistArtist),
FillBetween(FillBetweenArtist),
Step(StepArtist),
Stem(StemArtist),
BoxPlot(BoxPlotArtist),
ErrorBar(ErrorBarArtist),
Heatmap(HeatmapArtist),
Pie(PieArtist),
Violin(ViolinArtist),
Contour(ContourArtist),
Polar(PolarArtist),
Hexbin(HexbinArtist),
Waterfall(WaterfallArtist),
}
impl Artist {
pub fn label(&self) -> Option<&str> {
match self {
Artist::Line(a) => a.label.as_deref(),
Artist::Scatter(a) => a.label.as_deref(),
Artist::Bar(a) => a.label.as_deref(),
Artist::Histogram(a) => a.label.as_deref(),
Artist::FillBetween(a) => a.label.as_deref(),
Artist::Step(a) => a.label.as_deref(),
Artist::Stem(a) => a.label.as_deref(),
Artist::BoxPlot(a) => a.label.as_deref(),
Artist::ErrorBar(a) => a.label.as_deref(),
Artist::Heatmap(a) => a.label.as_deref(),
Artist::Pie(a) => a.label.as_deref(),
Artist::Violin(a) => a.label.as_deref(),
Artist::Contour(a) => a.label.as_deref(),
Artist::Polar(a) => a.label.as_deref(),
Artist::Hexbin(a) => a.label.as_deref(),
Artist::Waterfall(a) => a.label.as_deref(),
}
}
pub fn color(&self) -> Color {
match self {
Artist::Line(a) => a.color,
Artist::Scatter(a) => a.color,
Artist::Bar(a) => a.color,
Artist::Histogram(a) => a.color,
Artist::FillBetween(a) => a.color,
Artist::Step(a) => a.color,
Artist::Stem(a) => a.color,
Artist::BoxPlot(a) => a.color,
Artist::ErrorBar(a) => a.color,
Artist::Heatmap(a) => a.color,
Artist::Pie(a) => a.color,
Artist::Violin(a) => a.color,
Artist::Contour(a) => a.color,
Artist::Polar(a) => a.color,
Artist::Hexbin(a) => a.color,
Artist::Waterfall(a) => a.color,
}
}
pub fn data_bounds(&self) -> (f64, f64, f64, f64) {
match self {
Artist::Line(a) => a.data_bounds(),
Artist::Scatter(a) => a.data_bounds(),
Artist::Bar(a) => a.data_bounds(),
Artist::Histogram(a) => a.data_bounds(),
Artist::FillBetween(a) => a.data_bounds(),
Artist::Step(a) => a.data_bounds(),
Artist::Stem(a) => a.data_bounds(),
Artist::BoxPlot(a) => a.data_bounds(),
Artist::ErrorBar(a) => a.data_bounds(),
Artist::Heatmap(a) => a.data_bounds(),
Artist::Pie(a) => a.data_bounds(),
Artist::Violin(a) => a.data_bounds(),
Artist::Contour(a) => a.data_bounds(),
Artist::Polar(a) => a.data_bounds(),
Artist::Hexbin(a) => a.data_bounds(),
Artist::Waterfall(a) => a.data_bounds(),
}
}
}
fn series_bounds_or(series: &Series, fallback_min: f64, fallback_max: f64) -> (f64, f64) {
match series.bounds() {
Some((lo, hi)) => (lo, hi),
None => (fallback_min, fallback_max),
}
}
#[derive(Debug, Clone)]
pub struct LineArtist {
pub x: Series,
pub y: Series,
pub color: Color,
pub width: f64,
pub style: LineStyle,
pub label: Option<String>,
pub alpha: f64,
pub decimate: Option<(usize, DecimateMethod)>,
}
impl LineArtist {
pub fn data_bounds(&self) -> (f64, f64, f64, f64) {
let (xmin, xmax) = series_bounds_or(&self.x, 0.0, 1.0);
let (ymin, ymax) = series_bounds_or(&self.y, 0.0, 1.0);
(xmin, xmax, ymin, ymax)
}
}
#[derive(Debug, Clone)]
pub struct ScatterArtist {
pub x: Series,
pub y: Series,
pub color: Color,
pub marker: Marker,
pub size: f64,
pub label: Option<String>,
pub alpha: f64,
pub colors: Option<Vec<Color>>,
pub c: Option<Vec<f64>>,
pub cmap: Option<Colormap>,
}
impl ScatterArtist {
pub fn data_bounds(&self) -> (f64, f64, f64, f64) {
let (xmin, xmax) = series_bounds_or(&self.x, 0.0, 1.0);
let (ymin, ymax) = series_bounds_or(&self.y, 0.0, 1.0);
(xmin, xmax, ymin, ymax)
}
}
#[derive(Debug, Clone)]
pub struct BarArtist {
pub categories: Categories,
pub heights: Series,
pub color: Color,
pub label: Option<String>,
pub alpha: f64,
pub horizontal: bool,
pub bar_width: f64,
pub bottom: Option<Vec<f64>>,
pub offset: Option<Vec<f64>>,
}
impl BarArtist {
pub fn data_bounds(&self) -> (f64, f64, f64, f64) {
let n = self.categories.len() as f64;
let (height_min, height_max) = if let Some(ref bot) = self.bottom {
let mut vmin = f64::INFINITY;
let mut vmax = f64::NEG_INFINITY;
for i in 0..self.heights.len() {
let b = if i < bot.len() { bot[i] } else { 0.0 };
let h = self.heights.data[i];
let top = b + h;
vmin = vmin.min(b).min(top);
vmax = vmax.max(b).max(top);
}
if !vmin.is_finite() {
vmin = 0.0;
}
if !vmax.is_finite() {
vmax = 1.0;
}
(vmin.min(0.0), vmax)
} else {
let hmin = self.heights.min().unwrap_or(0.0).min(0.0);
let hmax = self.heights.max().unwrap_or(1.0);
(hmin, hmax)
};
let mut cat_min: f64 = -0.5;
let mut cat_max: f64 = if n > 0.0 { n - 0.5 } else { 0.5 };
if let Some(ref off) = self.offset {
let half_bar = self.bar_width * 0.5;
for i in 0..self.heights.len() {
let o = if i < off.len() { off[i] } else { 0.0 };
let center = i as f64 + o;
cat_min = cat_min.min(center - half_bar);
cat_max = cat_max.max(center + half_bar);
}
}
if self.horizontal {
(height_min, height_max, cat_min, cat_max)
} else {
(cat_min, cat_max, height_min, height_max)
}
}
}
#[derive(Debug, Clone)]
pub struct HistArtist {
pub data: Series,
pub bins: usize,
pub bin_edges: Vec<f64>,
pub counts: Vec<f64>,
pub color: Color,
pub label: Option<String>,
pub alpha: f64,
pub density: bool,
}
impl HistArtist {
pub fn data_bounds(&self) -> (f64, f64, f64, f64) {
if self.bin_edges.len() < 2 {
return (0.0, 1.0, 0.0, 1.0);
}
let xmin = self.bin_edges[0];
let xmax = self.bin_edges[self.bin_edges.len() - 1];
let ymax = self
.counts
.iter()
.copied()
.filter(|v| v.is_finite())
.fold(0.0_f64, f64::max);
let ymax = if ymax <= 0.0 { 1.0 } else { ymax };
(xmin, xmax, 0.0, ymax)
}
}
#[derive(Debug, Clone)]
pub struct FillBetweenArtist {
pub x: Series,
pub y1: Series,
pub y2: Series,
pub color: Color,
pub label: Option<String>,
pub alpha: f64,
}
impl FillBetweenArtist {
pub fn data_bounds(&self) -> (f64, f64, f64, f64) {
let (xmin, xmax) = series_bounds_or(&self.x, 0.0, 1.0);
let y1_min = self.y1.min();
let y2_min = self.y2.min();
let y1_max = self.y1.max();
let y2_max = self.y2.max();
let ymin = match (y1_min, y2_min) {
(Some(a), Some(b)) => a.min(b),
(Some(a), None) => a,
(None, Some(b)) => b,
(None, None) => 0.0,
};
let ymax = match (y1_max, y2_max) {
(Some(a), Some(b)) => a.max(b),
(Some(a), None) => a,
(None, Some(b)) => b,
(None, None) => 1.0,
};
(xmin, xmax, ymin, ymax)
}
}
#[derive(Debug, Clone)]
pub struct BoxPlotArtist {
pub stats: Vec<BoxStats>,
pub labels: Vec<String>,
pub color: Color,
pub label: Option<String>,
pub alpha: f64,
pub box_width: f64,
pub show_outliers: bool,
pub whisker_iq_factor: f64,
pub raw_data: Vec<Vec<f64>>,
}
impl BoxPlotArtist {
pub fn data_bounds(&self) -> (f64, f64, f64, f64) {
let n = self.stats.len();
if n == 0 {
return (0.0, 1.0, 0.0, 1.0);
}
let xmin = -0.5;
let xmax = n as f64 - 0.5;
let mut ymin = f64::INFINITY;
let mut ymax = f64::NEG_INFINITY;
for s in &self.stats {
ymin = ymin.min(s.whisker_low);
ymax = ymax.max(s.whisker_high);
for &o in &s.outliers {
ymin = ymin.min(o);
ymax = ymax.max(o);
}
}
if !ymin.is_finite() {
ymin = 0.0;
}
if !ymax.is_finite() {
ymax = 1.0;
}
(xmin, xmax, ymin, ymax)
}
}
#[derive(Debug, Clone)]
pub enum ErrorBarData {
Symmetric(Vec<f64>),
Asymmetric {
low: Vec<f64>,
high: Vec<f64>,
},
}
#[derive(Debug, Clone)]
pub struct ErrorBarArtist {
pub x: Series,
pub y: Series,
pub xerr: Option<ErrorBarData>,
pub yerr: Option<ErrorBarData>,
pub color: Color,
pub label: Option<String>,
pub cap_size: f64,
pub line_width: f64,
}
impl ErrorBarArtist {
pub fn data_bounds(&self) -> (f64, f64, f64, f64) {
let (mut xmin, mut xmax) = series_bounds_or(&self.x, 0.0, 1.0);
let (mut ymin, mut ymax) = series_bounds_or(&self.y, 0.0, 1.0);
if let Some(ref xerr) = self.xerr {
for i in 0..self.x.len() {
let xv = self.x.data[i];
let (lo, hi) = match xerr {
ErrorBarData::Symmetric(e) => (xv - e[i], xv + e[i]),
ErrorBarData::Asymmetric { low, high } => (xv - low[i], xv + high[i]),
};
xmin = xmin.min(lo);
xmax = xmax.max(hi);
}
}
if let Some(ref yerr) = self.yerr {
for i in 0..self.y.len() {
let yv = self.y.data[i];
let (lo, hi) = match yerr {
ErrorBarData::Symmetric(e) => (yv - e[i], yv + e[i]),
ErrorBarData::Asymmetric { low, high } => (yv - low[i], yv + high[i]),
};
ymin = ymin.min(lo);
ymax = ymax.max(hi);
}
}
(xmin, xmax, ymin, ymax)
}
}
#[derive(Debug, Clone)]
pub struct HeatmapArtist {
pub data: Vec<Vec<f64>>,
pub x_labels: Option<Vec<String>>,
pub y_labels: Option<Vec<String>>,
pub cmap: Colormap,
pub vmin: Option<f64>,
pub vmax: Option<f64>,
pub show_values: bool,
pub color: Color,
pub label: Option<String>,
pub show_colorbar: bool,
}
impl HeatmapArtist {
pub fn data_bounds(&self) -> (f64, f64, f64, f64) {
let nrows = self.data.len();
if nrows == 0 {
return (0.0, 1.0, 0.0, 1.0);
}
let ncols = self.data[0].len();
if ncols == 0 {
return (0.0, 1.0, 0.0, 1.0);
}
(0.0, ncols as f64, 0.0, nrows as f64)
}
}
#[derive(Debug, Clone)]
pub struct PieArtist {
pub sizes: Vec<f64>,
pub labels: Option<Vec<String>>,
pub colors: Option<Vec<Color>>,
pub explode: Option<Vec<f64>>,
pub autopct: bool,
pub start_angle: f64,
pub radius: f64,
pub label: Option<String>,
pub color: Color,
}
impl PieArtist {
pub fn data_bounds(&self) -> (f64, f64, f64, f64) {
let max_explode = self
.explode
.as_ref()
.map(|e| e.iter().copied().fold(0.0_f64, f64::max))
.unwrap_or(0.0);
let extent = self.radius * (1.0 + max_explode) + 0.1 * self.radius;
(-extent, extent, -extent, extent)
}
}
#[derive(Debug, Clone)]
pub struct ViolinArtist {
pub datasets: Vec<Vec<f64>>,
pub positions: Option<Vec<f64>>,
pub widths: f64,
pub show_median: bool,
pub show_quartiles: bool,
pub color: Color,
pub alpha: f64,
pub label: Option<String>,
pub bw_method: f64,
}
#[derive(Debug, Clone)]
pub struct ContourArtist {
pub x: Vec<f64>,
pub y: Vec<f64>,
pub z: Vec<Vec<f64>>,
pub levels: Option<Vec<f64>>,
pub filled: bool,
pub cmap: Colormap,
pub colors: Option<Vec<Color>>,
pub linewidths: f64,
pub label: Option<String>,
pub color: Color,
pub num_levels: usize,
}
impl ContourArtist {
pub fn data_bounds(&self) -> (f64, f64, f64, f64) {
if self.x.is_empty() || self.y.is_empty() {
return (0.0, 1.0, 0.0, 1.0);
}
let xmin = self.x.iter().copied().fold(f64::INFINITY, f64::min);
let xmax = self.x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let ymin = self.y.iter().copied().fold(f64::INFINITY, f64::min);
let ymax = self.y.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let (xmin, xmax) = if xmin.is_finite() && xmax.is_finite() {
(xmin, xmax)
} else {
(0.0, 1.0)
};
let (ymin, ymax) = if ymin.is_finite() && ymax.is_finite() {
(ymin, ymax)
} else {
(0.0, 1.0)
};
(xmin, xmax, ymin, ymax)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepWhere {
Pre,
Post,
Mid,
}
#[derive(Debug, Clone)]
pub struct StepArtist {
pub x: Series,
pub y: Series,
pub color: Color,
pub width: f64,
pub where_step: StepWhere,
pub label: Option<String>,
pub alpha: f64,
}
impl StepArtist {
pub fn data_bounds(&self) -> (f64, f64, f64, f64) {
let (xmin, xmax) = series_bounds_or(&self.x, 0.0, 1.0);
let (ymin, ymax) = series_bounds_or(&self.y, 0.0, 1.0);
(xmin, xmax, ymin, ymax)
}
}
#[derive(Debug, Clone)]
pub struct StemArtist {
pub x: Series,
pub y: Series,
pub color: Color,
pub line_width: f64,
pub marker_size: f64,
pub baseline: f64,
pub label: Option<String>,
pub alpha: f64,
}
impl StemArtist {
pub fn data_bounds(&self) -> (f64, f64, f64, f64) {
let (xmin, xmax) = series_bounds_or(&self.x, 0.0, 1.0);
let (ymin, ymax) = series_bounds_or(&self.y, 0.0, 1.0);
(xmin, xmax, ymin.min(self.baseline), ymax.max(self.baseline))
}
}
#[derive(Debug, Clone)]
pub struct PolarArtist {
pub theta: Vec<f64>,
pub r: Vec<f64>,
pub color: Color,
pub label: Option<String>,
pub alpha: f64,
pub linewidth: f64,
pub filled: bool,
pub marker: Option<Marker>,
}
#[derive(Debug, Clone)]
pub struct HexbinArtist {
pub x: Vec<f64>,
pub y: Vec<f64>,
pub gridsize: usize,
pub cmap: Colormap,
pub mincnt: usize,
pub alpha: f64,
pub color: Color,
pub label: Option<String>,
pub edgecolor: Option<Color>,
pub show_colorbar: bool,
}
impl HexbinArtist {
pub fn data_bounds(&self) -> (f64, f64, f64, f64) {
if self.x.is_empty() || self.y.is_empty() {
return (0.0, 1.0, 0.0, 1.0);
}
let mut xmin = f64::INFINITY;
let mut xmax = f64::NEG_INFINITY;
let mut ymin = f64::INFINITY;
let mut ymax = f64::NEG_INFINITY;
for &v in &self.x {
if v.is_finite() {
if v < xmin { xmin = v; }
if v > xmax { xmax = v; }
}
}
for &v in &self.y {
if v.is_finite() {
if v < ymin { ymin = v; }
if v > ymax { ymax = v; }
}
}
let (xmin, xmax) = if xmin.is_finite() && xmax.is_finite() {
(xmin, xmax)
} else {
(0.0, 1.0)
};
let (ymin, ymax) = if ymin.is_finite() && ymax.is_finite() {
(ymin, ymax)
} else {
(0.0, 1.0)
};
(xmin, xmax, ymin, ymax)
}
}
#[derive(Debug, Clone)]
pub struct WaterfallArtist {
pub categories: Categories,
pub values: Series,
pub total_indices: Vec<usize>,
pub increase_color: Color,
pub decrease_color: Color,
pub total_color: Color,
pub connector_lines: bool,
pub show_values: bool,
pub bar_width: f64,
pub label: Option<String>,
pub color: Color,
pub alpha: f64,
}
impl WaterfallArtist {
pub fn data_bounds(&self) -> (f64, f64, f64, f64) {
let n = self.categories.len() as f64;
if n == 0.0 {
return (0.0, 1.0, 0.0, 1.0);
}
let cat_min = -0.5;
let cat_max = n - 0.5;
let mut running = 0.0;
let mut y_min = 0.0_f64;
let mut y_max = 0.0_f64;
for i in 0..self.values.len() {
let prev = running;
if self.total_indices.contains(&i) {
running = self.values.data[i];
} else {
running += self.values.data[i];
}
if self.total_indices.contains(&i) {
y_min = y_min.min(0.0).min(running);
y_max = y_max.max(0.0).max(running);
} else {
y_min = y_min.min(prev).min(running);
y_max = y_max.max(prev).max(running);
}
}
y_min = y_min.min(0.0);
y_max = y_max.max(0.0);
if (y_max - y_min).abs() < f64::EPSILON {
y_max = y_min + 1.0;
}
(cat_min, cat_max, y_min, y_max)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_line() -> LineArtist {
LineArtist {
x: Series::new(vec![1.0, 2.0, 3.0]),
y: Series::new(vec![10.0, 20.0, 30.0]),
color: Color::TAB_BLUE,
width: 1.5,
style: LineStyle::Solid,
label: Some("line".to_string()),
alpha: 1.0,
decimate: None,
}
}
fn sample_scatter() -> ScatterArtist {
ScatterArtist {
x: Series::new(vec![0.0, 5.0, 10.0]),
y: Series::new(vec![-1.0, 0.0, 1.0]),
color: Color::TAB_ORANGE,
marker: Marker::Circle,
size: 6.0,
label: None,
alpha: 0.8,
colors: None,
c: None,
cmap: None,
}
}
fn sample_bar() -> BarArtist {
BarArtist {
categories: Categories::new(vec!["A".into(), "B".into(), "C".into()]),
heights: Series::new(vec![4.0, 7.0, 2.0]),
color: Color::TAB_GREEN,
label: Some("bars".to_string()),
alpha: 1.0,
horizontal: false,
bar_width: 0.8,
bottom: None,
offset: None,
}
}
fn sample_hist() -> HistArtist {
HistArtist {
data: Series::new(vec![1.0, 2.0, 2.5, 3.0, 3.5, 4.0]),
bins: 3,
bin_edges: vec![1.0, 2.0, 3.0, 4.0],
counts: vec![1.0, 2.0, 3.0],
color: Color::TAB_RED,
label: Some("hist".to_string()),
alpha: 0.7,
density: false,
}
}
fn sample_fill_between() -> FillBetweenArtist {
FillBetweenArtist {
x: Series::new(vec![0.0, 1.0, 2.0]),
y1: Series::new(vec![1.0, 3.0, 2.0]),
y2: Series::new(vec![0.0, 1.0, 0.5]),
color: Color::TAB_PURPLE,
label: Some("fill".to_string()),
alpha: 0.3,
}
}
#[test]
fn artist_label_returns_inner_label() {
let a = Artist::Line(sample_line());
assert_eq!(a.label(), Some("line"));
let a = Artist::Scatter(sample_scatter());
assert_eq!(a.label(), None);
let a = Artist::Bar(sample_bar());
assert_eq!(a.label(), Some("bars"));
let a = Artist::Histogram(sample_hist());
assert_eq!(a.label(), Some("hist"));
let a = Artist::FillBetween(sample_fill_between());
assert_eq!(a.label(), Some("fill"));
}
#[test]
fn artist_color_returns_inner_color() {
assert_eq!(Artist::Line(sample_line()).color(), Color::TAB_BLUE);
assert_eq!(Artist::Scatter(sample_scatter()).color(), Color::TAB_ORANGE);
assert_eq!(Artist::Bar(sample_bar()).color(), Color::TAB_GREEN);
assert_eq!(Artist::Histogram(sample_hist()).color(), Color::TAB_RED);
assert_eq!(
Artist::FillBetween(sample_fill_between()).color(),
Color::TAB_PURPLE
);
}
#[test]
fn artist_data_bounds_dispatches_correctly() {
let a = Artist::Line(sample_line());
assert_eq!(a.data_bounds(), (1.0, 3.0, 10.0, 30.0));
}
#[test]
fn line_data_bounds_basic() {
let a = sample_line();
assert_eq!(a.data_bounds(), (1.0, 3.0, 10.0, 30.0));
}
#[test]
fn line_data_bounds_empty_series() {
let a = LineArtist {
x: Series::new(vec![]),
y: Series::new(vec![]),
color: Color::BLACK,
width: 1.0,
style: LineStyle::Solid,
label: None,
alpha: 1.0,
decimate: None,
};
assert_eq!(a.data_bounds(), (0.0, 1.0, 0.0, 1.0));
}
#[test]
fn line_data_bounds_with_nan() {
let a = LineArtist {
x: Series::new(vec![f64::NAN, 2.0, 5.0]),
y: Series::new(vec![1.0, f64::NAN, 3.0]),
color: Color::BLACK,
width: 1.0,
style: LineStyle::Solid,
label: None,
alpha: 1.0,
decimate: None,
};
assert_eq!(a.data_bounds(), (2.0, 5.0, 1.0, 3.0));
}
#[test]
fn scatter_data_bounds_basic() {
let a = sample_scatter();
assert_eq!(a.data_bounds(), (0.0, 10.0, -1.0, 1.0));
}
#[test]
fn scatter_data_bounds_empty() {
let a = ScatterArtist {
x: Series::new(vec![]),
y: Series::new(vec![]),
color: Color::BLACK,
marker: Marker::Circle,
size: 6.0,
label: None,
alpha: 1.0,
colors: None,
c: None,
cmap: None,
};
assert_eq!(a.data_bounds(), (0.0, 1.0, 0.0, 1.0));
}
#[test]
fn bar_data_bounds_vertical() {
let a = sample_bar();
let (xmin, xmax, ymin, ymax) = a.data_bounds();
assert!((xmin - (-0.5)).abs() < f64::EPSILON);
assert!((xmax - 2.5).abs() < f64::EPSILON);
assert!((ymin - 0.0).abs() < f64::EPSILON);
assert!((ymax - 7.0).abs() < f64::EPSILON);
}
#[test]
fn bar_data_bounds_horizontal() {
let mut a = sample_bar();
a.horizontal = true;
let (xmin, xmax, ymin, ymax) = a.data_bounds();
assert!((xmin - 0.0).abs() < f64::EPSILON);
assert!((xmax - 7.0).abs() < f64::EPSILON);
assert!((ymin - (-0.5)).abs() < f64::EPSILON);
assert!((ymax - 2.5).abs() < f64::EPSILON);
}
#[test]
fn bar_data_bounds_negative_heights() {
let a = BarArtist {
categories: Categories::new(vec!["A".into(), "B".into()]),
heights: Series::new(vec![-3.0, 5.0]),
color: Color::BLACK,
label: None,
alpha: 1.0,
horizontal: false,
bar_width: 0.8,
bottom: None,
offset: None,
};
let (_, _, ymin, ymax) = a.data_bounds();
assert!((ymin - (-3.0)).abs() < f64::EPSILON);
assert!((ymax - 5.0).abs() < f64::EPSILON);
}
#[test]
fn bar_data_bounds_empty() {
let a = BarArtist {
categories: Categories::new(vec![]),
heights: Series::new(vec![]),
color: Color::BLACK,
label: None,
alpha: 1.0,
horizontal: false,
bar_width: 0.8,
bottom: None,
offset: None,
};
let (xmin, xmax, ymin, ymax) = a.data_bounds();
assert!((xmin - (-0.5)).abs() < f64::EPSILON);
assert!((xmax - 0.5).abs() < f64::EPSILON);
assert!((ymin - 0.0).abs() < f64::EPSILON);
assert!((ymax - 1.0).abs() < f64::EPSILON);
}
#[test]
fn bar_data_bounds_with_bottom() {
let a = BarArtist {
categories: Categories::new(vec!["A".into(), "B".into(), "C".into()]),
heights: Series::new(vec![3.0, 4.0, 2.0]),
color: Color::BLACK,
label: None,
alpha: 1.0,
horizontal: false,
bar_width: 0.8,
bottom: Some(vec![1.0, 2.0, 3.0]),
offset: None,
};
let (_, _, ymin, ymax) = a.data_bounds();
assert!((ymin - 0.0).abs() < f64::EPSILON);
assert!((ymax - 6.0).abs() < f64::EPSILON);
}
#[test]
fn bar_data_bounds_with_bottom_negative_base() {
let a = BarArtist {
categories: Categories::new(vec!["A".into(), "B".into()]),
heights: Series::new(vec![5.0, 3.0]),
color: Color::BLACK,
label: None,
alpha: 1.0,
horizontal: false,
bar_width: 0.8,
bottom: Some(vec![-2.0, 1.0]),
offset: None,
};
let (_, _, ymin, ymax) = a.data_bounds();
assert!((ymin - (-2.0)).abs() < f64::EPSILON);
assert!((ymax - 4.0).abs() < f64::EPSILON);
}
#[test]
fn bar_data_bounds_with_bottom_horizontal() {
let a = BarArtist {
categories: Categories::new(vec!["X".into(), "Y".into()]),
heights: Series::new(vec![4.0, 6.0]),
color: Color::BLACK,
label: None,
alpha: 1.0,
horizontal: true,
bar_width: 0.8,
bottom: Some(vec![1.0, 2.0]),
offset: None,
};
let (xmin, xmax, ymin, ymax) = a.data_bounds();
assert!((xmin - 0.0).abs() < f64::EPSILON);
assert!((xmax - 8.0).abs() < f64::EPSILON);
assert!((ymin - (-0.5)).abs() < f64::EPSILON);
assert!((ymax - 1.5).abs() < f64::EPSILON);
}
#[test]
fn bar_data_bounds_with_offset() {
let a = BarArtist {
categories: Categories::new(vec!["A".into(), "B".into()]),
heights: Series::new(vec![5.0, 3.0]),
color: Color::BLACK,
label: None,
alpha: 1.0,
horizontal: false,
bar_width: 0.4,
bottom: None,
offset: Some(vec![-0.2, -0.2]),
};
let (xmin, _xmax, _, _) = a.data_bounds();
assert!(xmin <= -0.4);
}
#[test]
fn bar_data_bounds_bottom_and_offset_combined() {
let a = BarArtist {
categories: Categories::new(vec!["A".into(), "B".into()]),
heights: Series::new(vec![3.0, 4.0]),
color: Color::BLACK,
label: None,
alpha: 1.0,
horizontal: false,
bar_width: 0.4,
bottom: Some(vec![2.0, 1.0]),
offset: Some(vec![0.2, 0.2]),
};
let (_, _, ymin, ymax) = a.data_bounds();
assert!((ymin - 0.0).abs() < f64::EPSILON);
assert!((ymax - 5.0).abs() < f64::EPSILON);
}
#[test]
fn bar_data_bounds_single_bar_with_bottom() {
let a = BarArtist {
categories: Categories::new(vec!["Solo".into()]),
heights: Series::new(vec![10.0]),
color: Color::BLACK,
label: None,
alpha: 1.0,
horizontal: false,
bar_width: 0.8,
bottom: Some(vec![5.0]),
offset: None,
};
let (_, _, ymin, ymax) = a.data_bounds();
assert!((ymin - 0.0).abs() < f64::EPSILON);
assert!((ymax - 15.0).abs() < f64::EPSILON);
}
#[test]
fn bar_data_bounds_zero_bottom() {
let a = BarArtist {
categories: Categories::new(vec!["A".into(), "B".into()]),
heights: Series::new(vec![3.0, 5.0]),
color: Color::BLACK,
label: None,
alpha: 1.0,
horizontal: false,
bar_width: 0.8,
bottom: Some(vec![0.0, 0.0]),
offset: None,
};
let (_, _, ymin, ymax) = a.data_bounds();
assert!((ymin - 0.0).abs() < f64::EPSILON);
assert!((ymax - 5.0).abs() < f64::EPSILON);
}
#[test]
fn bar_data_bounds_empty_with_bottom() {
let a = BarArtist {
categories: Categories::new(vec![]),
heights: Series::new(vec![]),
color: Color::BLACK,
label: None,
alpha: 1.0,
horizontal: false,
bar_width: 0.8,
bottom: Some(vec![]),
offset: None,
};
let (xmin, xmax, ymin, ymax) = a.data_bounds();
assert!((xmin - (-0.5)).abs() < f64::EPSILON);
assert!((xmax - 0.5).abs() < f64::EPSILON);
assert!((ymin - 0.0).abs() < f64::EPSILON);
assert!((ymax - 1.0).abs() < f64::EPSILON);
}
#[test]
fn bar_data_bounds_stacked_three_layers() {
let a = BarArtist {
categories: Categories::new(vec!["A".into()]),
heights: Series::new(vec![1.0]),
color: Color::BLACK,
label: None,
alpha: 1.0,
horizontal: false,
bar_width: 0.8,
bottom: Some(vec![5.0]),
offset: None,
};
let (_, _, ymin, ymax) = a.data_bounds();
assert!((ymin - 0.0).abs() < f64::EPSILON);
assert!((ymax - 6.0).abs() < f64::EPSILON);
}
#[test]
fn bar_builder_bottom_sets_field() {
let mut a = sample_bar();
a.bottom(vec![1.0, 2.0, 3.0]);
assert_eq!(a.bottom.as_ref().unwrap(), &vec![1.0, 2.0, 3.0]);
}
#[test]
fn bar_builder_offset_sets_field() {
let mut a = sample_bar();
a.offset(vec![0.1, 0.2, 0.3]);
assert_eq!(a.offset.as_ref().unwrap(), &vec![0.1, 0.2, 0.3]);
}
#[test]
fn hist_data_bounds_basic() {
let a = sample_hist();
let (xmin, xmax, ymin, ymax) = a.data_bounds();
assert!((xmin - 1.0).abs() < f64::EPSILON);
assert!((xmax - 4.0).abs() < f64::EPSILON);
assert!((ymin - 0.0).abs() < f64::EPSILON);
assert!((ymax - 3.0).abs() < f64::EPSILON);
}
#[test]
fn hist_data_bounds_empty_bins() {
let a = HistArtist {
data: Series::new(vec![]),
bins: 0,
bin_edges: vec![],
counts: vec![],
color: Color::BLACK,
label: None,
alpha: 1.0,
density: false,
};
assert_eq!(a.data_bounds(), (0.0, 1.0, 0.0, 1.0));
}
#[test]
fn hist_data_bounds_single_edge_pair() {
let a = HistArtist {
data: Series::new(vec![1.0]),
bins: 1,
bin_edges: vec![0.5, 1.5],
counts: vec![1.0],
color: Color::BLACK,
label: None,
alpha: 1.0,
density: false,
};
let (xmin, xmax, ymin, ymax) = a.data_bounds();
assert!((xmin - 0.5).abs() < f64::EPSILON);
assert!((xmax - 1.5).abs() < f64::EPSILON);
assert!((ymin - 0.0).abs() < f64::EPSILON);
assert!((ymax - 1.0).abs() < f64::EPSILON);
}
#[test]
fn hist_data_bounds_all_zero_counts() {
let a = HistArtist {
data: Series::new(vec![]),
bins: 2,
bin_edges: vec![0.0, 1.0, 2.0],
counts: vec![0.0, 0.0],
color: Color::BLACK,
label: None,
alpha: 1.0,
density: false,
};
let (_, _, _, ymax) = a.data_bounds();
assert!((ymax - 1.0).abs() < f64::EPSILON);
}
#[test]
fn fill_between_data_bounds_basic() {
let a = sample_fill_between();
let (xmin, xmax, ymin, ymax) = a.data_bounds();
assert!((xmin - 0.0).abs() < f64::EPSILON);
assert!((xmax - 2.0).abs() < f64::EPSILON);
assert!((ymin - 0.0).abs() < f64::EPSILON);
assert!((ymax - 3.0).abs() < f64::EPSILON);
}
#[test]
fn fill_between_data_bounds_empty() {
let a = FillBetweenArtist {
x: Series::new(vec![]),
y1: Series::new(vec![]),
y2: Series::new(vec![]),
color: Color::BLACK,
label: None,
alpha: 1.0,
};
assert_eq!(a.data_bounds(), (0.0, 1.0, 0.0, 1.0));
}
#[test]
fn fill_between_data_bounds_y2_extends_beyond_y1() {
let a = FillBetweenArtist {
x: Series::new(vec![0.0, 1.0]),
y1: Series::new(vec![1.0, 2.0]),
y2: Series::new(vec![-5.0, 10.0]),
color: Color::BLACK,
label: None,
alpha: 1.0,
};
let (_, _, ymin, ymax) = a.data_bounds();
assert!((ymin - (-5.0)).abs() < f64::EPSILON);
assert!((ymax - 10.0).abs() < f64::EPSILON);
}
#[test]
fn fill_between_data_bounds_one_series_empty() {
let a = FillBetweenArtist {
x: Series::new(vec![0.0, 1.0]),
y1: Series::new(vec![2.0, 8.0]),
y2: Series::new(vec![]),
color: Color::BLACK,
label: None,
alpha: 1.0,
};
let (_, _, ymin, ymax) = a.data_bounds();
assert!((ymin - 2.0).abs() < f64::EPSILON);
assert!((ymax - 8.0).abs() < f64::EPSILON);
}
}