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),
}
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(),
}
}
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,
}
}
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(),
}
}
}
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,
}
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>>,
}
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,
}
impl BarArtist {
pub fn data_bounds(&self) -> (f64, f64, f64, f64) {
let n = self.categories.len() as f64;
let height_min = self.heights.min().unwrap_or(0.0).min(0.0);
let height_max = self.heights.max().unwrap_or(1.0);
let cat_min = -0.5;
let cat_max = if n > 0.0 { n - 0.5 } else { 0.5 };
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)
}
}
#[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,
}
}
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,
}
}
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,
}
}
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,
};
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,
};
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,
};
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,
};
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,
};
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 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);
}
}