extern crate open;
use std::fs::File;
use std::io::Write;
use std::error::Error;
use crate::{
Color,
PlotResult
};
use crate::err::PlotError;
use crate::style::PlotStyle;
use super::{
draw_line,
draw_text
};
pub enum Location {
Northwest
}
pub struct Scatterline {
width: usize,
height: usize,
axis_pad: usize,
x_label: String,
x_limits: Option<[f32; 2]>,
y_label: String,
y_limits: Option<[f32; 2]>,
title: String,
anno_style: PlotStyle,
x_dataset: Vec<Vec<f32>>,
y_dataset: Vec<Vec<f32>>,
legend_names: Option<Vec<String>>,
plot_styles: Vec<PlotStyle>,
max_ticks: usize,
axis_equal: bool
}
impl Default for Scatterline {
fn default() -> Scatterline {
Scatterline {
width: 1000,
height: 750,
axis_pad: 50,
x_label: "".to_string(),
x_limits: None,
y_label: "".to_string(),
y_limits: None,
title: "".to_string(),
anno_style: PlotStyle::default(),
x_dataset: Vec::default(),
y_dataset: Vec::default(),
legend_names: None,
plot_styles: Vec::default(),
max_ticks: 11,
axis_equal: false
}
}
}
impl Scatterline {
pub fn label_x(&mut self, new_label: &str) {
self.x_label = new_label.to_string();
}
pub fn label_y(&mut self, new_label: &str) {
self.y_label = new_label.to_string();
}
pub fn title(&mut self, new_title: &str) {
self.title = new_title.to_string();
}
pub fn add_data(&mut self, x_data: &Vec<f32>, y_data: &Vec<f32>, plot_style: &PlotStyle) -> PlotResult<()> {
if x_data.len() != y_data.len() {
return Err(PlotError::DataLengthError)
}
self.x_dataset.push(x_data.clone());
self.y_dataset.push(y_data.clone());
self.plot_styles.push(plot_style.clone());
Ok(())
}
pub fn set_xlims(&mut self, lower_lim: f32, upper_lim: f32) {
self.x_limits = Some([lower_lim, upper_lim]);
}
pub fn set_ylims(&mut self, lower_lim: f32, upper_lim: f32) {
self.y_limits = Some([lower_lim, upper_lim]);
}
pub fn axis_equal(&mut self) {
self.axis_equal = true
}
pub fn axis_auto(&mut self) {
self.axis_equal = false
}
pub fn assign_legend(&mut self, legend_names: &Vec<String>) -> PlotResult<()> {
if legend_names.len() != self.x_dataset.len() {
return Err(PlotError::DataLengthError)
}
self.legend_names = Some(legend_names.clone());
Ok(())
}
pub fn render(&self, file_name: &str) -> Result<(), Box<dyn Error>> {
let point_r: usize = 3;
let mut render_string: String = format!(r#"<!DOCTYPE svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="-50 -50 {} {}" width="{}" height="{}">"#, self.width + 50, self.height + 50, self.width, self.height);
draw_text(&mut render_string, self.width / 2, self.axis_pad / 2, 0, &self.title, &self.anno_style, "xx-large");
let n_ticks: usize = (&self.x_dataset).into_iter()
.fold(usize::MIN, |left, right| left.max(right.len() as usize))
.min(self.max_ticks);
let x_data_min: f32 = (&self.x_dataset).into_iter()
.map(|x_set| x_set.into_iter()
.fold(f32::INFINITY, |left, &right| left.min(right))
).fold(f32::INFINITY, |left, right| left.min(right));
let mut x_data_max: f32 = (&self.x_dataset).into_iter()
.map(|x_set| x_set.into_iter()
.fold(f32::NEG_INFINITY, |left, &right| left.max(right))
).fold(f32::NEG_INFINITY, |left, right| left.max(right));
let y_data_min: f32 = (&self.y_dataset).into_iter()
.map(|y_set| y_set.into_iter()
.fold(f32::INFINITY, |left, &right| left.min(right))
).fold(f32::INFINITY, |left, right| left.min(right));
let mut y_data_max: f32 = (&self.y_dataset).into_iter()
.map(|y_set| y_set.into_iter()
.fold(f32::NEG_INFINITY, |left, &right| left.max(right))
).fold(f32::NEG_INFINITY, |left, right| left.max(right));
if self.axis_equal {
let aspect_ratio: f32 = (self.width as f32 - 2.0 * self.axis_pad as f32) / (self.height as f32 - 2.0 * self.axis_pad as f32);
let x_range: f32 = x_data_max - x_data_min;
let y_range: f32 = y_data_max - y_data_min;
if x_range < (y_range * aspect_ratio) {
x_data_max = y_range * aspect_ratio + x_data_min;
} else {
y_data_max = x_range / aspect_ratio + y_data_min;
}
}
let x_bounds: [f32; 2] = self.x_limits.unwrap_or([x_data_min, x_data_max]);
let y_bounds: [f32; 2] = self.y_limits.unwrap_or([y_data_min, y_data_max]);
let x_abs_min: f32 = x_bounds[0];
let x_abs_max: f32 = x_bounds[1];
let y_abs_min: f32 = y_bounds[0];
let y_abs_max: f32 = y_bounds[1];
self.draw_axes(&mut render_string, x_abs_min, x_abs_max, y_abs_min, y_abs_max, n_ticks, n_ticks);
render_string.push_str(&format!(r#"<svg width="{}" height="{}" x="{}" y="{}">"#, self.width - 2 * self.axis_pad, self.height - 2 * self.axis_pad, self.axis_pad, self.axis_pad));
let origin_line_style: PlotStyle = PlotStyle {
stroke_color: Color::LightGray,
..Default::default()
};
if (x_abs_min < 0.0) && (0.0 < x_abs_max) {
let origin_x: usize = ((self.width - 2 * self.axis_pad) as f32 * (-x_bounds[0] / (x_bounds[1] - x_bounds[0]))) as usize;
draw_line(&mut render_string, origin_x, 0, origin_x, self.height - self.axis_pad, &origin_line_style);
}
if (y_abs_min < 0.0) && (0.0 < y_abs_max) {
println!("{:?}", y_bounds);
let origin_y: usize = ((self.height - 2 * self.axis_pad) as f32 - (self.height - 2 * self.axis_pad) as f32 * (-y_bounds[0] / (y_bounds[1] - y_bounds[0]))) as usize;
draw_line(&mut render_string, 0, origin_y, self.width - self.axis_pad, origin_y, &origin_line_style);
}
for data_idx in 0..self.x_dataset.len() {
let x_data: &Vec<f32> = &self.x_dataset[data_idx];
let y_data: &Vec<f32> = &self.y_dataset[data_idx];
let mapped_x: Vec<usize> = (0..x_data.len()).into_iter()
.map(|idx| ((self.width - 2 * self.axis_pad) as f32 * (x_data[idx] - x_abs_min) / (x_abs_max - x_abs_min)) as usize)
.collect::<Vec<usize>>();
let mapped_y: Vec<usize> = (0..x_data.len()).into_iter()
.map(|idx| (self.height - self.axis_pad) - (self.axis_pad as f32 + (self.height - 2 * self.axis_pad) as f32 * (y_data[idx] - y_abs_min) / (y_abs_max - y_abs_min)) as usize)
.collect::<Vec<usize>>();
if self.plot_styles[data_idx].has_markers {
let point_string: String = (0..x_data.len())
.map(|subidx| format!(r#"<circle r="{}" cx="{}" cy="{}" fill="{}" stroke="{}" stroke-width="1"><title>({}, {})</title></circle>"#,
point_r,
mapped_x[subidx],
mapped_y[subidx],
self.plot_styles[data_idx].stroke_color,
self.plot_styles[data_idx].stroke_color,
x_data[subidx],
y_data[subidx],
)).collect::<String>();
render_string.push_str(&point_string);
}
if self.plot_styles[data_idx].stroke_width > 0 {
render_string.push_str(&format!(r#"<polyline fill="none" stroke="{}" stroke-width="{}" points=" "#,
self.plot_styles[data_idx].stroke_color,
self.plot_styles[data_idx].stroke_width
));
let polyline_points_string: String = (0..mapped_x.len())
.map(|subidx| format!("{},{} ", mapped_x[subidx], mapped_y[subidx]))
.collect::<String>();
render_string.push_str(&polyline_points_string);
render_string.push_str("\"/>");
}
}
if self.legend_names.is_some() {
self.draw_legend(&mut render_string, Location::Northwest);
}
render_string.push_str(&format!("</svg></svg>"));
{
let mut output_svg: File = File::create(format!("{}.svg", file_name))?;
output_svg.write_all(render_string.as_bytes())?;
}
open::that(format!("./{}.svg", file_name))?;
Ok(())
}
fn draw_axes(&self, mut render_string: &mut String, x_start: f32, x_end: f32, y_start: f32, y_end: f32, n_x_ticks: usize, n_y_ticks: usize) {
let tick_r: usize = 6;
draw_line(&mut render_string, self.axis_pad, self.height - self.axis_pad, self.width - self.axis_pad, self.height - self.axis_pad, &self.anno_style);
draw_line(&mut render_string, self.axis_pad, self.height - self.axis_pad, self.axis_pad, self.axis_pad, &self.anno_style);
let x_label_x: usize = self.axis_pad + (self.width - 2 * self.axis_pad) / 2;
let y_label_y: usize = self.axis_pad + (self.height - 2 * self.axis_pad) / 2;
draw_text(&mut render_string, x_label_x, self.height - 10, 0, &self.x_label, &self.anno_style, "large");
draw_text(&mut render_string, 10, y_label_y, 270, &self.y_label, &self.anno_style, "large");
for x_tick in 0..n_x_ticks {
let progression: f32 = x_tick as f32 / (n_x_ticks - 1) as f32;
let x_tick_loc: usize = self.axis_pad + (progression * (self.width - 2 * self.axis_pad) as f32) as usize;
let x_tick_val: f32 = x_start + progression * (x_end - x_start);
draw_line(&mut render_string, x_tick_loc, self.height - self.axis_pad + tick_r, x_tick_loc, self.height - self.axis_pad - tick_r, &self.anno_style);
draw_text(&mut render_string, x_tick_loc, self.height - self.axis_pad + 3 * tick_r, 0, &format!("{:.2}", x_tick_val), &self.anno_style, "medium");
}
for y_tick in 0..n_y_ticks {
let progression: f32 = y_tick as f32 / (n_y_ticks - 1) as f32;
let y_tick_loc: usize = self.height - (self.axis_pad + (progression * (self.height - 2 * self.axis_pad) as f32) as usize);
let y_tick_val: f32 = y_start + progression * (y_end - y_start);
draw_line(&mut render_string, self.axis_pad - tick_r, y_tick_loc, self.axis_pad + tick_r, y_tick_loc, &self.anno_style);
draw_text(&mut render_string, self.axis_pad - 3 * tick_r, y_tick_loc, 270, &format!("{:.2}", y_tick_val), &self.anno_style, "medium");
}
}
fn draw_legend(&self, render_string: &mut String, location: Location) {
let entries: &Vec<String> = self.legend_names.as_ref().unwrap();
let entry_height: usize = 20;
let char_width: usize = 8;
let max_entry_length: usize = entries.into_iter()
.fold(usize::MIN, |left, right| left.max(right.len() as usize));
let (legend_x_loc, legend_y_loc) = match location {
Location::Northwest =>
(20, 20)
};
render_string.push_str(&format!(r#"<rect x="{}" y="{}" width="{}" height="{}" fill="none" stroke="{}" stroke-width="{}"/>"#,
legend_x_loc,
legend_y_loc,
entry_height + char_width * max_entry_length + entry_height / 2,
entry_height * self.legend_names.as_ref().unwrap().len() as usize,
self.anno_style.stroke_color,
self.anno_style.stroke_width
));
for entry_idx in 0..entries.len() {
render_string.push_str(&format!(r#"<rect x="{}" y="{}" width="{}" height="{}" fill="{}" stroke="none" />"#,
legend_x_loc,
legend_y_loc + entry_idx as usize * entry_height,
entry_height,
entry_height,
self.plot_styles[entry_idx].stroke_color
));
let entry_string: &String = &entries[entry_idx];
render_string.push_str(&format!(r#"<text x="{}" y="{}" color="{}" dominant-baseline="middle">{}</text>"#,
legend_x_loc + entry_height + entry_height / 4,
legend_y_loc + entry_idx as usize * entry_height + entry_height / 2,
self.anno_style.stroke_color,
entry_string
));
}
}
}