use std::collections::HashMap;
use std;
use histogram;
use scatter;
use axis;
use utils::PairWise;
fn value_to_axis_cell_offset(value: f64, axis: &axis::Axis, face_cells: u32) -> i32 {
let data_per_cell = (axis.max() - axis.min()) / face_cells as f64;
((value - axis.min()) / data_per_cell).round() as i32
}
fn tick_offset_map(axis: &axis::Axis, face_width: u32) -> HashMap<i32, f64> {
axis.ticks()
.iter()
.map(|&tick| (value_to_axis_cell_offset(tick, axis, face_width), tick))
.collect()
}
fn bound_cell_offsets(hist: &histogram::Histogram, face_width: u32) -> Vec<i32> {
hist.bin_bounds
.iter()
.map(|&bound| value_to_axis_cell_offset(bound, &hist.x_axis, face_width))
.collect()
}
fn bins_for_cells(bound_cell_offsets: &[i32], face_width: u32) -> Vec<Option<i32>> {
let bound_cells = bound_cell_offsets;
let bin_width_in_cells = bound_cells.pairwise().map(|(&a, &b)| b - a);
let bins_cell_offset = bound_cells.first().unwrap();
let mut cell_bins: Vec<Option<i32>> = vec![None]; for (bin, width) in bin_width_in_cells.enumerate() {
for _ in 0..width {
cell_bins.push(Some(bin as i32));
}
}
cell_bins.push(None);
if *bins_cell_offset < 0 {
cell_bins =
cell_bins.iter().skip(bins_cell_offset.wrapping_abs() as usize).cloned().collect();
} else if *bins_cell_offset > 0 {
let mut new_bins = vec![None; (*bins_cell_offset) as usize];
new_bins.extend(cell_bins.iter());
cell_bins = new_bins;
}
if cell_bins.len() < face_width as usize + 2 {
let deficit = face_width as usize + 2 - cell_bins.len();
let mut new_bins = cell_bins;
new_bins.extend(vec![None; deficit].iter());
cell_bins = new_bins;
} else if cell_bins.len() > face_width as usize + 2 {
let new_bins = cell_bins;
cell_bins = new_bins.iter().take(face_width as usize + 2).cloned().collect();
}
cell_bins
}
#[derive(Debug)]
struct XAxisLabel {
text: String,
offset: i32,
}
impl XAxisLabel {
fn len(&self) -> usize {
self.text.len()
}
fn footprint(&self) -> usize {
if self.len() % 2 == 0 {
self.len() + 1
} else {
self.len()
}
}
fn start_offset(&self) -> i32 {
self.offset as i32 - self.footprint() as i32 / 2
}
}
fn create_x_axis_labels(x_tick_map: &HashMap<i32, f64>) -> Vec<XAxisLabel> {
let mut ls: Vec<_> = x_tick_map.iter()
.map(|(&offset, &tick)| {
XAxisLabel {
text: tick.to_string(),
offset: offset,
}
})
.collect();
ls.sort_by_key(|l| l.offset);
ls
}
fn render_y_axis_strings(y_axis: &axis::Axis,
face_height: u32)
-> (Vec<String>, Vec<String>, Vec<String>, i32) {
let y_tick_map = tick_offset_map(&y_axis, face_height);
let longest_y_label_width = y_tick_map.values()
.map(|n| n.to_string().len())
.max()
.expect("ERROR: There are no y-axis ticks");
let y_label_strings: Vec<_> = (0..face_height + 1)
.map(|line| match y_tick_map.get(&(line as i32)) {
Some(v) => v.to_string(),
None => "".to_string(),
})
.collect();
let y_tick_strings: Vec<_> = (0..face_height + 1)
.map(|line| match y_tick_map.get(&(line as i32)) {
Some(_) => "-".to_string(),
None => " ".to_string(),
})
.collect();
let y_axis_line_strings: Vec<String> = std::iter::repeat('+')
.take(1)
.chain(std::iter::repeat('|').take(face_height as usize))
.map(|s| s.to_string())
.collect();
(y_label_strings, y_tick_strings, y_axis_line_strings, longest_y_label_width as i32)
}
fn render_x_axis_strings(x_axis: &axis::Axis, face_width: u32) -> (String, String, String, i32) {
let x_tick_map = tick_offset_map(x_axis, face_width as u32);
let x_axis_tick_string: String = (0..face_width + 1)
.map(|cell| match x_tick_map.get(&(cell as i32)) {
Some(_) => '|',
None => ' ',
})
.collect();
let x_labels = create_x_axis_labels(&x_tick_map);
let start_offset = x_labels.iter()
.map(|label| label.start_offset())
.min()
.expect("ERROR: Could not compute start offset of x-axis");
let mut x_axis_label_string = "".to_string();
for label in (&x_labels).iter() {
let spaces_to_append = label.start_offset() - start_offset -
x_axis_label_string.len() as i32;
if spaces_to_append.is_positive() {
for _ in 0..spaces_to_append {
x_axis_label_string.push(' ');
}
} else {
for _ in 0..spaces_to_append.wrapping_neg() {
x_axis_label_string.pop();
}
}
let formatted_label = format!("{: ^footprint$}", label.text, footprint = label.footprint());
x_axis_label_string.push_str(&formatted_label);
}
let x_axis_line_string: String = std::iter::repeat('+')
.take(1)
.chain(std::iter::repeat('-').take(face_width as usize))
.collect();
(x_axis_label_string, x_axis_tick_string, x_axis_line_string, start_offset)
}
fn render_face_bars(h: &histogram::Histogram, face_width: u32, face_height: u32) -> Vec<String> {
let bound_cells = bound_cell_offsets(&h, face_width);
let cell_bins = bins_for_cells(&bound_cells, face_width);
let cell_heights: Vec<_> = cell_bins.iter()
.map(|&bin| match bin {
None => 0,
Some(b) => {
value_to_axis_cell_offset(h.bin_counts[b as usize] as f64, &h.y_axis, face_height)
}
})
.collect();
let mut face_strings: Vec<String> = vec![];
for line in 1..face_height + 1 {
let mut line_string = String::new();
for column in 1..face_width as usize + 1 {
line_string.push(if bound_cells.contains(&(column as i32)) {
let b = cell_heights[column - 1].cmp(&(line as i32));
let a = cell_heights[column + 1].cmp(&(line as i32));
match b {
std::cmp::Ordering::Less => {
match a {
std::cmp::Ordering::Less => ' ',
std::cmp::Ordering::Equal => '-', std::cmp::Ordering::Greater => '|',
}
}
std::cmp::Ordering::Equal => {
match a {
std::cmp::Ordering::Less => '-', std::cmp::Ordering::Equal => '-', std::cmp::Ordering::Greater => '|', }
}
std::cmp::Ordering::Greater => {
match a {
std::cmp::Ordering::Less => '|',
std::cmp::Ordering::Equal => '|', std::cmp::Ordering::Greater => '|',
}
}
}
} else {
let bin_height_cells = cell_heights[column];
if bin_height_cells == line as i32 {
'-' } else {
' ' }
});
}
face_strings.push(line_string);
}
face_strings
}
fn render_face_points(s: &scatter::Scatter, face_width: u32, face_height: u32) -> Vec<String> {
let points: Vec<_> = s.data
.iter()
.map(|&(x, y)| {
(value_to_axis_cell_offset(x, &s.x_axis, face_width),
value_to_axis_cell_offset(y, &s.y_axis, face_height))
})
.collect();
let mut face_strings: Vec<String> = vec![];
for line in 1..face_height + 1 {
let mut line_string = String::new();
for column in 1..face_width as usize + 1 {
line_string.push(if points.contains(&(column as i32, line as i32)) {
'o'
} else {
' '
});
}
face_strings.push(line_string);
}
face_strings
}
pub fn draw_histogram(h: &histogram::Histogram) {
let face_width = 90;
let face_height = 30u32;
let (y_label_strings, y_tick_strings, y_axis_line_strings, longest_y_label_width) =
render_y_axis_strings(&h.y_axis, face_height);
let (x_axis_label_string, x_axis_tick_string, x_axis_line_string, start_offset) =
render_x_axis_strings(&h.x_axis, face_width);
let face_strings = render_face_bars(&h, face_width as u32, face_height);
let left_gutter_width = std::cmp::max(longest_y_label_width as i32 + 1,
start_offset.wrapping_neg()) as
usize;
for line in 0..face_height {
let cell_position = face_height - line;
let axis_label = &y_label_strings[cell_position as usize];
let axis_tick = &y_tick_strings[cell_position as usize];
let axis_line = &y_axis_line_strings[cell_position as usize];
let mut axis_label_and_tick = String::new();
axis_label_and_tick.push_str(axis_label);
axis_label_and_tick.push_str(axis_tick);
let face_line = &face_strings[(cell_position as usize) - 1];
println!("{:>left_gutter_width$}{}{}",
axis_label_and_tick,
axis_line,
face_line,
left_gutter_width = left_gutter_width);
}
let reduced_x_axis_line = x_axis_line_string[1..].to_string();
println!("{:>left_gutter_width$}{}{}",
y_label_strings[0],
y_axis_line_strings[0],
reduced_x_axis_line,
left_gutter_width = left_gutter_width);
println!("{:>left_gutter_width$}{:-<plot_width$}",
"",
x_axis_tick_string,
left_gutter_width = left_gutter_width,
plot_width = face_width as usize + 1);
println!("{:>start_offset$}{: <plot_width$}",
"",
x_axis_label_string,
start_offset = (left_gutter_width as i32 + start_offset) as usize,
plot_width = (face_width as i32 + 1 + start_offset) as usize);
}
pub fn draw_scatter(s: &scatter::Scatter) {
let face_width = 90;
let face_height = 30u32;
let (y_label_strings, y_tick_strings, y_axis_line_strings, longest_y_label_width) =
render_y_axis_strings(&s.y_axis, face_height);
let (x_axis_label_string, x_axis_tick_string, x_axis_line_string, start_offset) =
render_x_axis_strings(&s.x_axis, face_width);
let face_strings = render_face_points(&s, face_width as u32, face_height);
let left_gutter_width = std::cmp::max(longest_y_label_width as i32 + 1,
start_offset.wrapping_neg()) as
usize;
for line in 0..face_height {
let cell_position = face_height - line;
let axis_label = &y_label_strings[cell_position as usize];
let axis_tick = &y_tick_strings[cell_position as usize];
let axis_line = &y_axis_line_strings[cell_position as usize];
let mut axis_label_and_tick = String::new();
axis_label_and_tick.push_str(axis_label);
axis_label_and_tick.push_str(axis_tick);
let face_line = &face_strings[(cell_position as usize) - 1];
println!("{:>left_gutter_width$}{}{}",
axis_label_and_tick,
axis_line,
face_line,
left_gutter_width = left_gutter_width);
}
let reduced_x_axis_line = x_axis_line_string[1..].to_string();
println!("{:>left_gutter_width$}{}{}",
y_label_strings[0],
y_axis_line_strings[0],
reduced_x_axis_line,
left_gutter_width = left_gutter_width);
println!("{:>left_gutter_width$}{:-<plot_width$}",
"",
x_axis_tick_string,
left_gutter_width = left_gutter_width,
plot_width = face_width as usize + 1);
println!("{:>start_offset$}{: <plot_width$}",
"",
x_axis_label_string,
start_offset = (left_gutter_width as i32 + start_offset) as usize,
plot_width = (face_width as i32 + 1 + start_offset) as usize);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bins_for_cells() {
let face_width = 10;
let n = i32::max_value();
let run_bins_for_cells = |bound_cell_offsets: &[i32]| -> Vec<_> {
bins_for_cells(&bound_cell_offsets, face_width)
.iter()
.map(|&a| a.unwrap_or(n))
.collect()
};
assert_eq!(run_bins_for_cells(&vec![-4, -1, 4, 7, 10]),
[1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, n]);
assert_eq!(run_bins_for_cells(&vec![0, 2, 4, 8, 10]),
[n, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, n]);
assert_eq!(run_bins_for_cells(&vec![3, 5, 7, 9, 10]),
[n, n, n, n, 0, 0, 1, 1, 2, 2, 3, n]);
assert_eq!(run_bins_for_cells(&vec![0, 2, 4, 6, 8]),
[n, 0, 0, 1, 1, 2, 2, 3, 3, n, n, n]);
assert_eq!(run_bins_for_cells(&vec![0, 3, 6, 9, 12]),
[n, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3]);
assert_eq!(run_bins_for_cells(&vec![-5, -4, -3, -1, 0]),
[3, n, n, n, n, n, n, n, n, n, n, n]);
assert_eq!(run_bins_for_cells(&vec![10, 12, 14, 16, 18]),
[n, n, n, n, n, n, n, n, n, n, n, 0]);
assert_eq!(run_bins_for_cells(&vec![15, 16, 17, 18, 19]),
[n, n, n, n, n, n, n, n, n, n, n, n]);
assert_eq!(run_bins_for_cells(&vec![-19, -18, -17, -16, -1]),
[n, n, n, n, n, n, n, n, n, n, n, n]);
}
#[test]
fn test_value_to_axis_cell_offset() {
assert_eq!(value_to_axis_cell_offset(3.0, &axis::Axis::new(5.0, 10.0), 10),
-4);
}
#[test]
fn test_x_axis_label() {
let l = XAxisLabel {
text: "3".to_string(),
offset: 2,
};
assert_eq!(l.len(), 1);
assert!(l.footprint() % 2 != 0);
assert_eq!(l.start_offset(), 2);
let l = XAxisLabel {
text: "34".to_string(),
offset: 2,
};
assert_eq!(l.len(), 2);
assert!(l.footprint() % 2 != 0);
assert_eq!(l.start_offset(), 1);
let l = XAxisLabel {
text: "345".to_string(),
offset: 2,
};
assert_eq!(l.len(), 3);
assert!(l.footprint() % 2 != 0);
assert_eq!(l.start_offset(), 1);
let l = XAxisLabel {
text: "3454".to_string(),
offset: 1,
};
assert_eq!(l.len(), 4);
assert!(l.footprint() % 2 != 0);
assert_eq!(l.start_offset(), -1);
}
}