use rich::Color;
use super::{Dataset, GraphType, Marker};
#[derive(Clone, Debug, PartialEq)]
#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
pub struct Persistence {
frames: Vec<Vec<(f64, f64)>>,
style: String,
background: Option<Color>,
floor: f64,
graph_type: GraphType,
marker: Marker,
}
impl Persistence {
pub fn new<F>(frames: impl IntoIterator<Item = F>) -> Self
where
F: IntoIterator<Item = (f64, f64)>,
{
Persistence {
frames: frames
.into_iter()
.map(|f| f.into_iter().collect())
.collect(),
style: "green".to_string(),
background: None,
floor: 0.25,
graph_type: GraphType::Line,
marker: Marker::Braille,
}
}
pub fn style(mut self, style: impl Into<String>) -> Self {
self.style = style.into();
self
}
pub fn background(mut self, colour: &str) -> Self {
self.background = Color::parse(colour).ok();
self
}
pub fn fade(mut self, floor: f64) -> Self {
if !floor.is_nan() {
self.floor = floor.clamp(0.0, 1.0);
}
self
}
pub fn graph_type(mut self, graph_type: GraphType) -> Self {
self.graph_type = graph_type;
self
}
pub fn marker(mut self, marker: Marker) -> Self {
self.marker = marker;
self
}
pub fn len(&self) -> usize {
self.frames.len()
}
pub fn is_empty(&self) -> bool {
self.frames.is_empty()
}
pub fn styles(&self) -> Vec<String> {
let n = self.frames.len();
(0..n)
.map(|i| {
let keep = if n < 2 {
1.0
} else {
self.floor + (1.0 - self.floor) * i as f64 / (n - 1) as f64
};
self.faded(keep)
})
.collect()
}
pub fn datasets(&self) -> Vec<Dataset> {
self.frames
.iter()
.zip(self.styles())
.map(|(points, style)| {
Dataset::new(points.iter().copied())
.graph_type(self.graph_type)
.marker(self.marker)
.style(style)
})
.collect()
}
fn faded(&self, keep: f64) -> String {
let background = self
.background
.as_ref()
.and_then(Color::get_truecolor)
.unwrap_or(rich::ColorTriplet::new(0, 0, 0));
let mut words: Vec<String> = Vec::new();
let mut faded = false;
let mut after_on = false;
for word in self.style.split_whitespace() {
let colour = (!faded && !after_on)
.then(|| Color::parse(word).ok())
.flatten()
.and_then(|c| c.get_truecolor());
match colour {
Some(c) => {
let mix = |a: u8, b: u8| {
(f64::from(b) + (f64::from(a) - f64::from(b)) * keep).round() as u8
};
let (r, g, b) = (
mix(c.red, background.red),
mix(c.green, background.green),
mix(c.blue, background.blue),
);
words.push(rich::ColorTriplet::new(r, g, b).hex());
faded = true;
}
None => words.push(word.to_string()),
}
after_on = word.eq_ignore_ascii_case("on");
}
words.join(" ")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn older_frames_fade_towards_the_background() {
let p = Persistence::new([[(0.0, 0.0)], [(1.0, 1.0)]])
.style("bold #ffffff on blue")
.background("#000000")
.fade(0.5);
assert_eq!(p.styles(), ["bold #808080 on blue", "bold #ffffff on blue"]);
let light = Persistence::new([[(0.0, 0.0)], [(1.0, 1.0)]])
.style("#000000")
.background("white")
.fade(0.0);
assert_eq!(light.styles(), ["#c0c0c0", "#000000"]);
}
#[test]
fn a_theme_key_is_not_faded_and_one_frame_is_whole() {
let keyed = Persistence::new([[(0.0, 0.0)], [(1.0, 1.0)]]).style("chart.series.2");
assert_eq!(keyed.styles(), ["chart.series.2", "chart.series.2"]);
let one = Persistence::new([[(0.0, 0.0)]]).style("#123456");
assert_eq!(one.styles(), ["#123456"]);
assert!(Persistence::new(Vec::<Vec<(f64, f64)>>::new()).is_empty());
}
#[test]
fn datasets_keep_the_frames_in_order() {
let p = Persistence::new([vec![(0.0, 1.0)], vec![(0.0, 2.0), (1.0, 2.0)]])
.graph_type(GraphType::Scatter)
.marker(Marker::Dot);
let sets = p.datasets();
assert_eq!(p.len(), 2);
assert_eq!(sets[0].points(), [(0.0, 1.0)]);
assert_eq!(sets[1].points(), [(0.0, 2.0), (1.0, 2.0)]);
}
}