use std::ops::Range;
use std::sync::Arc;
use crate::data::{IntoSeries, Series};
use crate::mark::Categories;
use crate::render::Color;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum LineStyle {
#[default]
Pixels,
Corners,
}
#[derive(Clone)]
pub struct Line<'a> {
pub(crate) source: Source<'a>,
pub(crate) color: Option<Color>,
pub(crate) label: Option<String>,
pub(crate) style: LineStyle,
pub(crate) color_by: Option<Categories>,
}
#[derive(Clone)]
pub(crate) enum Source<'a> {
Points {
x: Option<Series<'a>>,
y: Series<'a>,
},
Function {
domain: (f64, f64),
function: Arc<dyn Fn(f64) -> f64 + Send + Sync>,
},
}
impl<'a> Line<'a> {
pub fn y(values: impl IntoSeries<'a>) -> Line<'a> {
Line {
source: Source::Points {
x: None,
y: values.into_series(),
},
color: None,
label: None,
style: LineStyle::Pixels,
color_by: None,
}
}
pub fn xy(x: impl IntoSeries<'a>, y: impl IntoSeries<'a>) -> Line<'a> {
let x = x.into_series();
let y = y.into_series();
let line = Line {
source: Source::Points { x: Some(x), y },
color: None,
label: None,
style: LineStyle::Pixels,
color_by: None,
};
line.validate()
.expect("Line::xy requires series of equal length");
line
}
pub fn function(
domain: Range<f64>,
function: impl Fn(f64) -> f64 + Send + Sync + 'static,
) -> Line<'a> {
let line = Line {
source: Source::Function {
domain: (domain.start, domain.end),
function: Arc::new(function),
},
color: None,
label: None,
style: LineStyle::Pixels,
color_by: None,
};
line.validate()
.expect("Line::function requires a finite, non-empty domain");
line
}
#[must_use]
pub fn style(mut self, style: LineStyle) -> Line<'a> {
self.style = style;
self
}
#[must_use]
pub fn color(mut self, color: Color) -> Line<'a> {
self.color = Some(color);
self
}
#[must_use]
pub fn label(mut self, label: impl Into<String>) -> Line<'a> {
self.label = Some(label.into());
self
}
#[must_use]
pub fn color_by(mut self, categories: impl IntoIterator<Item = impl Into<String>>) -> Line<'a> {
self.color_by = Some(Categories::new(categories));
self.validate()
.expect("Line::color_by requires one category per point and point data");
self
}
pub(crate) fn validate(&self) -> crate::Result<()> {
match &self.source {
Source::Points { x, y } => {
if let Some(x) = x {
super::pair("Line: x and y", x.len(), y.len())?;
}
if let Some(categories) = &self.color_by {
super::pair("Line: color_by and y", categories.len(), y.len())?;
}
}
Source::Function { domain, .. } => {
if !(domain.0.is_finite() && domain.1.is_finite() && domain.0 < domain.1) {
return Err(crate::Error::InvalidParameter {
detail: "a function Line needs a finite non-empty domain",
});
}
if self.color_by.is_some() {
return Err(crate::Error::InvalidParameter {
detail: "a function Line cannot have a color_by channel",
});
}
}
}
Ok(())
}
pub fn into_owned(self) -> Line<'static> {
Line {
source: match self.source {
Source::Points { x, y } => Source::Points {
x: x.map(Series::into_owned),
y: y.into_owned(),
},
Source::Function { domain, function } => Source::Function { domain, function },
},
color: self.color,
label: self.label,
style: self.style,
color_by: self.color_by,
}
}
}
impl std::fmt::Debug for Line<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut debug = f.debug_struct("Line");
match &self.source {
Source::Points { x, y } => {
debug.field("points", &y.len());
debug.field("indexed", &x.is_none());
}
Source::Function { domain, .. } => {
debug.field("function_over", domain);
}
}
debug.field("color", &self.color).finish()
}
}
#[cfg(feature = "serde")]
mod serde_impls {
use serde::ser::Error as _;
use super::*;
#[derive(serde::Serialize)]
struct Repr<'s> {
x: Option<&'s Series<'s>>,
y: &'s Series<'s>,
color: &'s Option<Color>,
label: &'s Option<String>,
style: LineStyle,
#[serde(skip_serializing_if = "Option::is_none")]
color_by: &'s Option<Categories>,
}
#[derive(serde::Deserialize)]
struct OwnedRepr {
x: Option<Series<'static>>,
y: Series<'static>,
color: Option<Color>,
label: Option<String>,
style: LineStyle,
#[serde(default)]
color_by: Option<Categories>,
}
impl serde::Serialize for Line<'_> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match &self.source {
Source::Points { x, y } => Repr {
x: x.as_ref(),
y,
color: &self.color,
label: &self.label,
style: self.style,
color_by: &self.color_by,
}
.serialize(serializer),
Source::Function { .. } => Err(S::Error::custom(
"a function-backed Line cannot be serialized; sample it into points first",
)),
}
}
}
impl<'de, 'a> serde::Deserialize<'de> for Line<'a> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let repr = OwnedRepr::deserialize(deserializer)?;
Ok(Line {
source: Source::Points {
x: repr.x,
y: repr.y,
},
color: repr.color,
label: repr.label,
style: repr.style,
color_by: repr.color_by,
})
}
}
}
#[cfg(test)]
#[path = "tests/line_tests.rs"]
mod tests;