mod svg;
type Percentage = f64;
const VERSION: &str = "0.7.3";
const REPOSITORY: &str = "https://github.com/emilbratt/eb_bars";
const DEFAULT_SIZE: (u32, u32) = (1600, 1000);
const DEFAULT_BAR_COLOR: &str = "rgb(112, 153, 182)";
const DEFAULT_BASE_COLOR: &str = "rgb(197, 197, 197)";
const DEFAULT_BAR_GAP: Percentage = 0.0;
const DEFAULT_BIN_GAP: Percentage = 10.0;
const DEFAULT_FONT_SIZE: Percentage = 100.0;
const DEFAULT_LEGEND_POSITION: (Percentage, Percentage) = (90.0, 20.0);
const DEFAULT_TEXT_SIDE_OFFSET: Percentage = 35.0;
const DEFAULT_TICK_LENGTH: Percentage = 10.0;
#[derive(Debug, Default)]
enum BinMarkerPosition {
Left,
#[default]
Middle,
Right,
}
#[derive(Debug, Default)]
struct PlotLegend<'a> {
categories: Option<&'a[&'a str]>,
position: Option<(Percentage, Percentage)>,
}
#[derive(Debug)]
enum BarColorLayout<'a> {
Category(Vec<&'a str>), Indexed(Vec<Vec<&'a str>>), Threshold((&'a str, &'a str, &'a str, &'a str)), Uniform(&'a str), }
impl Default for BarColorLayout<'_> {
fn default() -> Self {
Self::Uniform(DEFAULT_BAR_COLOR)
}
}
#[derive(Debug, Default)]
struct BarColors<'a> {
layout: BarColorLayout<'a>,
overrides: Vec<(usize, usize, &'a str)>,
}
#[derive(Debug)]
struct Colors<'a> {
background: Option<&'a str>,
bars: BarColors<'a>,
line: &'a str,
text: &'a str,
tick: &'a str,
}
impl Default for Colors<'_> {
fn default() -> Self {
Self {
background: None,
bars: BarColors::default(),
line: DEFAULT_BASE_COLOR,
text: DEFAULT_BASE_COLOR,
tick: DEFAULT_BASE_COLOR,
}
}
}
#[derive(Debug, Default)]
struct Show {
window_border: bool,
plot_border: bool,
horizontal_lines: bool,
vertical_lines: bool,
}
#[derive(Debug, Default)]
struct PlotText<'a> {
left: Option<&'a str>,
left_offset: Option<Percentage>,
right: Option<&'a str>,
right_offset: Option<Percentage>,
top: Option<&'a str>,
top_offset: Option<Percentage>,
bottom: Option<&'a str>,
bottom_offset: Option<Percentage>,
}
#[derive(Debug)]
struct PlotLayout {
bar_gap: Percentage,
bin_gap: Percentage,
bin_marker_position: BinMarkerPosition,
font_size: Percentage,
plot_window_scale: Option<(Percentage, Percentage, Percentage, Percentage)>,
scale_range: Option<(i64, i64, usize)>,
x_axis_tick_length: Percentage,
y_axis_tick_length: Percentage,
negative_bars_go_down: bool,
}
impl Default for PlotLayout {
fn default() -> Self {
Self {
bin_gap: DEFAULT_BIN_GAP,
bar_gap: DEFAULT_BAR_GAP,
bin_marker_position: BinMarkerPosition::default(),
font_size: DEFAULT_FONT_SIZE,
plot_window_scale: None,
scale_range: None,
x_axis_tick_length: DEFAULT_TICK_LENGTH,
y_axis_tick_length: DEFAULT_TICK_LENGTH,
negative_bars_go_down: false,
}
}
}
#[derive(Debug)]
enum LinesAt<'a> {
Horizontal(f64, &'a str),
Vertical(f64, &'a str),
}
#[derive(Debug)]
pub struct BarPlot<'a> {
values: Vec<&'a [f64]>,
markers: Option<&'a [&'a str]>,
lines_at: Vec<LinesAt<'a>>,
size: (u32, u32),
colors: Colors<'a>,
legend: PlotLegend<'a>,
layout: PlotLayout,
show: Show,
plot_text: PlotText<'a>,
}
#[allow(clippy::new_without_default)]
impl <'a>BarPlot<'a> {
pub fn new() -> Self {
Self {
values: Vec::new(),
markers: None,
lines_at: Vec::new(),
size: DEFAULT_SIZE,
colors: Colors::default(),
legend: PlotLegend::default(),
layout: PlotLayout::default(),
show: Show::default(),
plot_text: PlotText::default(),
}
}
pub fn add_values(&mut self, values: &'a [f64]) {
if !self.values.is_empty() {
let exp_count = self.values[0].len();
let count = values.len();
assert_eq!(
exp_count,
count,
"Added values should be same count as old, expected {exp_count}, got {count}"
);
}
self.values.push(values);
}
pub fn set_background_color(&mut self, color: &'a str) {
self.colors.background = Some(color);
}
pub fn set_line_color(&mut self, color: &'a str) {
self.colors.line = color;
}
pub fn set_text_color(&mut self, color: &'a str) {
self.colors.text = color;
}
pub fn set_tick_color(&mut self, color: &'a str) {
self.colors.tick = color;
}
pub fn set_bar_colors_by_uniform(&mut self, color: &'a str) {
self.colors.bars.layout = BarColorLayout::Uniform(color);
}
pub fn set_bar_colors_by_threshold(&mut self, min: &'a str, low: &'a str, high: &'a str, max: &'a str) {
self.colors.bars.layout = BarColorLayout::Threshold((min, low, high, max));
}
pub fn add_bar_colors_by_category(&mut self, color: &'a str) {
if let BarColorLayout::Category(v) = &mut self.colors.bars.layout {
v.push(color);
} else {
self.colors.bars.layout = BarColorLayout::Category(vec![color]);
}
}
pub fn add_bar_colors_from_vec(&mut self, colors: Vec<&'a str>) {
if let BarColorLayout::Indexed(v) = &mut self.colors.bars.layout {
v.push(colors);
} else {
self.colors.bars.layout = BarColorLayout::Indexed(vec![colors]);
}
}
pub fn add_bar_color_override(&mut self, category: usize, bar: usize, color: &'a str) {
assert!(
category < self.values.len(),
"{}\n{}",
format!("Can not add category index '{category}' as there are not enough categories added yet."),
"This may result in current override being ignored.",
);
self.colors.bars.overrides.push((category, bar, color));
}
pub fn set_show_horizontal_lines(&mut self) {
self.show.horizontal_lines = true;
}
pub fn add_horizontal_line_at(&mut self, p: Percentage, color: &'a str) {
self.lines_at.push(LinesAt::Horizontal(p, color));
}
pub fn set_show_vertical_lines(&mut self) {
self.show.vertical_lines = true;
}
pub fn add_vertical_line_at(&mut self, p: Percentage, color: &'a str) {
self.lines_at.push(LinesAt::Vertical(p, color));
}
pub fn set_plot_window_size(
&mut self,
x_length: Percentage,
x_offset: Percentage,
y_length: Percentage,
y_offset: Percentage
) {
assert!(x_length <= 100.0 && x_offset <= 100.0, "plot window width cannot exceed 100%");
assert!(y_length <= 100.0 && y_offset <= 100.0, "plot window height cannot exceed 100%");
self.layout.plot_window_scale = Some((x_length, x_offset, y_length, y_offset));
}
pub fn set_scale_range(&mut self, min: i64, max: i64, step: u64) {
self.layout.scale_range = Some((min, max, step as usize));
}
pub fn set_bin_markers(&mut self, markers: &'a [&'a str]) {
self.markers = Some(markers);
}
pub fn set_bin_markers_middle(&mut self) {
self.layout.bin_marker_position = BinMarkerPosition::Middle;
}
pub fn set_bin_markers_left(&mut self) {
self.layout.bin_marker_position = BinMarkerPosition::Left;
}
pub fn set_bin_markers_right(&mut self) {
self.layout.bin_marker_position = BinMarkerPosition::Right;
}
pub fn set_bar_gap(&mut self, gap: Percentage) {
self.layout.bar_gap = gap;
}
pub fn set_bin_gap(&mut self, gap: Percentage) {
self.layout.bin_gap = gap;
}
pub fn set_y_axis_tick_length(&mut self, p: Percentage) {
self.layout.y_axis_tick_length = p;
}
pub fn set_x_axis_tick_length(&mut self, p: Percentage) {
self.layout.x_axis_tick_length = p;
}
pub fn set_negative_bars_go_down(&mut self) {
self.layout.negative_bars_go_down = true;
}
pub fn set_text_left(&mut self, text: &'a str) {
self.plot_text.left = Some(text);
}
pub fn set_text_left_offset(&mut self, offset: Percentage) {
self.plot_text.left_offset = Some(offset);
}
pub fn set_text_right(&mut self, text: &'a str) {
self.plot_text.right = Some(text);
}
pub fn set_text_right_offset(&mut self, offset: Percentage) {
self.plot_text.right_offset = Some(offset);
}
pub fn set_text_bottom(&mut self, text: &'a str) {
self.plot_text.bottom = Some(text);
}
pub fn set_text_bottom_offset(&mut self, offset: Percentage) {
self.plot_text.bottom_offset = Some(offset);
}
pub fn set_text_top(&mut self, text: &'a str) {
self.plot_text.top = Some(text);
}
pub fn set_text_top_offset(&mut self, offset: Percentage) {
self.plot_text.top_offset = Some(offset);
}
pub fn set_legend(&mut self, categories: &'a [&'a str]) {
self.legend.categories = Some(categories);
}
pub fn set_legend_position(&mut self, x: Percentage, y: Percentage) {
self.legend.position = Some((x,y));
}
pub fn set_font_size(&mut self, p: Percentage) {
self.layout.font_size = p;
}
pub fn set_show_window_border(&mut self) {
self.show.window_border = true;
}
pub fn set_show_plot_border(&mut self) {
self.show.plot_border = true;
}
pub fn to_svg(&mut self, width: u32, height: u32) -> String {
assert!(!self.values.is_empty(), "Can not generate plot without any values..");
self.size = (width, height);
let n_categories = self.values.len();
match &mut self.colors.bars.layout {
BarColorLayout::Category(colors) => {
let n_colors = colors.len();
assert_eq!(
n_categories,
n_colors,
"Got {n_categories} categories and {n_colors} colors.",
);
}
BarColorLayout::Indexed(matrix) => {
let n_color_vectors = matrix.len();
assert_eq!(
n_categories,
n_color_vectors,
"Got {n_categories} categories and {n_color_vectors} color vectors.",
);
for (i, colors) in matrix.iter().enumerate() {
let values = self.values[i].len();
let n_colors = colors.len();
assert_eq!(
values,
n_colors,
"Got {values} values and {n_colors} colors for category {i}.",
);
}
}
_ => (),
}
svg::render(self)
}
}
#[cfg(test)]
mod tests {
use std::fs;
use toml;
use super::{VERSION, REPOSITORY};
#[test]
fn version_and_repo() {
let contents = fs::read_to_string("Cargo.toml").unwrap();
let value = contents.parse::<toml::Table>().unwrap();
let version = value["package"]["version"].as_str().unwrap();
assert_eq!(version, VERSION);
let repository = value["package"]["repository"].as_str().unwrap();
assert_eq!(repository, REPOSITORY);
}
}