use plotters::prelude::*;
#[derive(Clone)]
pub struct PlotConfig {
pub width: u32,
pub height: u32,
pub title: String,
pub xlabel: String,
pub ylabel: String,
pub line_color: RGBColor,
pub species_colors: Option<Vec<RGBColor>>,
pub background: RGBColor,
pub line_width: u32,
pub show_grid: bool,
}
impl Default for PlotConfig {
fn default() -> Self {
Self {
width: 1024,
height: 768,
title: "Plot".to_string(),
xlabel: String::new(), ylabel: "Concentration (mol/L)".to_string(),
line_color: RED,
species_colors: None,
background: WHITE,
line_width: 2,
show_grid: true,
}
}
}
pub trait IntoOptionalTitle {
fn into_optional_title(self) -> Option<String>;
}
impl IntoOptionalTitle for &str {
fn into_optional_title(self) -> Option<String> {
Some(self.to_string())
}
}
impl IntoOptionalTitle for String {
fn into_optional_title(self) -> Option<String> {
Some(self)
}
}
impl<T: IntoOptionalTitle> IntoOptionalTitle for Option<T> {
fn into_optional_title(self) -> Option<String> {
self.and_then(|t| t.into_optional_title())
}
}
pub const NO_TITLE: Option<&str> = None;
impl PlotConfig {
#[allow(clippy::field_reassign_with_default)]
pub fn chromatogram(title: impl IntoOptionalTitle) -> Self {
let mut config = Self::default();
config.xlabel = "Time (s)".to_string();
config.title = title
.into_optional_title()
.unwrap_or_else(|| "Chromatogram".to_string());
config
}
#[allow(clippy::field_reassign_with_default)]
pub fn steady_state(title: impl IntoOptionalTitle) -> Self {
let mut config = Self::default();
config.xlabel = "Position (m)".to_string();
config.title = title
.into_optional_title()
.unwrap_or_else(|| "Spatial Profile".to_string());
config
}
#[allow(clippy::field_reassign_with_default)]
pub fn multi_species_colors(colors: Vec<RGBColor>) -> Self {
let mut config = Self::default();
config.species_colors = Some(colors);
config
}
pub(crate) fn get_species_color(&self, species_index: usize) -> RGBColor {
if let Some(ref colors) = self.species_colors
&& species_index < colors.len()
{
return colors[species_index];
}
let default_colors = vec![
RED,
BLUE,
GREEN,
MAGENTA,
CYAN,
BLACK,
RGBColor(255, 165, 0), RGBColor(128, 0, 128), RGBColor(255, 192, 203), RGBColor(165, 42, 42), ];
default_colors[species_index % default_colors.len()]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plot_config_default() {
let config = PlotConfig::default();
assert_eq!(config.width, 1024);
assert_eq!(config.height, 768);
assert!(config.show_grid);
}
#[test]
fn test_chromatogram_config_default() {
let config = PlotConfig::chromatogram(NO_TITLE);
assert_eq!(config.xlabel, "Time (s)");
assert_eq!(config.title, "Chromatogram");
}
#[test]
fn test_chromatogram_config_with_str() {
let config = PlotConfig::chromatogram("TFA Elution");
assert_eq!(config.xlabel, "Time (s)");
assert_eq!(config.title, "TFA Elution");
}
#[test]
fn test_chromatogram_config_with_string() {
let title = format!("TFA: {}", "Gaussian");
let config = PlotConfig::chromatogram(title);
assert_eq!(config.xlabel, "Time (s)");
assert_eq!(config.title, "TFA: Gaussian");
}
#[test]
fn test_steady_state_config_default() {
let config = PlotConfig::steady_state(NO_TITLE);
assert_eq!(config.xlabel, "Position (m)");
assert_eq!(config.title, "Spatial Profile");
}
#[test]
fn test_steady_state_config_with_title() {
let config = PlotConfig::steady_state("Final Equilibrium");
assert_eq!(config.xlabel, "Position (m)");
assert_eq!(config.title, "Final Equilibrium");
}
#[test]
fn test_get_species_color_default_palette() {
let config = PlotConfig::default();
assert_eq!(config.get_species_color(0), RED);
assert_eq!(config.get_species_color(1), BLUE);
assert_eq!(config.get_species_color(10), RED); }
#[test]
fn test_get_species_color_custom() {
use plotters::style::full_palette::{LIGHTBLUE, LIGHTGREEN, ORANGE};
let config = PlotConfig::multi_species_colors(vec![ORANGE, LIGHTGREEN, LIGHTBLUE]);
assert_eq!(config.get_species_color(0), ORANGE);
assert_eq!(config.get_species_color(1), LIGHTGREEN);
assert_eq!(config.get_species_color(2), LIGHTBLUE);
}
}