use crate::data::{IntoSeries, Series};
use crate::mark::Categories;
use crate::render::Color;
#[derive(Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Bars<'a> {
pub(crate) placement: Placement<'a>,
pub(crate) values: Series<'a>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub(crate) base: Option<Series<'a>>,
pub(crate) color: Option<Color>,
pub(crate) label: Option<String>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub(crate) color_by: Option<Categories>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "std::ops::Not::not")
)]
pub(crate) horizontal: bool,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub(crate) enum Placement<'a> {
Bands(Vec<String>),
Spans {
start: f64,
width: f64,
},
At {
x: Series<'a>,
width: f64,
},
Intervals {
starts: Series<'a>,
ends: Series<'a>,
},
}
impl<'a> Bars<'a> {
pub fn new(
categories: impl IntoIterator<Item = impl Into<String>>,
values: impl IntoSeries<'a>,
) -> Bars<'a> {
let categories: Vec<String> = categories.into_iter().map(Into::into).collect();
let values = values.into_series();
let bars = Bars {
placement: Placement::Bands(categories),
values,
base: None,
color: None,
label: None,
color_by: None,
horizontal: false,
};
bars.validate()
.expect("Bars::new requires one category per value");
bars
}
pub fn spans(start: f64, width: f64, values: impl IntoSeries<'a>) -> Bars<'a> {
let bars = Bars {
placement: Placement::Spans { start, width },
values: values.into_series(),
base: None,
color: None,
label: None,
color_by: None,
horizontal: false,
};
bars.validate()
.expect("Bars::spans requires a finite start and a positive width");
bars
}
pub fn at(x: impl IntoSeries<'a>, width: f64, values: impl IntoSeries<'a>) -> Bars<'a> {
let bars = Bars {
placement: Placement::At {
x: x.into_series(),
width,
},
values: values.into_series(),
base: None,
color: None,
label: None,
color_by: None,
horizontal: false,
};
bars.validate()
.expect("Bars::at requires one position per value and a finite positive width");
bars
}
pub fn intervals(
starts: impl IntoSeries<'a>,
ends: impl IntoSeries<'a>,
values: impl IntoSeries<'a>,
) -> Bars<'a> {
let bars = Bars {
placement: Placement::Intervals {
starts: starts.into_series(),
ends: ends.into_series(),
},
values: values.into_series(),
base: None,
color: None,
label: None,
color_by: None,
horizontal: false,
};
bars.validate().expect(
"Bars::intervals requires one start and one end per value, each start below its end",
);
bars
}
#[must_use]
pub fn base(mut self, base: impl IntoSeries<'a>) -> Bars<'a> {
self.base = Some(base.into_series());
self.validate()
.expect("Bars::base requires one base per value");
self
}
#[must_use]
pub fn horizontal(mut self) -> Bars<'a> {
self.horizontal = true;
self
}
#[must_use]
pub fn color(mut self, color: Color) -> Bars<'a> {
self.color = Some(color);
self
}
#[must_use]
pub fn label(mut self, label: impl Into<String>) -> Bars<'a> {
self.label = Some(label.into());
self
}
#[must_use]
pub fn color_by(mut self, groups: impl IntoIterator<Item = impl Into<String>>) -> Bars<'a> {
self.color_by = Some(Categories::new(groups));
self.validate()
.expect("Bars::color_by requires one group per bar");
self
}
pub(crate) fn validate(&self) -> crate::Result<()> {
match &self.placement {
Placement::Bands(categories) => {
super::pair(
"Bars: categories and values",
categories.len(),
self.values.len(),
)?;
}
Placement::Spans { start, width }
if !(start.is_finite() && width.is_finite() && *width > 0.0) =>
{
return Err(crate::Error::InvalidParameter {
detail: "Bars spans need a finite start and finite positive width",
});
}
Placement::Spans { .. } => {}
Placement::At { x, width } => {
super::pair("Bars: x and values", x.len(), self.values.len())?;
if !(width.is_finite() && *width > 0.0) {
return Err(crate::Error::InvalidParameter {
detail: "Bars positions need a finite positive width",
});
}
}
Placement::Intervals { starts, ends } => {
super::pair("Bars: starts and values", starts.len(), self.values.len())?;
super::pair("Bars: ends and values", ends.len(), self.values.len())?;
let reversed = starts
.as_slice()
.iter()
.zip(ends.as_slice())
.any(|(start, end)| start.is_finite() && end.is_finite() && start >= end);
if reversed {
return Err(crate::Error::InvalidParameter {
detail: "Bars intervals need each start below its end",
});
}
}
}
if let Some(base) = &self.base {
super::pair("Bars: base and values", base.len(), self.values.len())?;
}
if let Some(groups) = &self.color_by {
super::pair("Bars: color_by and values", groups.len(), self.values.len())?;
}
Ok(())
}
pub fn into_owned(self) -> Bars<'static> {
Bars {
placement: match self.placement {
Placement::Bands(categories) => Placement::Bands(categories),
Placement::Spans { start, width } => Placement::Spans { start, width },
Placement::At { x, width } => Placement::At {
x: x.into_owned(),
width,
},
Placement::Intervals { starts, ends } => Placement::Intervals {
starts: starts.into_owned(),
ends: ends.into_owned(),
},
},
values: self.values.into_owned(),
base: self.base.map(Series::into_owned),
color: self.color,
label: self.label,
color_by: self.color_by,
horizontal: self.horizontal,
}
}
}
impl std::fmt::Debug for Bars<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Bars")
.field("bars", &self.values.len())
.field("based", &self.base.is_some())
.field("horizontal", &self.horizontal)
.field("color", &self.color)
.finish()
}
}
#[cfg(test)]
#[path = "tests/bars_tests.rs"]
mod tests;