use crate::legend::add_legends;
use crate::options::{CharSet, Config, DEFAULT_CHAR_SET};
use crate::utils::{calculate_height, interpolate_array, min_max_float64_slice};
use crate::{utils, AnsiColor};
#[derive(Clone)]
struct Cell {
text: String,
color: AnsiColor
}
impl Default for Cell {
fn default() -> Self {
Cell {
text: " ".to_string(),
color: AnsiColor::DEFAULT,
}
}
}
pub fn plot(series: &[f64], config: Config) -> String {
plot_many(&mut [series], config)
}
pub(crate) fn get_char_set(config: &Config, series_index: usize) -> CharSet {
if series_index < config.series_chars.len() {
return config.series_chars[series_index].clone();
}
DEFAULT_CHAR_SET
}
pub fn plot_many(data: &[&[f64]], config: Config) -> String {
let mut config = config;
if config.offset == 0 {
config.offset = 3;
}
if config.line_ending.is_empty() {
config.line_ending = "\n".to_string();
}
let mut data: Vec<Vec<f64>> = data.iter().map(|s| s.to_vec()).collect();
let mut len_max: usize = 0;
for series in data.iter() {
len_max = len_max.max(series.len());
}
if config.width > 0 {
for i in 0..data.len() {
while data[i].len() < len_max {
data[i].push(f64::NAN);
}
data[i] = interpolate_array(&data[i], config.width as u32);
}
len_max = config.width;
}
let mut minimum = f64::INFINITY;
let mut maximum = f64::NEG_INFINITY;
for i in 0..data.len() {
let values = min_max_float64_slice(&data[i]);
match values {
Some((min_value, max_value)) => {
if min_value < minimum {
minimum = min_value;
}
if max_value > maximum {
maximum = max_value;
}
},
None => println!("Values were not provided"),
}
}
if let Some(lb) = config.lower_bound {
if lb < minimum { minimum = lb; }
}
if let Some(ub) = config.upper_bound {
if ub > maximum { maximum = ub; }
}
let interval = (maximum - minimum).abs();
if config.height <= 0 {
config.height = calculate_height(interval)
}
if config.offset <= 0 {
config.offset = 3;
}
let ratio: f64;
if interval != 0.0 {
ratio = config.height as f64 / interval;
} else {
ratio = 1.0;
}
let min2 = utils::round(minimum * ratio);
let max2 = utils::round(maximum * ratio);
let intmin2 = min2.round() as isize;
let intmax2 = max2.round() as isize;
let rows = (intmax2 - intmin2).unsigned_abs();
let width = len_max + config.offset;
let mut plot: Vec<Vec<Cell>> = vec![vec![Cell::default(); width]; rows + 1];
let mut precision: usize = config.precision.unwrap_or(2);
let mut log_maximum = maximum.abs().max(minimum.abs()).log10();
if minimum == 0.0 && maximum == 0.0 {
log_maximum = f64::from(-1);
}
if log_maximum < 0.0 {
if log_maximum.fract() != 0.0 {
precision += log_maximum.abs() as usize;
} else {
precision += (log_maximum.abs() - 1.0) as usize;
}
} else if log_maximum > 2.0 && config.precision.is_none() {
precision = 0;
}
let mut max_num_length = format!("{:.1$}", maximum, precision).chars().count();
let min_num_length = format!("{:.precision$}", minimum, precision = precision).chars()
.count();
let mut magnitudes: Vec<f64> = Vec::with_capacity(rows + 1);
if config.y_axis_value_formatter.is_some() {
max_num_length = 0;
}
let y = intmin2;
for y in y..intmax2 + 1 {
let magnitude: f64;
if rows > 0 && interval > 0.0 {
magnitude = maximum - (((y - intmin2) as f64 * interval) / rows as f64);
} else if interval == 0.0 {
magnitude = minimum;
} else {
magnitude = y as f64;
}
magnitudes.push(magnitude);
if let Some(formatter) = &config.y_axis_value_formatter {
let result = formatter(magnitude);
let l = result.chars().count();
if l > max_num_length {
max_num_length = l;
}
}
}
let max_width: usize;
if config.y_axis_value_formatter.is_some() {
max_width = max_num_length;
} else {
max_width = (max_num_length as f64).max(min_num_length as f64) as usize;
}
let left_pad = config.offset + max_width;
for (w, magnitude) in magnitudes.iter().enumerate() {
let mut label = String::new();
if config.y_axis_value_formatter.is_none() {
label = format!("{:>width$.prec$}", magnitude, width = max_width + 1, prec = precision);
} else {
if let Some(formatter) = &config.y_axis_value_formatter {
let val = formatter(*magnitude);
let padding = " ".repeat((max_width + 1) - val.chars().count());
label = format!("{}{}", padding, val);
}
}
let float_value = (config.offset as f64) - (label.chars().count() as f64);
let h = float_value.max(0.0) as usize;
plot[w][h].text = label;
plot[w][h].color = config.label_color;
plot[w][config.offset - 1].text = "┤".to_string();
plot[w][config.offset - 1].color = config.axis_color;
}
for i in 0..data.len() {
let series = &data[i];
let mut color = AnsiColor::DEFAULT;
if i < config.series_colors.len() {
color = config.series_colors[i];
}
let char_set = get_char_set(&config, i);
let (mut y0, mut y1): (usize, usize);
if !series[0].is_nan() {
y0 = ((series[0] * ratio).round() - min2) as usize;
plot[rows - y0][config.offset - 1].text = "┼".to_string(); plot[rows - y0][config.offset - 1].color = config.axis_color;
}
if series.len() > 1 {
for x in 0..series.len() - 1 {
let d0 = series[x];
let d1 = series[x+1];
if d0.is_nan() && d1.is_nan() {
continue;
}
if !d0.is_nan() && d1.is_nan() {
y0 = ((d0 * ratio).round() - intmin2 as f64) as usize;
plot[rows - y0][x + config.offset].text = char_set.end_cap.to_string();
plot[rows - y0][x + config.offset].color = color;
continue;
}
if d0.is_nan() && !d1.is_nan() {
y1 = ((d1 * ratio).round() - intmin2 as f64) as usize;
plot[rows - y1][x + config.offset].text = char_set.start_cap.to_string();
plot[rows - y1][x + config.offset].color = color;
continue;
}
y0 = ((d0 * ratio).round() - intmin2 as f64) as usize;
y1 = ((d1 * ratio).round() - intmin2 as f64) as usize;
if y0 == y1 {
plot[rows - y0][x + config.offset].text = char_set.horizontal.to_string();
} else {
if y0 > y1 {
plot[rows - y1][x + config.offset].text = char_set.arc_up_right.to_string();
plot[rows - y0][x + config.offset].text = char_set.arc_down_left.to_string();
} else {
plot[rows - y1][x + config.offset].text = char_set.arc_down_right.to_string();
plot[rows - y0][x + config.offset].text = char_set.arc_up_left.to_string();
}
let start = (y0 as f64).min(y1 as f64) as usize + 1;
let end = (y0 as f64).max(y1 as f64) as usize;
let y = start;
for y in y..end {
plot[rows - y][x + config.offset].text = char_set.vertical_line.to_string();
}
}
let start = (y0 as f64).min(y1 as f64) as usize;
let end = (y0 as f64).max(y1 as f64) as usize;
let y = start;
for y in y..=end {
plot[rows - y][x + config.offset].color = color;
}
}
}
}
let mut lines = String::new();
for (h, horizontal) in plot.iter().enumerate() {
if h != 0 {
lines.push_str(&config.line_ending);
}
let mut last_char_index = 0;
for i in (0..width).rev() {
if horizontal[i].text != " " {
last_char_index = i;
break;
}
}
let mut c = AnsiColor::DEFAULT;
for v in horizontal[..=last_char_index].iter() {
if v.color != c {
c = v.color;
lines.push_str(c.to_string().as_str());
}
lines.push_str(v.text.as_str());
}
if c != AnsiColor::DEFAULT {
lines.push_str(AnsiColor::DEFAULT.to_string().as_str());
}
}
if config.x_axis_range.is_some() {
add_x_axis(&mut lines, &config, len_max, left_pad);
}
if !config.caption.is_empty() {
lines.push_str(&config.line_ending);
lines.push_str(&" ".repeat(left_pad));
if config.caption.len() < len_max {
lines.push_str(&" ".repeat((len_max - config.caption.len())/2));
}
if config.caption_color != AnsiColor::DEFAULT {
lines.push_str(config.caption_color.to_string().as_str());
}
lines.push_str(config.caption.as_str());
if config.caption_color != AnsiColor::DEFAULT {
lines.push_str(AnsiColor::DEFAULT.to_string().as_str())
}
}
if config.series_legends.len() > 0 {
add_legends(&mut lines, &config, len_max, left_pad);
}
lines.to_string()
}
fn add_x_axis(lines: &mut String, config: &Config, len_max: usize, left_pad: usize) {
let default_x_axis_formatter = |v: f64| format!("{}", v);
if len_max <= 0 {
return;
}
let x_min = config.x_axis_range.unwrap()[0];
let x_max = config.x_axis_range.unwrap()[1];
let mut tick_count = config.x_axis_tick_count;
if len_max == 1 {
tick_count = 1;
} else if tick_count < 2 {
tick_count = 5;
}
if tick_count > len_max {
tick_count = len_max;
}
let decimal_formatter: fn(f64) -> String = |v: f64| format!("{:.2}", v);
let default_fn: fn(f64) -> String = default_x_axis_formatter;
let mut formatter = config.x_axis_value_formatter.as_ref().map(|f| f.as_ref());
struct Tick {
col: usize,
value: f64,
label: String
}
let mut ticks: Vec<Tick> = Vec::with_capacity(tick_count);
for i in 0..tick_count {
let col = if tick_count == 1 {
0
} else {
(((len_max - 1) as f64 * i as f64) / (tick_count - 1) as f64).round() as usize
};
let value = if tick_count == 1 {
x_min
} else {
x_min + (i as f64 / (tick_count - 1) as f64) * (x_max - x_min)
};
ticks.push(Tick { col, value, label: String::new() });
}
if formatter.is_none() {
let mut has_decimal = false;
let mut last_end: isize = -1;
for i in 0..ticks.len() {
let label = default_x_axis_formatter(ticks[i].value);
let label_len = label.chars().count();
let start_col = left_pad + ticks[i].col - (label_len / 2);
if start_col as isize > last_end {
if ticks[i].value != ticks[i].value.floor() {
has_decimal = true;
break;
}
last_end = start_col as isize + label_len as isize;
}
}
if has_decimal {
formatter = Some(&decimal_formatter);
} else {
formatter = Some(&default_fn);
}
}
for i in 0..ticks.len() {
ticks[i].label = formatter.unwrap()(ticks[i].value);
}
let total_width = left_pad + len_max;
let mut axis_line: Vec<char> = vec![' '; total_width];
axis_line[left_pad - 1] = DEFAULT_CHAR_SET.up_right;
for i in 0..len_max {
axis_line[left_pad + i] = DEFAULT_CHAR_SET.horizontal;
}
for tk in &ticks {
axis_line[left_pad + tk.col] = DEFAULT_CHAR_SET.down_horizontal;
}
lines.push_str(config.line_ending.as_str());
let text_stream = axis_line.iter().collect::<String>();
let axis_str = text_stream.trim_end();
if config.axis_color != AnsiColor::DEFAULT {
lines.push_str(config.axis_color.to_string().as_str());
}
lines.push_str(axis_str);
if config.axis_color != AnsiColor::DEFAULT {
lines.push_str(AnsiColor::DEFAULT.to_string().as_str());
}
let mut max_right_extent = total_width;
for tk in &ticks {
let label_len = tk.label.chars().count();
let end_col = left_pad + tk.col + (label_len - (label_len / 2));
if end_col > max_right_extent {
max_right_extent = end_col;
}
}
let mut label_line: Vec<char> = vec![' '; max_right_extent];
for i in 0..label_line.len() {
label_line[i] = ' ';
}
let mut last_end: isize = -1;
for tk in &ticks {
let label_runes: Vec<char> = tk.label.chars().collect();
let label_len = label_runes.len();
let mut start_col = (left_pad + tk.col) as isize - (label_len / 2) as isize;
if start_col < 0 {
start_col = 0;
}
if start_col <= last_end {
continue
}
for (j, r) in label_runes.iter().enumerate() {
let pos = start_col + j as isize;
if pos < label_line.len() as isize {
label_line[pos as usize] = *r;
}
}
last_end = start_col + label_len as isize;
}
let text = label_line.iter().collect::<String>();
let label_str = text.trim_end();
if !label_str.is_empty() {
lines.push_str(config.line_ending.as_str());
if config.label_color != AnsiColor::DEFAULT {
lines.push_str(config.label_color.to_string().as_str());
}
lines.push_str(label_str);
if config.label_color != AnsiColor::DEFAULT {
lines.push_str(AnsiColor::DEFAULT.to_string().as_str());
}
}
}