#[derive(Debug, Clone)]
pub struct Series {
pub data: Vec<f64>,
}
impl Series {
pub fn new(data: Vec<f64>) -> Self {
Self { data }
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn min(&self) -> Option<f64> {
self.data
.iter()
.copied()
.filter(|v| v.is_finite())
.reduce(f64::min)
}
pub fn max(&self) -> Option<f64> {
self.data
.iter()
.copied()
.filter(|v| v.is_finite())
.reduce(f64::max)
}
pub fn bounds(&self) -> Option<(f64, f64)> {
Some((self.min()?, self.max()?))
}
}
pub trait IntoSeries {
fn into_series(self) -> Series;
}
impl IntoSeries for Vec<f64> {
fn into_series(self) -> Series {
Series::new(self)
}
}
impl IntoSeries for &[f64] {
fn into_series(self) -> Series {
Series::new(self.to_vec())
}
}
impl IntoSeries for &Vec<f64> {
fn into_series(self) -> Series {
Series::new(self.clone())
}
}
impl<const N: usize> IntoSeries for [f64; N] {
fn into_series(self) -> Series {
Series::new(self.to_vec())
}
}
impl<const N: usize> IntoSeries for &[f64; N] {
fn into_series(self) -> Series {
Series::new(self.to_vec())
}
}
impl IntoSeries for Series {
fn into_series(self) -> Series {
self
}
}
impl IntoSeries for std::ops::Range<i32> {
fn into_series(self) -> Series {
Series::new(self.map(|v| v as f64).collect())
}
}
impl IntoSeries for Vec<i32> {
fn into_series(self) -> Series {
Series::new(self.into_iter().map(|v| v as f64).collect())
}
}
impl IntoSeries for &[i32] {
fn into_series(self) -> Series {
Series::new(self.iter().map(|&v| v as f64).collect())
}
}
#[cfg(feature = "ndarray")]
mod ndarray_impls {
use super::{IntoSeries, Series};
use ndarray::{Array1, ArrayView1};
impl IntoSeries for &Array1<f64> {
fn into_series(self) -> Series {
match self.as_slice() {
Some(slice) => Series::new(slice.to_vec()),
None => Series::new(self.iter().copied().collect()),
}
}
}
impl IntoSeries for Array1<f64> {
fn into_series(self) -> Series {
if self.is_standard_layout() {
let len = self.len();
let (vec, offset) = self.into_raw_vec_and_offset();
let offset = offset.unwrap_or(0);
if offset == 0 && vec.len() == len {
Series::new(vec)
} else {
Series::new(vec[offset..offset + len].to_vec())
}
} else {
Series::new(self.iter().copied().collect())
}
}
}
impl IntoSeries for ArrayView1<'_, f64> {
fn into_series(self) -> Series {
match self.as_slice() {
Some(slice) => Series::new(slice.to_vec()),
None => Series::new(self.iter().copied().collect()),
}
}
}
macro_rules! impl_into_series_ndarray_cast {
($t:ty) => {
impl IntoSeries for &Array1<$t> {
fn into_series(self) -> Series {
Series::new(self.iter().map(|&v| v as f64).collect())
}
}
impl IntoSeries for Array1<$t> {
fn into_series(self) -> Series {
Series::new(self.iter().map(|&v| v as f64).collect())
}
}
impl IntoSeries for ArrayView1<'_, $t> {
fn into_series(self) -> Series {
Series::new(self.iter().map(|&v| v as f64).collect())
}
}
};
}
impl_into_series_ndarray_cast!(f32);
impl_into_series_ndarray_cast!(i32);
impl_into_series_ndarray_cast!(i64);
}
#[cfg(feature = "polars")]
mod polars_impls {
use super::{Categories, IntoCategories, IntoSeries, Series};
use polars::prelude::*;
fn extract_numeric(series: &polars::prelude::Series) -> Vec<f64> {
let ca = series
.cast(&DataType::Float64)
.unwrap_or_else(|_| {
panic!(
"plotkit-polars: cannot cast series {:?} (dtype {:?}) to Float64",
series.name(),
series.dtype()
)
});
let ca = ca
.f64()
.expect("cast to Float64 always yields f64 chunked array");
ca.into_iter().map(|opt| opt.unwrap_or(f64::NAN)).collect()
}
impl IntoSeries for &polars::prelude::Series {
fn into_series(self) -> Series {
Series::new(extract_numeric(self))
}
}
impl IntoSeries for polars::prelude::Series {
fn into_series(self) -> Series {
(&self).into_series()
}
}
impl IntoCategories for &polars::prelude::Series {
fn into_categories(self) -> Categories {
let ca = self.str().unwrap_or_else(|_| {
panic!(
"plotkit-polars: series {:?} (dtype {:?}) is not a string type",
self.name(),
self.dtype()
)
});
let labels: Vec<String> = ca
.into_iter()
.map(|opt| opt.unwrap_or("null").to_owned())
.collect();
Categories::new(labels)
}
}
}
impl IntoSeries for Vec<f32> {
fn into_series(self) -> Series {
Series::new(self.into_iter().map(|v| v as f64).collect())
}
}
impl IntoSeries for &[f32] {
fn into_series(self) -> Series {
Series::new(self.iter().map(|&v| v as f64).collect())
}
}
#[derive(Debug, Clone)]
pub struct Categories {
pub labels: Vec<String>,
}
impl Categories {
pub fn new(labels: Vec<String>) -> Self {
Self { labels }
}
pub fn len(&self) -> usize {
self.labels.len()
}
pub fn is_empty(&self) -> bool {
self.labels.is_empty()
}
}
pub trait IntoCategories {
fn into_categories(self) -> Categories;
}
impl IntoCategories for &[&str] {
fn into_categories(self) -> Categories {
Categories::new(self.iter().map(|s| (*s).to_owned()).collect())
}
}
impl IntoCategories for Vec<String> {
fn into_categories(self) -> Categories {
Categories::new(self)
}
}
impl IntoCategories for &[String] {
fn into_categories(self) -> Categories {
Categories::new(self.to_vec())
}
}
impl IntoCategories for Vec<&str> {
fn into_categories(self) -> Categories {
Categories::new(self.into_iter().map(|s| s.to_owned()).collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn series_from_vec_f64() {
let s = vec![1.0, 2.0, 3.0].into_series();
assert_eq!(s.len(), 3);
assert_eq!(s.data, vec![1.0, 2.0, 3.0]);
}
#[test]
fn series_from_slice_f64() {
let data: &[f64] = &[4.0, 5.0];
let s = data.into_series();
assert_eq!(s.data, vec![4.0, 5.0]);
}
#[test]
fn series_from_vec_ref() {
let v = vec![1.0, 2.0];
let s = (&v).into_series();
assert_eq!(s.data, vec![1.0, 2.0]);
}
#[test]
fn series_from_array() {
let s = [10.0, 20.0, 30.0].into_series();
assert_eq!(s.data, vec![10.0, 20.0, 30.0]);
}
#[test]
fn series_from_array_ref() {
let arr = [7.0, 8.0];
let s = (&arr).into_series();
assert_eq!(s.data, vec![7.0, 8.0]);
}
#[test]
fn series_identity() {
let original = Series::new(vec![1.0]);
let s = original.into_series();
assert_eq!(s.data, vec![1.0]);
}
#[test]
fn series_from_range() {
let s = (0..4).into_series();
assert_eq!(s.data, vec![0.0, 1.0, 2.0, 3.0]);
}
#[test]
fn series_from_vec_i32() {
let s = vec![1i32, 2, 3].into_series();
assert_eq!(s.data, vec![1.0, 2.0, 3.0]);
}
#[test]
fn series_from_slice_i32() {
let data: &[i32] = &[10, 20];
let s = data.into_series();
assert_eq!(s.data, vec![10.0, 20.0]);
}
#[test]
fn series_from_vec_f32() {
let s = vec![1.5f32, 2.5].into_series();
assert_eq!(s.data, vec![1.5f64, 2.5]);
}
#[test]
fn series_from_slice_f32() {
let data: &[f32] = &[0.1, 0.2];
let s = data.into_series();
assert_eq!(s.len(), 2);
}
#[test]
fn series_empty() {
let s = Series::new(vec![]);
assert!(s.is_empty());
assert_eq!(s.min(), None);
assert_eq!(s.max(), None);
assert_eq!(s.bounds(), None);
}
#[test]
fn series_min_max_bounds() {
let s = vec![3.0, 1.0, 4.0, 1.5, 9.0].into_series();
assert_eq!(s.min(), Some(1.0));
assert_eq!(s.max(), Some(9.0));
assert_eq!(s.bounds(), Some((1.0, 9.0)));
}
#[test]
fn series_min_max_ignores_nan() {
let s = vec![f64::NAN, 2.0, f64::INFINITY, 1.0, f64::NEG_INFINITY].into_series();
assert_eq!(s.min(), Some(1.0));
assert_eq!(s.max(), Some(2.0));
}
#[test]
fn categories_from_str_slice() {
let cats: &[&str] = &["a", "b", "c"];
let c = cats.into_categories();
assert_eq!(c.labels, vec!["a", "b", "c"]);
}
#[test]
fn categories_from_vec_string() {
let c = vec!["x".to_string(), "y".to_string()].into_categories();
assert_eq!(c.labels, vec!["x", "y"]);
}
#[test]
fn categories_from_string_slice() {
let v = vec!["p".to_string(), "q".to_string()];
let c = v.as_slice().into_categories();
assert_eq!(c.labels, vec!["p", "q"]);
}
#[test]
fn categories_from_vec_str_ref() {
let c = vec!["foo", "bar"].into_categories();
assert_eq!(c.labels, vec!["foo", "bar"]);
assert_eq!(c.len(), 2);
assert!(!c.is_empty());
}
}