use ron::de::from_reader;
use serde::Deserialize;
use std::fs::File;
use tracing::{debug, error, info};
mod data;
use crate::{
canvas::{
axes::axis_x::build_x_axis_label,
axes::axis_y::build_y_axis_label,
axes::{
axis_x::get_x_axis_pixel_length, axis_y::get_y_axis_pixel_length, draw_xy_axes,
get_xy_axis_pixel_min_max, get_xy_axis_pixel_origin,
},
best_fit::BestFit,
draw_base_canvas,
glyphs::FontSizes,
legend::build_legend,
plot::DataSymbol,
quadrants::get_quadrants,
save_image,
title::build_title,
VHConsumedCanvasSpace,
},
colours::*,
scatter::data::{build_data_points, get_data_bounds, get_legend_fields},
};
#[derive(Debug, Deserialize)]
struct Scatter {
title: String,
canvas_pixel_size: (u32, u32),
x_axis_label: String,
x_axis_resolution: u32,
y_axis_label: String,
y_axis_resolution: u32,
has_grid: bool,
has_legend: bool,
data_sets: Vec<DataSet>,
}
#[derive(Debug, Deserialize)]
pub struct DataSet {
data_path: String,
has_headers: bool,
x_axis_csv_column: usize,
x_axis_error_bar_csv_column: Option<usize>,
y_axis_csv_column: usize,
y_axis_error_bar_csv_column: Option<usize>,
name: String,
colour: Colour,
symbol: DataSymbol,
symbol_radius: u32,
symbol_thickness: u32,
best_fit: Option<BestFit>,
}
pub fn scatter_builder(path: &str, output: &str, csv_delimiter: &str) {
info!("Building scatter chart...");
let scatter: Scatter = Scatter::deserialise(path);
info!("Drawing canvas...");
let mut canvas = draw_base_canvas(scatter.canvas_pixel_size);
info!("Calculating font sizes...");
let font_sizes = FontSizes::new(&scatter.canvas_pixel_size);
let mut canvas_edges_used = VHConsumedCanvasSpace::new();
info!("Building title...");
canvas_edges_used.add(build_title(
&mut canvas,
&scatter.title,
font_sizes.title_font_size,
));
if scatter.has_legend {
let legend_fields = get_legend_fields(&scatter.data_sets);
let legend_origin_x = canvas.dimensions().0
- canvas_edges_used.h_space_from_right
- (canvas.dimensions().0 / 10);
let legend_origin_y = canvas_edges_used.v_space_from_top + canvas.dimensions().1 / 4;
canvas_edges_used.add(build_legend(
&mut canvas,
(legend_origin_x, legend_origin_y),
legend_fields,
font_sizes.legend_font_size,
));
}
info!("Finding min and max range of data...");
let (min_xy, max_xy): ((f32, f32), (f32, f32)) =
get_data_bounds(&scatter.data_sets, csv_delimiter);
let min_x_scaled = if min_xy.0.is_sign_positive() {
min_xy.0 / 1.1
} else {
min_xy.0 * 1.1
};
let min_y_scaled = if min_xy.1.is_sign_positive() {
min_xy.1 / 1.1
} else {
min_xy.1 * 1.1
};
let max_x_scaled = if max_xy.0.is_sign_positive() {
max_xy.0 * 1.1
} else {
max_xy.0 / 1.1
};
let max_y_scaled = if max_xy.1.is_sign_positive() {
max_xy.1 * 1.1
} else {
max_xy.1 / 1.1
};
let min_xy_scaled = (min_x_scaled as i32, min_y_scaled as i32);
debug!("Minimum x-y with buffer space {:?}", min_xy_scaled);
let max_xy_scaled = (max_x_scaled as i32, max_y_scaled as i32);
debug!("Maximum x-y with buffer space {:?}", max_xy_scaled);
let quadrants = get_quadrants(min_xy_scaled, max_xy_scaled);
info!("Quadrants to draw based on data set {:?}", quadrants);
info!("Building y-axis label...");
canvas_edges_used.add(build_y_axis_label(
&mut canvas,
scatter.y_axis_label,
font_sizes.axis_font_size,
&quadrants,
canvas_edges_used.v_space_from_top,
canvas_edges_used.h_space_from_right,
canvas_edges_used.v_space_from_bottom,
canvas_edges_used.h_space_from_left,
));
info!("Building x-axis label...");
canvas_edges_used.add(build_x_axis_label(
&mut canvas,
scatter.x_axis_label,
font_sizes.axis_font_size,
&quadrants,
canvas_edges_used.v_space_from_top,
canvas_edges_used.h_space_from_right,
canvas_edges_used.v_space_from_bottom,
canvas_edges_used.h_space_from_left,
));
let (axis_min, axis_max): ((u32, u32), (u32, u32)) = get_xy_axis_pixel_min_max(
&quadrants,
canvas_edges_used.v_space_from_top,
canvas_edges_used.h_space_from_right,
canvas_edges_used.v_space_from_bottom,
canvas_edges_used.h_space_from_left,
canvas.dimensions(),
scatter.x_axis_resolution,
scatter.y_axis_resolution,
);
debug!("Minimum axis placement {:?}", axis_min);
debug!("Maximun axis placement {:?}", axis_max);
let axis_origin: (u32, u32) = get_xy_axis_pixel_origin(&quadrants, axis_min, axis_max);
debug!("Origin axis placement {:?}", axis_origin);
let x_axis_length = get_x_axis_pixel_length(axis_min.0, axis_max.0);
let y_axis_length = get_y_axis_pixel_length(axis_max.1, axis_min.1);
debug!("X-axis length {}", x_axis_length);
debug!("Y-axis length {}", y_axis_length);
let x_data_min_max_limits: (i32, i32) = (min_xy_scaled.0, max_xy_scaled.0);
let y_data_min_max_limits: (i32, i32) = (min_xy_scaled.1, max_xy_scaled.1);
if !(max_xy_scaled.0 as f32 - min_xy_scaled.0 as f32).is_normal() {
error!("Difference between the smallest and largest x values have produced Zero, Infinite, NaN or a Subnormal value. Likely if your data set only contains a single row. Ensure you have multiple rows and that your largest x value minus your smallest x doesn't produce zero");
std::process::exit(1)
}
let x_axis_data_scale_factor: f32 =
x_axis_length as f32 / (max_xy_scaled.0 as f32 - min_xy_scaled.0 as f32).abs();
if !(max_xy_scaled.1 as f32 - min_xy_scaled.1 as f32).is_normal() {
error!("Difference between the smallest and largest y values have produced Zero, Infinite, NaN or a Subnormal value. Likely if your data set only contains a single row. Ensure you have multiple rows and that your largest y value minus your smallest y doesn't produce zero");
std::process::exit(1)
}
let y_axis_data_scale_factor: f32 =
y_axis_length as f32 / (max_xy_scaled.1 as f32 - min_xy_scaled.1 as f32).abs();
debug!("X-axis scale factor {}", x_axis_data_scale_factor);
debug!("Y-axis scale factor {}", y_axis_data_scale_factor);
draw_xy_axes(
&quadrants,
&mut canvas,
axis_origin,
axis_min,
axis_max,
x_axis_length,
y_axis_length,
x_data_min_max_limits,
y_data_min_max_limits,
font_sizes.axis_unit_font_size,
scatter.has_grid,
scatter.x_axis_resolution,
scatter.y_axis_resolution,
);
for set in &scatter.data_sets {
match &set.best_fit {
Some(curve) => {
info!("Plotting best fit...");
let points = curve.find_coordinates(
x_data_min_max_limits.0,
x_data_min_max_limits.1,
y_data_min_max_limits.0,
y_data_min_max_limits.1,
scatter.canvas_pixel_size.0 as i32 * 2,
);
let origin_offset = (axis_origin.0, axis_origin.1);
for p in points.iter() {
p.draw_point(
&mut canvas,
x_axis_data_scale_factor,
y_axis_data_scale_factor,
origin_offset,
);
}
}
None => {}
}
}
build_data_points(
&scatter.data_sets,
csv_delimiter,
&mut canvas,
x_axis_data_scale_factor,
y_axis_data_scale_factor,
(axis_origin.0, axis_origin.1),
);
save_image(canvas, output, scatter.title);
}
impl Scatter {
fn deserialise(path: &str) -> Scatter {
let f = match File::open(path) {
Ok(file) => file,
Err(e) => {
error!("Failed to open .ron file at {}, error: {:?}", path, e);
std::process::exit(1)
}
};
let scatter: Scatter = match from_reader(f) {
Ok(x) => x,
Err(e) => {
error!(
"Failed to load config, maybe you're missing a comma? Error: {}",
e
);
std::process::exit(1);
}
};
debug!("Ron config {:?}", &scatter);
scatter
}
}