use std::borrow::Cow;
use crate::data::{IntoSeries, Series};
use crate::mark::Categories;
use crate::scale::Colormap;
use crate::stat::Reducer;
#[derive(Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Cells<'a> {
pub(crate) columns: usize,
pub(crate) values: Series<'a>,
pub(crate) extents: Option<((f64, f64), (f64, f64))>,
pub(crate) colormap: Colormap,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub(crate) rgb: Option<Cow<'a, [(u8, u8, u8)]>>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub(crate) classes: Option<Categories>,
#[cfg_attr(
feature = "serde",
serde(default = "default_reduce", skip_serializing_if = "is_default_reduce")
)]
pub(crate) reduce: Reducer,
}
#[cfg(feature = "serde")]
fn default_reduce() -> Reducer {
Reducer::Mean
}
#[cfg(feature = "serde")]
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_default_reduce(reduce: &Reducer) -> bool {
*reduce == Reducer::Mean
}
impl<'a> Cells<'a> {
pub fn matrix(columns: usize, values: impl IntoSeries<'a>) -> Cells<'a> {
Cells::try_matrix(columns, values)
.expect("Cells::matrix requires columns to divide the value count evenly")
}
pub fn try_matrix(columns: usize, values: impl IntoSeries<'a>) -> crate::Result<Cells<'a>> {
let values = values.into_series();
let cells = Cells {
columns,
values,
extents: None,
colormap: Colormap::DEFAULT,
rgb: None,
classes: None,
reduce: Reducer::Mean,
};
cells.validate()?;
Ok(cells)
}
pub fn rgb(columns: usize, pixels: impl Into<Cow<'a, [(u8, u8, u8)]>>) -> Cells<'a> {
Cells::try_rgb(columns, pixels)
.expect("Cells::rgb requires columns to divide the pixel count evenly")
}
pub fn try_rgb(
columns: usize,
pixels: impl Into<Cow<'a, [(u8, u8, u8)]>>,
) -> crate::Result<Cells<'a>> {
let cells = Cells {
columns,
values: Vec::<f64>::new().into_series(),
extents: None,
colormap: Colormap::DEFAULT,
rgb: Some(pixels.into()),
classes: None,
reduce: Reducer::Mean,
};
cells.validate()?;
Ok(cells)
}
pub fn classes(
columns: usize,
labels: impl IntoIterator<Item = impl Into<String>>,
) -> Cells<'a> {
Cells::try_classes(columns, labels)
.expect("Cells::classes requires columns to divide the label count evenly")
}
pub fn try_classes(
columns: usize,
labels: impl IntoIterator<Item = impl Into<String>>,
) -> crate::Result<Cells<'a>> {
let cells = Cells {
columns,
values: Vec::<f64>::new().into_series(),
extents: None,
colormap: Colormap::DEFAULT,
rgb: None,
classes: Some(Categories::new(labels)),
reduce: Reducer::Mean,
};
cells.validate()?;
Ok(cells)
}
#[must_use]
pub fn extents(self, x: (f64, f64), y: (f64, f64)) -> Cells<'a> {
self.try_extents(x, y)
.expect("Cells::extents requires finite, non-empty bounds")
}
pub fn try_extents(mut self, x: (f64, f64), y: (f64, f64)) -> crate::Result<Cells<'a>> {
self.extents = Some((x, y));
self.validate()?;
Ok(self)
}
#[must_use]
pub fn colormap(mut self, colormap: Colormap) -> Cells<'a> {
self.colormap = colormap;
self
}
#[must_use]
pub fn reduce(mut self, reducer: Reducer) -> Cells<'a> {
self.reduce = reducer;
self
}
pub(crate) fn rows(&self) -> usize {
let count = if let Some(classes) = &self.classes {
classes.len()
} else if let Some(pixels) = &self.rgb {
pixels.len()
} else {
self.values.len()
};
count / self.columns.max(1)
}
pub(crate) fn validate(&self) -> crate::Result<()> {
if self.columns == 0 {
return Err(crate::Error::EmptyDimension {
what: "Cells columns",
});
}
if !self.values.len().is_multiple_of(self.columns) {
return Err(crate::Error::NonRectangular {
mark: "Cells",
shape: (self.values.len(), self.columns),
});
}
if let Some(pixels) = &self.rgb
&& !pixels.len().is_multiple_of(self.columns)
{
return Err(crate::Error::NonRectangular {
mark: "Cells",
shape: (pixels.len(), self.columns),
});
}
if let Some(classes) = &self.classes
&& !classes.len().is_multiple_of(self.columns)
{
return Err(crate::Error::NonRectangular {
mark: "Cells",
shape: (classes.len(), self.columns),
});
}
let channels = usize::from(!self.values.is_empty())
+ usize::from(self.rgb.is_some())
+ usize::from(self.classes.is_some());
if channels > 1 {
return Err(crate::Error::InvalidParameter {
detail: "Cells carries exactly one of values, rgb pixels, or classes",
});
}
self.colormap.validate()?;
if let Reducer::Percentile(position) = self.reduce
&& !(0.0..=1.0).contains(&position)
{
return Err(crate::Error::InvalidParameter {
detail: "a Cells percentile reducer needs a position in [0, 1]",
});
}
if let Some((x, y)) = self.extents {
if !(x.0.is_finite() && x.1.is_finite() && y.0.is_finite() && y.1.is_finite()) {
return Err(crate::Error::InvalidParameter {
detail: "Cells extents must be finite",
});
}
if x.0 == x.1 || y.0 == y.1 {
return Err(crate::Error::InvalidParameter {
detail: "Cells extents must be non-empty",
});
}
}
Ok(())
}
pub fn into_owned(self) -> Cells<'static> {
Cells {
columns: self.columns,
values: self.values.into_owned(),
extents: self.extents,
colormap: self.colormap,
rgb: self.rgb.map(|pixels| Cow::Owned(pixels.into_owned())),
classes: self.classes,
reduce: self.reduce,
}
}
}
impl std::fmt::Debug for Cells<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Cells")
.field("columns", &self.columns)
.field("rows", &self.rows())
.finish_non_exhaustive()
}
}
#[cfg(test)]
#[path = "tests/cells_tests.rs"]
mod tests;