use crate::render::{Color, LineStyle};
#[derive(Debug, Clone)]
pub struct Theme {
pub background: Color,
pub foreground: Color,
pub grid_color: Color,
pub line_width: f32,
pub line_style: LineStyle,
pub font_family: String,
pub font_size: f32,
pub title_font_size: f32,
pub legend_font_size: f32,
pub axis_label_font_size: f32,
pub tick_label_font_size: f32,
pub color_palette: Vec<Color>,
pub margin: f32,
pub padding: f32,
pub colorblind_friendly: bool,
}
impl Theme {
pub fn builder() -> ThemeBuilder {
ThemeBuilder::default()
}
pub fn light() -> Self {
Self {
background: Color::WHITE,
foreground: Color::BLACK,
grid_color: Color::LIGHT_GRAY,
line_width: 1.5, line_style: LineStyle::Solid,
font_family: "sans-serif".to_string(),
font_size: 10.0, title_font_size: 14.0, legend_font_size: 9.0, axis_label_font_size: 10.0, tick_label_font_size: 9.0, color_palette: Color::default_palette().to_vec(),
margin: 0.1,
padding: 8.0,
colorblind_friendly: false,
}
}
pub fn dark() -> Self {
Self {
background: Color::from_hex("#1e1e1e").unwrap(),
foreground: Color::WHITE,
grid_color: Color::DARK_GRAY,
line_width: 1.5, line_style: LineStyle::Solid,
font_family: "sans-serif".to_string(),
font_size: 10.0, title_font_size: 14.0, legend_font_size: 9.0, axis_label_font_size: 10.0, tick_label_font_size: 9.0, color_palette: Self::dark_palette(),
margin: 0.1,
padding: 8.0,
colorblind_friendly: false,
}
}
pub fn publication() -> Self {
Self {
background: Color::WHITE,
foreground: Color::BLACK,
grid_color: Color::from_hex("#E0E0E0").unwrap(),
line_width: 1.5,
line_style: LineStyle::Solid,
font_family: "Times New Roman".to_string(),
font_size: 10.0,
title_font_size: 12.0,
legend_font_size: 9.0,
axis_label_font_size: 10.0,
tick_label_font_size: 8.0,
color_palette: Self::publication_palette(),
margin: 0.08,
padding: 6.0,
colorblind_friendly: false,
}
}
pub fn minimal() -> Self {
Self {
background: Color::WHITE,
foreground: Color::BLACK,
grid_color: Color::TRANSPARENT,
line_width: 1.5,
line_style: LineStyle::Solid,
font_family: "Helvetica".to_string(),
font_size: 11.0,
title_font_size: 14.0,
legend_font_size: 10.0,
axis_label_font_size: 11.0,
tick_label_font_size: 9.0,
color_palette: Self::minimal_palette(),
margin: 0.05,
padding: 4.0,
colorblind_friendly: false,
}
}
pub fn colorblind_friendly() -> Self {
let mut theme = Self::light();
theme.color_palette = Self::colorblind_palette();
theme.colorblind_friendly = true;
theme
}
pub fn seaborn() -> Self {
Self {
background: Color::WHITE,
foreground: Color::from_hex("#262626").unwrap(), grid_color: Color::from_hex("#F0F0F0").unwrap(), line_width: 1.5,
line_style: LineStyle::Solid,
font_family: "DejaVu Sans".to_string(), font_size: 11.0,
title_font_size: 14.0,
legend_font_size: 10.0,
axis_label_font_size: 11.0,
tick_label_font_size: 9.0,
color_palette: Self::seaborn_palette(),
margin: 0.08,
padding: 8.0,
colorblind_friendly: false,
}
}
pub fn ieee() -> Self {
Self {
background: Color::WHITE,
foreground: Color::BLACK,
grid_color: Color::from_hex("#E5E5E5").unwrap(), line_width: 0.75, line_style: LineStyle::Solid,
font_family: "serif".to_string(), font_size: 8.0, title_font_size: 9.0, legend_font_size: 6.0, axis_label_font_size: 8.0, tick_label_font_size: 7.0, color_palette: Self::wong_palette(), margin: 0.12, padding: 6.0,
colorblind_friendly: true,
}
}
pub fn nature() -> Self {
Self {
background: Color::WHITE,
foreground: Color::BLACK,
grid_color: Color::TRANSPARENT, line_width: 0.75, line_style: LineStyle::Solid,
font_family: "sans-serif".to_string(), font_size: 7.0, title_font_size: 8.05, legend_font_size: 5.95, axis_label_font_size: 7.0, tick_label_font_size: 5.95, color_palette: Self::scientific_palette(), margin: 0.08, padding: 4.0,
colorblind_friendly: false,
}
}
pub fn presentation() -> Self {
Self {
background: Color::WHITE,
foreground: Color::BLACK,
grid_color: Color::from_hex("#CCCCCC").unwrap(), line_width: 2.5, line_style: LineStyle::Solid,
font_family: "sans-serif".to_string(), font_size: 14.0, title_font_size: 19.6, legend_font_size: 11.9, axis_label_font_size: 14.0, tick_label_font_size: 11.9, color_palette: Self::presentation_palette(), margin: 0.15, padding: 12.0,
colorblind_friendly: false,
}
}
pub fn paul_tol() -> Self {
Self {
background: Color::WHITE,
foreground: Color::BLACK,
grid_color: Color::from_hex("#E0E0E0").unwrap(),
line_width: 1.5,
line_style: LineStyle::Solid,
font_family: "Arial".to_string(),
font_size: 11.0,
title_font_size: 14.0,
legend_font_size: 10.0,
axis_label_font_size: 11.0,
tick_label_font_size: 9.0,
color_palette: Self::paul_tol_palette(),
margin: 0.1,
padding: 8.0,
colorblind_friendly: true,
}
}
pub fn get_color(&self, index: usize) -> Color {
if self.color_palette.is_empty() {
Color::BLACK
} else {
self.color_palette[index % self.color_palette.len()]
}
}
pub fn effective_grid_color(&self) -> Color {
self.grid_color
}
fn dark_palette() -> Vec<Color> {
vec![
Color::from_hex("#8dd3c7").unwrap(), Color::from_hex("#ffffb3").unwrap(), Color::from_hex("#bebada").unwrap(), Color::from_hex("#fb8072").unwrap(), Color::from_hex("#80b1d3").unwrap(), Color::from_hex("#fdb462").unwrap(), Color::from_hex("#b3de69").unwrap(), Color::from_hex("#fccde5").unwrap(), ]
}
fn publication_palette() -> Vec<Color> {
vec![
Color::BLACK,
Color::DARK_GRAY,
Color::from_hex("#404040").unwrap(),
Color::from_hex("#606060").unwrap(),
Color::from_hex("#808080").unwrap(),
Color::from_hex("#A0A0A0").unwrap(),
]
}
fn minimal_palette() -> Vec<Color> {
vec![
Color::BLACK,
Color::from_hex("#666666").unwrap(),
Color::from_hex("#999999").unwrap(),
Color::from_hex("#CCCCCC").unwrap(),
]
}
fn colorblind_palette() -> Vec<Color> {
vec![
Color::from_hex("#1f77b4").unwrap(), Color::from_hex("#ff7f0e").unwrap(), Color::from_hex("#2ca02c").unwrap(), Color::from_hex("#d62728").unwrap(), Color::from_hex("#9467bd").unwrap(), Color::from_hex("#8c564b").unwrap(), Color::from_hex("#e377c2").unwrap(), Color::from_hex("#bcbd22").unwrap(), Color::from_hex("#17becf").unwrap(), ]
}
fn wong_palette() -> Vec<Color> {
vec![
Color::from_hex("#000000").unwrap(), Color::from_hex("#E69F00").unwrap(), Color::from_hex("#56B4E9").unwrap(), Color::from_hex("#009E73").unwrap(), Color::from_hex("#F0E442").unwrap(), Color::from_hex("#0072B2").unwrap(), Color::from_hex("#D55E00").unwrap(), Color::from_hex("#CC79A7").unwrap(), ]
}
fn paul_tol_palette() -> Vec<Color> {
vec![
Color::from_hex("#004488").unwrap(), Color::from_hex("#DDAA33").unwrap(), Color::from_hex("#BB5566").unwrap(), Color::from_hex("#000000").unwrap(), Color::from_hex("#999933").unwrap(), Color::from_hex("#DDDDDD").unwrap(), Color::from_hex("#EE8866").unwrap(), Color::from_hex("#77AADD").unwrap(), ]
}
fn scientific_palette() -> Vec<Color> {
vec![
Color::from_hex("#1f77b4").unwrap(), Color::from_hex("#ff7f0e").unwrap(), Color::from_hex("#2ca02c").unwrap(), Color::from_hex("#d62728").unwrap(), Color::from_hex("#9467bd").unwrap(), Color::from_hex("#8c564b").unwrap(), Color::from_hex("#e377c2").unwrap(), Color::from_hex("#7f7f7f").unwrap(), Color::from_hex("#bcbd22").unwrap(), Color::from_hex("#17becf").unwrap(), ]
}
fn presentation_palette() -> Vec<Color> {
vec![
Color::from_hex("#1f77b4").unwrap(), Color::from_hex("#ff7f0e").unwrap(), Color::from_hex("#2ca02c").unwrap(), Color::from_hex("#d62728").unwrap(), Color::from_hex("#9467bd").unwrap(), Color::from_hex("#000000").unwrap(), ]
}
fn seaborn_palette() -> Vec<Color> {
vec![
Color::from_hex("#1f77b4").unwrap(), Color::from_hex("#ff7f0e").unwrap(), Color::from_hex("#2ca02c").unwrap(), Color::from_hex("#d62728").unwrap(), Color::from_hex("#9467bd").unwrap(), Color::from_hex("#8c564b").unwrap(), Color::from_hex("#e377c2").unwrap(), Color::from_hex("#7f7f7f").unwrap(), Color::from_hex("#bcbd22").unwrap(), Color::from_hex("#17becf").unwrap(), ]
}
pub fn to_typography_config(&self) -> crate::core::config::TypographyConfig {
use super::FontFamily;
use crate::core::config::TypographyConfig;
let family = match self.font_family.to_lowercase().as_str() {
s if s.contains("serif") && !s.contains("sans") => FontFamily::Serif,
s if s.contains("mono") || s.contains("courier") || s.contains("consolas") => {
FontFamily::Monospace
}
_ => FontFamily::SansSerif,
};
TypographyConfig {
base_size: self.font_size,
title_scale: self.title_font_size / self.font_size,
label_scale: self.axis_label_font_size / self.font_size,
tick_scale: self.tick_label_font_size / self.font_size,
legend_scale: self.legend_font_size / self.font_size,
family,
title_weight: super::FontWeight::Normal,
}
}
pub fn to_line_config(&self) -> crate::core::config::LineConfig {
use crate::core::config::LineConfig;
LineConfig {
data_width: self.line_width,
axis_width: self.line_width * 0.5,
grid_width: self.line_width * 0.3,
tick_width: self.line_width * 0.4,
tick_length: 4.0, }
}
}
impl Default for Theme {
fn default() -> Self {
Self::light()
}
}
#[derive(Debug, Clone)]
pub struct ThemeBuilder {
theme: Theme,
}
impl ThemeBuilder {
pub fn background(mut self, color: Color) -> Self {
self.theme.background = color;
self
}
pub fn foreground(mut self, color: Color) -> Self {
self.theme.foreground = color;
self
}
pub fn grid_color(mut self, color: Color) -> Self {
self.theme.grid_color = color;
self
}
pub fn line_width(mut self, width: f32) -> Self {
self.theme.line_width = width.max(0.1);
self
}
pub fn line_style(mut self, style: LineStyle) -> Self {
self.theme.line_style = style;
self
}
pub fn font<S: Into<String>>(mut self, font_family: S) -> Self {
self.theme.font_family = font_family.into();
self
}
pub fn font_size(mut self, size: f32) -> Self {
self.theme.font_size = size.max(6.0);
self
}
pub fn title_font_size(mut self, size: f32) -> Self {
self.theme.title_font_size = size.max(6.0);
self
}
pub fn legend_font_size(mut self, size: f32) -> Self {
self.theme.legend_font_size = size.max(6.0);
self
}
pub fn palette<I>(mut self, colors: I) -> Self
where
I: IntoIterator<Item = Color>,
{
self.theme.color_palette = colors.into_iter().collect();
self
}
pub fn colorblind_palette(mut self, enabled: bool) -> Self {
self.theme.colorblind_friendly = enabled;
if enabled {
self.theme.color_palette = Theme::colorblind_palette();
}
self
}
pub fn margin(mut self, margin: f32) -> Self {
self.theme.margin = margin.clamp(0.0, 0.5);
self
}
pub fn padding(mut self, padding: f32) -> Self {
self.theme.padding = padding.max(0.0);
self
}
pub fn build(self) -> Theme {
self.theme
}
}
#[allow(clippy::derivable_impls)] impl Default for ThemeBuilder {
fn default() -> Self {
Self {
theme: Theme::light(),
}
}
}
#[allow(clippy::upper_case_acronyms)] pub enum ThemeVariant {
Light,
Dark,
Publication,
Minimal,
ColorblindFriendly,
Seaborn,
IEEE,
Nature,
Presentation,
PaulTol,
}
impl ThemeVariant {
pub fn to_theme(&self) -> Theme {
match self {
ThemeVariant::Light => Theme::light(),
ThemeVariant::Dark => Theme::dark(),
ThemeVariant::Publication => Theme::publication(),
ThemeVariant::Minimal => Theme::minimal(),
ThemeVariant::ColorblindFriendly => Theme::colorblind_friendly(),
ThemeVariant::Seaborn => Theme::seaborn(),
ThemeVariant::IEEE => Theme::ieee(),
ThemeVariant::Nature => Theme::nature(),
ThemeVariant::Presentation => Theme::presentation(),
ThemeVariant::PaulTol => Theme::paul_tol(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_theme() {
let theme = Theme::default();
assert_eq!(theme.background, Color::WHITE);
assert_eq!(theme.foreground, Color::BLACK);
assert!(theme.font_size > 0.0);
assert!(!theme.color_palette.is_empty());
}
#[test]
fn test_theme_variants() {
let light = Theme::light();
let dark = Theme::dark();
let publication = Theme::publication();
let minimal = Theme::minimal();
let colorblind = Theme::colorblind_friendly();
assert_eq!(light.background, Color::WHITE);
assert_eq!(dark.background, Color::from_hex("#1e1e1e").unwrap());
assert_eq!(publication.font_family, "Times New Roman");
assert_eq!(minimal.font_family, "Helvetica");
assert!(colorblind.colorblind_friendly);
}
#[test]
fn test_scientific_themes() {
let ieee = Theme::ieee();
let nature = Theme::nature();
let presentation = Theme::presentation();
let paul_tol = Theme::paul_tol();
assert_eq!(ieee.font_family, "serif");
assert_eq!(ieee.font_size, 8.0);
assert!(ieee.colorblind_friendly);
assert!((ieee.line_width - 0.75).abs() < 0.01);
assert_eq!(nature.font_family, "sans-serif");
assert_eq!(nature.font_size, 7.0);
assert_eq!(nature.grid_color, Color::TRANSPARENT);
assert_eq!(presentation.font_size, 14.0);
assert!((presentation.line_width - 2.5).abs() < 0.01);
assert!((presentation.title_font_size - 19.6).abs() < 0.01);
assert!(paul_tol.colorblind_friendly);
assert_eq!(paul_tol.color_palette.len(), 8);
}
#[test]
fn test_scientific_color_palettes() {
let wong = Theme::wong_palette();
let paul_tol = Theme::paul_tol_palette();
let scientific = Theme::scientific_palette();
let presentation = Theme::presentation_palette();
assert_eq!(wong.len(), 8);
assert_eq!(wong[0], Color::from_hex("#000000").unwrap());
assert_eq!(paul_tol.len(), 8);
assert_eq!(paul_tol[0], Color::from_hex("#004488").unwrap());
assert_eq!(scientific.len(), 10);
assert_eq!(presentation.len(), 6);
}
#[test]
fn test_theme_builder() {
let theme = Theme::builder()
.background(Color::BLUE)
.foreground(Color::WHITE)
.font("Helvetica")
.font_size(14.0)
.line_width(3.0)
.margin(0.05)
.colorblind_palette(true)
.build();
assert_eq!(theme.background, Color::BLUE);
assert_eq!(theme.foreground, Color::WHITE);
assert_eq!(theme.font_family, "Helvetica");
assert_eq!(theme.font_size, 14.0);
assert_eq!(theme.line_width, 3.0);
assert_eq!(theme.margin, 0.05);
assert!(theme.colorblind_friendly);
}
#[test]
fn test_color_cycling() {
let theme = Theme::light();
let color0 = theme.get_color(0);
let color1 = theme.get_color(1);
let color_cycle = theme.get_color(theme.color_palette.len());
assert_eq!(color0, color_cycle); assert_ne!(color0, color1); }
#[test]
fn test_builder_validation() {
let theme = Theme::builder()
.font_size(-5.0) .line_width(-1.0) .margin(-0.1) .build();
assert!(theme.font_size >= 6.0); assert!(theme.line_width >= 0.1); assert!(theme.margin >= 0.0); }
#[test]
fn test_theme_variant_conversion() {
let light = ThemeVariant::Light.to_theme();
let dark = ThemeVariant::Dark.to_theme();
let ieee = ThemeVariant::IEEE.to_theme();
let nature = ThemeVariant::Nature.to_theme();
let presentation = ThemeVariant::Presentation.to_theme();
let paul_tol = ThemeVariant::PaulTol.to_theme();
assert_eq!(light.background, Color::WHITE);
assert_ne!(dark.background, Color::WHITE);
assert_eq!(ieee.font_family, "serif");
assert_eq!(nature.grid_color, Color::TRANSPARENT);
assert_eq!(presentation.font_size, 14.0);
assert!(paul_tol.colorblind_friendly);
}
#[test]
fn test_empty_palette() {
let mut theme = Theme::light();
theme.color_palette.clear();
assert_eq!(theme.get_color(0), Color::BLACK);
assert_eq!(theme.get_color(5), Color::BLACK);
}
}