#![deny(missing_docs)]
use failure::{bail, Fallible};
use log::{debug, info, warn, LevelFilter};
use std::{cmp::max, convert, fmt, iter, u8};
use termion::color::{self, Fg, LightBlack, Reset};
pub struct Graph<V> {
lines_to_be_removed: Vec<String>,
columns: Vec<Column<V>>,
prefix_len: usize,
}
impl<V> Graph<V>
where
V: Clone + Default + Ord + PartialEq + fmt::Debug,
f64: convert::From<V>,
{
pub fn new() -> Self {
Self::with_prefix_length(8)
}
pub fn with_prefix_length(length: usize) -> Self {
Graph {
lines_to_be_removed: vec![],
columns: vec![],
prefix_len: length + 3,
}
}
pub fn set_log_level(self, level: LevelFilter) -> Self {
if mowl::init_with_level(level).is_err() {
warn!("Logger already set.");
} else {
info!("Log level set to: {:?}", level);
}
self
}
pub fn add<T>(&mut self, identifier: T, value: V) -> Fallible<T>
where
T: fmt::Display,
{
let line_name = format!("{}", identifier);
debug!("Adding value {:?} to line '{}'", value, line_name);
let add_new_line = {
if let Some(line) = self.line_already_existing(&line_name) {
debug!("Line already exist, just adding the value");
line.add_value(value.clone());
false
} else {
true
}
};
if add_new_line {
debug!("Adding new line");
let column = self.get_next_free_column();
let mut line = Line::new(&line_name);
line.add_value(value);
*column = Column::Used(line);
}
Ok(identifier)
}
pub fn remove<T>(&mut self, identifier: T) -> Fallible<T>
where
T: fmt::Display,
{
let line_name = format!("{}", identifier);
if self.line_already_existing(&line_name).is_none() {
bail!("Line does not exist and can not be removed");
}
self.lines_to_be_removed.push(line_name);
Ok(identifier)
}
pub fn print(&mut self) -> Fallible<()> {
fn fillchar() -> String {
format!("{}┈{}", Fg(LightBlack), Fg(Reset))
}
let start_ch = "┬";
let line_chr = "│";
let nodata_c = "╎";
let end_char = "┴";
let col_width = 2;
let (width, _) = termion::terminal_size()?;
let mut cursor = self.prefix_len as u16;
let end_cursor = if width % 2 == 0 { width - 2 } else { width - 1 };
struct Row {
content: String,
prefix: Option<String>,
}
let mut row = Row {
content: String::with_capacity(width as usize),
prefix: None,
};
macro_rules! get_value {
($($p:ident)*) => (
$(self.columns.iter().filter_map(|c| {
match *c {
Column::Used(ref line) => line.values.iter().$p(),
_ => None,
}
}).$p().cloned().unwrap_or_default())*
)
}
let (min, max) = (get_value!(min), get_value!(max));
for column in &mut self.columns {
if end_cursor < cursor + col_width {
row.content += "…";
cursor += 1;
break;
}
let free_column = match *column {
Column::Used(ref mut line) => {
let mut row_prefix = format!(
"{:>w$.*}",
self.prefix_len - 3,
line.name,
w = self.prefix_len - 3
);
let (c, free_column) = if line.started {
if self.lines_to_be_removed.contains(&line.name) {
row_prefix += " ← ";
row.prefix = Some(row_prefix);
(end_char, true)
} else {
(
if line.got_data { line_chr } else { nodata_c },
false,
)
}
} else {
row_prefix += " → ";
row.prefix = Some(row_prefix);
line.started = true;
(start_ch, false)
};
let value = line.values.last().cloned().unwrap_or_default();
let (r, g, b) =
Self::rgb(min.clone(), max.clone(), value.clone());
row.content += &format!(
"{}{}{}",
Fg(color::Rgb(r, g, b)),
c,
Fg(Reset)
);
row.content += &fillchar();
line.got_data = false;
free_column
}
Column::Free => {
row.content += &fillchar();
row.content += &fillchar();
false
}
};
if free_column {
*column = Column::Free;
}
cursor += col_width;
}
for _ in cursor..width {
row.content += &fillchar();
}
let prefix_string = match row.prefix {
Some(prefix) => prefix,
_ => iter::repeat(' ').take(self.prefix_len).collect::<String>(),
};
println!("{}{}", prefix_string, row.content);
self.lines_to_be_removed.clear();
Ok(())
}
pub fn print_if_new_data(&mut self) -> Fallible<bool> {
if !self.lines_to_be_removed.is_empty()
|| self
.columns
.iter()
.filter(|c| match **c {
Column::Used(ref line) if line.got_data => true,
_ => false,
})
.count()
> 0
{
self.print()?;
Ok(true)
} else {
Ok(false)
}
}
fn get_next_free_column(&mut self) -> &mut Column<V> {
let free_column_count = self
.columns
.iter_mut()
.filter(|c| **c == Column::Free)
.count();
if free_column_count == 0 {
self.columns.push(Column::Free);
self.columns.iter_mut().rev().next().unwrap()
} else {
self.columns
.iter_mut()
.find(|c| **c == Column::Free)
.unwrap()
}
}
fn line_already_existing(
&mut self,
line_name: &str,
) -> Option<&mut Line<V>> {
let line_string = line_name.to_owned();
self.columns
.iter_mut()
.filter_map(|c| match *c {
Column::Used(ref mut line) if line_string == line.name => {
Some(line)
}
_ => None,
})
.next()
}
fn rgb(minimum: V, maximum: V, value: V) -> (u8, u8, u8) {
let soft_scale = 125;
if minimum == maximum {
return (soft_scale, soft_scale, u8::MAX);
}
let minimum = f64::from(minimum);
let maximum = f64::from(maximum);
let value = f64::from(value);
let ratio = 2f64 * (value - minimum) / (maximum - minimum);
let mut b = max(0, (255f64 * (1f64 - ratio)) as i64) as u8;
let mut r = max(0, (255f64 * (ratio - 1f64)) as i64) as u8;
let mut g = (255 - b - r) as u8;
b = b.saturating_add(soft_scale);
r = r.saturating_add(soft_scale);
g = g.saturating_add(soft_scale);
(r, g, b)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct Line<V> {
got_data: bool,
name: String,
started: bool,
values: Vec<V>,
}
impl<V> Line<V> {
fn new(name: &str) -> Self {
Line {
got_data: false,
name: name.to_owned(),
started: false,
values: vec![],
}
}
fn add_value(&mut self, value: V) {
self.values.push(value);
self.got_data = true;
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum Column<V> {
Free,
Used(Line<V>),
}