use std::vec::Vec;
#[derive(Default)]
pub struct Config {
width: u32,
height: u32,
offset: u32,
caption: String,
}
impl Config {
fn with_caption(mut self, caption: String) -> Self {
self.caption = caption;
self
}
fn with_height(mut self, height: u32) -> Self {
self.height = height;
self
}
fn with_width(mut self, width: u32) -> Self {
self.width = width;
self
}
fn with_offset(mut self, offset: u32) -> Self {
self.offset = offset;
self
}
}
pub fn plot(series: Vec<f64>, mut config: Config) -> String {
let series_inner = if config.width > 0 {
interpolate(series, config.width)
} else {
series
};
let (min, max) = min_max(&series_inner);
let interval = (max - min).abs();
if config.height == 0 {
if interval <= 1f64 {
config.height =
(interval * f64::from(10i32.pow((-interval.log10()).ceil() as u32))) as u32;
} else {
config.height = interval as u32;
}
}
if config.offset == 0 {
config.offset = 3;
}
let ratio = if interval != 0f64 {
f64::from(config.height) / interval
} else {
1f64
};
let min2 = (min * ratio).round();
let max2 = (max * ratio).round();
let int_min2 = min2 as i32;
let int_max2 = max2 as i32;
let rows = f64::from(int_max2 - int_min2).abs() as i32;
let width = series_inner.len() + config.offset as usize;
let mut plot: Vec<Vec<String>> = Vec::new();
for _i in 0..=rows {
let mut line = Vec::<String>::new();
for _j in 0..width {
line.push(" ".to_string());
}
plot.push(line);
}
let mut precision = 2;
let log_maximum = if min == 0f64 && max == 0f64 {
-1f64
} else {
f64::max(max.abs(), min.abs()).log10()
};
if log_maximum < 0f64 {
if log_maximum % 1f64 != 0f64 {
precision += log_maximum.abs() as i32;
} else {
precision += (log_maximum.abs() - 1f64) as i32;
}
} else if log_maximum > 2f64 {
precision = 0;
}
let max_number_label_length = format!("{:.*}", precision as usize, max).len();
let min_number_label_length = format!("{:.*}", precision as usize, min).len();
let max_label_width = usize::max(max_number_label_length, min_number_label_length);
for y in int_min2..=int_max2 {
let magnitude = if rows > 0 {
max - f64::from(y - int_min2) * interval / f64::from(rows)
} else {
f64::from(y)
};
let label = format!(
"{number:LW$.PREC$}",
LW = max_label_width + 1,
PREC = precision as usize,
number = magnitude
);
let w = (y - int_min2) as usize;
let h = f64::max(f64::from(config.offset) - label.len() as f64, 0f64) as usize;
plot[w][h] = label;
if y == 0 {
plot[w][(config.offset - 1) as usize] = "┼".to_string();
} else {
plot[w][(config.offset - 1) as usize] = "┤".to_string();
};
}
let mut y0 = ((series_inner[0] * ratio).round() - min2) as i32;
let mut y1: i32;
plot[(rows - y0) as usize][(config.offset - 1) as usize] = "┼".to_string();
for x in 0..series_inner.len() - 1 {
y0 = ((series_inner[x] * ratio).round() - f64::from(int_min2)) as i32;
y1 = ((series_inner[x + 1] * ratio).round() - f64::from(int_min2)) as i32;
if y0 == y1 {
plot[(rows - y0) as usize][(x as u32 + config.offset) as usize] = "─".to_string();
} else if y0 > y1 {
plot[(rows - y1) as usize][(x as u32 + config.offset) as usize] = "╰".to_string();
plot[(rows - y0) as usize][(x as u32 + config.offset) as usize] = "╮".to_string();
} else {
plot[(rows - y1) as usize][(x as u32 + config.offset) as usize] = "╭".to_string();
plot[(rows - y0) as usize][(x as u32 + config.offset) as usize] = "╯".to_string();
}
let start = f64::min(f64::from(y0), f64::from(y1)) as i32 + 1;
let end = f64::max(f64::from(y0), f64::from(y1)) as i32;
for y in start..end {
plot[(rows - y) as usize][(x as u32 + config.offset) as usize] = "│".to_string();
}
}
let mut res: String = plot
.into_iter()
.map(|line| line.join(""))
.collect::<Vec<String>>()
.join("\n");
res.pop();
if !config.caption.is_empty() {
res.push_str("\n");
res.push_str(
std::iter::repeat(" ")
.take(config.offset as usize + max_label_width + 2)
.collect::<String>()
.as_ref(),
);
res.push_str(config.caption.as_ref());
}
res
}
fn interpolate(series: Vec<f64>, count: u32) -> Vec<f64> {
let mut result = Vec::new();
let spring_factor = (series.len() - 1) as f64 / f64::from(count - 1);
result.push(series[0]);
for i in 1..count - 1 {
let spring = f64::from(i) * spring_factor;
let before = spring.floor();
let after = spring.ceil();
let at_point = spring - before;
result.push(linear_interpolate(
series[before as usize],
series[after as usize],
at_point,
))
}
result.push(series[series.len() - 1]);
result
}
fn linear_interpolate(before: f64, after: f64, at_point: f64) -> f64 {
before + (after - before) * at_point
}
fn min_max(series: &[f64]) -> (f64, f64) {
let min = series
.iter()
.fold(std::f64::MAX, |accu, &x| if x < accu { x } else { accu });
let max = series
.iter()
.fold(std::f64::MIN, |accu, &x| if x > accu { x } else { accu });
(min, max)
}
#[cfg(test)]
mod tests {
macro_rules! graph_eq {
($fname:ident ? [$($series:expr),*] => $rhs:expr) => {
#[test]
fn $fname(){
let res = crate::plot(vec![$(f64::from($series),)*], crate::Config::default());
assert_eq!(res, $rhs);
}};
($fname:ident ? [$($series:expr),*] ? $config:expr => $rhs:expr) => {
#[test]
fn $fname(){
let res = crate::plot(vec![$(f64::from($series),)*], $config);
assert_eq!(res, $rhs);
}};
}
graph_eq!(test_ones ? [1, 1, 1, 1, 1] => " 1.00 ┼────");
graph_eq!(test_zeros ? [0, 0, 0, 0, 0] => " 0.00 ┼────");
graph_eq!(test_three ? [2, 1, 1, 2, -2, 5, 7, 11, 3, 7, 1] => " 11.00 ┤ ╭╮
10.00 ┤ ││
9.00 ┼ ││
8.00 ┤ ││
7.00 ┤ ╭╯│╭╮
6.00 ┤ │ │││
5.00 ┤ ╭╯ │││
4.00 ┤ │ │││
3.00 ┤ │ ╰╯│
2.00 ┼╮ ╭╮│ │
1.00 ┤╰─╯││ ╰
0.00 ┤ ││
-1.00 ┤ ││
-2.00 ┤ ╰╯ ");
graph_eq!(test_four ? [2, 1, 1, 2, -2, 5, 7, 11, 3, 7, 4, 5, 6, 9, 4, 0, 6, 1, 5, 3, 6, 2] ?
crate::Config::default().with_caption("Plot using asciigraph.".to_string())
=> " 11.00 ┤ ╭╮
10.00 ┤ ││
9.00 ┼ ││ ╭╮
8.00 ┤ ││ ││
7.00 ┤ ╭╯│╭╮ ││
6.00 ┤ │ │││ ╭╯│ ╭╮ ╭╮
5.00 ┤ ╭╯ │││╭╯ │ ││╭╮││
4.00 ┤ │ ││╰╯ ╰╮││││││
3.00 ┤ │ ╰╯ ││││╰╯│
2.00 ┼╮ ╭╮│ ││││ ╰
1.00 ┤╰─╯││ ││╰╯
0.00 ┤ ││ ╰╯
-1.00 ┤ ││
-2.00 ┤ ╰╯
Plot using asciigraph.");
graph_eq!(test_five ? [ 2, 1, 1, 2, -2, 5, 7, 11, 3, 7, 4, 5, 6, 9, 4, 0, 6, 1, 5, 3, 6, 2] ?
crate::Config::default().with_caption("Plot using asciigraph.".to_string())
=> " 11.00 ┤ ╭╮
10.00 ┤ ││
9.00 ┼ ││ ╭╮
8.00 ┤ ││ ││
7.00 ┤ ╭╯│╭╮ ││
6.00 ┤ │ │││ ╭╯│ ╭╮ ╭╮
5.00 ┤ ╭╯ │││╭╯ │ ││╭╮││
4.00 ┤ │ ││╰╯ ╰╮││││││
3.00 ┤ │ ╰╯ ││││╰╯│
2.00 ┼╮ ╭╮│ ││││ ╰
1.00 ┤╰─╯││ ││╰╯
0.00 ┤ ││ ╰╯
-1.00 ┤ ││
-2.00 ┤ ╰╯
Plot using asciigraph." );
graph_eq!(test_six ? [0.2, 0.1, 0.2, 2, -0.9, 0.7, 0.91, 0.3, 0.7, 0.4, 0.5] ?
crate::Config::default().with_caption("Plot using asciigraph.".to_string())
=> " 2.00 ┤ ╭╮ ╭╮
0.55 ┼──╯│╭╯╰───
-0.90 ┤ ╰╯
Plot using asciigraph." );
graph_eq!(test_seven ? [2, 1, 1, 2, -2, 5, 7, 11, 3, 7, 1] ?
crate::Config::default().with_height(4).with_offset(3)
=> " 11.00 ┤ ╭╮
7.75 ┼ ╭─╯│╭╮
4.50 ┼╮ ╭╮│ ╰╯│
1.25 ┤╰─╯││ ╰
-2.00 ┤ ╰╯ "
);
graph_eq!(test_eight ? [0.453, 0.141, 0.951, 0.251, 0.223, 0.581, 0.771, 0.191, 0.393, 0.617, 0.478]
=> " 0.95 ┤ ╭╮
0.85 ┤ ││ ╭╮
0.75 ┤ ││ ││
0.65 ┤ ││ ╭╯│ ╭╮
0.55 ┤ ││ │ │ │╰
0.44 ┼╮││ │ │╭╯
0.34 ┤│││ │ ││
0.24 ┤││╰─╯ ╰╯
0.14 ┤╰╯ ");
graph_eq!(test_nine ? [0.01, 0.004, 0.003, 0.0042, 0.0083, 0.0033, 0.0079]
=> " 0.010 ┼╮
0.009 ┤│
0.008 ┤│ ╭╮╭
0.007 ┤│ │││
0.006 ┤│ │││
0.005 ┤│ │││
0.004 ┤╰╮╭╯││
0.003 ┤ ╰╯ ╰╯"
);
graph_eq!(test_ten ? [192, 431, 112, 449, -122, 375, 782, 123, 911, 1711, 172] ? crate::Config::default().with_height(10)
=> " 1711 ┤ ╭╮
1528 ┼ ││
1344 ┤ ││
1161 ┤ ││
978 ┤ ╭╯│
794 ┤ ╭╮│ │
611 ┤ │││ │
428 ┤╭╮╭╮╭╯││ │
245 ┼╯╰╯││ ╰╯ ╰
61 ┤ ││
-122 ┤ ╰╯ ");
graph_eq!(test_eleven ? [0.3189989805, 0.149949026, 0.30142492354, 0.195129182935, 0.3142492354,
0.1674974513, 0.3142492354, 0.1474974513, 0.3047974513] ?
crate::Config::default().with_width(30).with_height(5).with_caption("Plot with custom height & width.".to_string())
=> " 0.32 ┼╮ ╭─╮ ╭╮ ╭
0.29 ┤╰╮ ╭─╮ ╭╯ │ ╭╯│ │
0.26 ┤ │ ╭╯ ╰╮ ╭╯ ╰╮ ╭╯ ╰╮ ╭╯
0.23 ┤ ╰╮ ╭╯ ╰╮│ ╰╮╭╯ ╰╮ ╭╯
0.20 ┤ ╰╮│ ╰╯ ╰╯ │╭╯
0.16 ┤ ╰╯ ╰╯
Plot with custom height & width."
);
graph_eq!(test_twelve ? [0, 0, 0, 0, 1.5, 0, 0, -0.5, 9, -3, 0, 0, 1, 2, 1, 0, 0, 0, 0,
0, 0, 0, 0, 1.5, 0, 0, -0.5, 8, -3, 0, 0, 1, 2, 1, 0, 0, 0, 0,
0, 0, 0, 0, 1.5, 0, 0, -0.5, 10, -3, 0, 0, 1, 2, 1, 0, 0, 0, 0] ?
crate::Config::default().with_offset(10).with_height(10).with_caption("I'm a doctor, not an engineer.".to_string())
=> " 10.00 ┤ ╭╮
8.70 ┤ ╭╮ ││
7.40 ┼ ││ ╭╮ ││
6.10 ┤ ││ ││ ││
4.80 ┤ ││ ││ ││
3.50 ┤ ││ ││ ││
2.20 ┤ ││ ╭╮ ││ ╭╮ ││ ╭╮
0.90 ┤ ╭╮ ││ ╭╯╰╮ ╭╮ ││ ╭╯╰╮ ╭╮ ││ ╭╯╰╮
-0.40 ┼───╯╰──╯│╭─╯ ╰───────╯╰──╯│╭─╯ ╰───────╯╰──╯│╭─╯ ╰───
-1.70 ┤ ││ ││ ││
-3.00 ┤ ╰╯ ╰╯ ╰╯
I'm a doctor, not an engineer.");
graph_eq!(test_thirteen ? [-5, -2, -3, -4, 0, -5, -6, -7, -8, 0, -9, -3, -5, -2, -9, -3, -1]
=> " 0.00 ┤ ╭╮ ╭╮
-1.00 ┤ ││ ││ ╭
-2.00 ┤╭╮ ││ ││ ╭╮ │
-3.00 ┤│╰╮││ ││╭╮││╭╯
-4.00 ┤│ ╰╯│ │││││││
-5.00 ┼╯ ╰╮ │││╰╯││
-6.00 ┤ ╰╮ │││ ││
-7.00 ┤ ╰╮│││ ││
-8.00 ┤ ╰╯││ ││
-9.00 ┼ ╰╯ ╰╯ ");
graph_eq!(test_fourteen ? [-0.000018527, -0.021, -0.00123, 0.00000021312,
-0.0434321234, -0.032413241234, 0.0000234234] ?
crate::Config::default().with_height(5).with_width(45)
=> " 0.000 ┼─╮ ╭────────╮ ╭
-0.008 ┤ ╰──╮ ╭──╯ ╰─╮ ╭─╯
-0.017 ┤ ╰─────╯ ╰╮ ╭─╯
-0.025 ┤ ╰─╮ ╭─╯
-0.034 ┤ ╰╮ ╭────╯
-0.042 ┼ ╰───╯ "
);
graph_eq!(test_fifteen ? [57.76, 54.04, 56.31, 57.02, 59.5, 52.63, 52.97, 56.44, 56.75, 52.96, 55.54,
55.09, 58.22, 56.85, 60.61, 59.62, 59.73, 59.93, 56.3, 54.69, 55.32, 54.03, 50.98, 50.48, 54.55, 47.49,
55.3, 46.74, 46, 45.8, 49.6, 48.83, 47.64, 46.61, 54.72, 42.77, 50.3, 42.79, 41.84, 44.19, 43.36, 45.62,
45.09, 44.95, 50.36, 47.21, 47.77, 52.04, 47.46, 44.19, 47.22, 45.55, 40.65, 39.64, 37.26, 40.71, 42.15,
36.45, 39.14, 36.62]
=> " 60.61 ┤ ╭╮ ╭╮
59.60 ┤ ╭╮ │╰─╯│
58.60 ┤ ││ ╭╮│ │
57.59 ┼╮ ╭╯│ │││ │
56.58 ┤│╭╯ │ ╭─╮ │╰╯ ╰╮
55.58 ┤││ │ │ │╭─╯ │╭╮ ╭╮
54.57 ┤╰╯ │ │ ││ ╰╯╰╮ ╭╮││ ╭╮
53.56 ┤ │╭╯ ╰╯ │ ││││ ││
52.56 ┤ ╰╯ │ ││││ ││ ╭╮
51.55 ┤ ╰╮││││ ││ ││
50.54 ┤ ╰╯│││ ││╭╮ ╭╮ ││
49.54 ┤ │││ ╭─╮ ││││ ││ ││
48.53 ┤ │││ │ │ ││││ ││ ││
47.52 ┤ ╰╯│ │ ╰╮││││ │╰─╯╰╮╭╮
46.52 ┤ ╰─╮│ ╰╯│││ │ │││
45.51 ┤ ╰╯ │││ ╭──╯ ││╰╮
44.50 ┤ │││ ╭╮│ ╰╯ │
43.50 ┤ ││╰╮│╰╯ │
42.49 ┤ ╰╯ ╰╯ │ ╭╮
41.48 ┤ │ ││
40.48 ┤ ╰╮ ╭╯│
39.47 ┤ ╰╮│ │╭╮
38.46 ┤ ││ │││
37.46 ┤ ╰╯ │││
36.45 ┤ ╰╯╰"
);
#[test]
fn test_min_max() {
assert_eq!(
(-2f64, 11f64),
crate::min_max(&vec![
2f64, 1f64, 1f64, 2f64, -2f64, 5f64, 7f64, 11f64, 3f64, 7f64, 1f64
])
);
}
}