use crate::charts::boxplot::BoxStats;
use crate::colormap::Colormap;
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),
}
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(),
}
}
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,
}
}
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(),
}
}
}
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>>,
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,
}
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)
}
}
#[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>,
}
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, 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))
}
}
#[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,
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,
}
}
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,
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,
};
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);
}
}