use crate::primitives::{Color, FontWeight};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum LineStyle {
Solid,
Dashed,
Dotted,
DashDot,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Marker {
Circle,
Square,
Triangle,
Diamond,
Plus,
Cross,
Star,
Point,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Loc {
Best,
UpperRight,
UpperLeft,
LowerLeft,
LowerRight,
Right,
CenterLeft,
CenterRight,
LowerCenter,
UpperCenter,
Center,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum GridAxis {
X,
Y,
#[default]
Both,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TickDirection {
Outward,
Inward,
}
#[derive(Debug, Clone)]
pub struct Theme {
pub figure_background: Color,
pub axes_background: Color,
pub grid_color: Color,
pub grid_width: f64,
pub show_grid: bool,
pub spine_color: Color,
pub spine_width: f64,
pub show_top_spine: bool,
pub show_right_spine: bool,
pub show_bottom_spine: bool,
pub show_left_spine: bool,
pub tick_color: Color,
pub tick_length: f64,
pub tick_direction: TickDirection,
pub tick_label_size: f64,
pub axis_label_size: f64,
pub title_size: f64,
pub title_weight: FontWeight,
pub text_color: Color,
pub line_width: f64,
pub marker_size: f64,
pub marker_alpha: f64,
pub color_cycle: Vec<Color>,
pub font_family: Option<String>,
}
const TABLEAU_10: [Color; 10] = Color::TABLEAU_10;
impl Default for Theme {
fn default() -> Self {
let spine = Color::rgb(0x33, 0x33, 0x33);
Self {
figure_background: Color::WHITE,
axes_background: Color::WHITE,
grid_color: Color::rgb(0xE6, 0xE6, 0xE6),
grid_width: 1.0,
show_grid: true,
spine_color: spine,
spine_width: 1.0,
show_top_spine: false,
show_right_spine: false,
show_bottom_spine: true,
show_left_spine: true,
tick_color: spine,
tick_length: 4.0,
tick_direction: TickDirection::Outward,
tick_label_size: 9.0,
axis_label_size: 11.0,
title_size: 14.0,
title_weight: FontWeight::Bold,
text_color: spine,
line_width: 1.5,
marker_size: 6.0,
marker_alpha: 0.8,
color_cycle: TABLEAU_10.to_vec(),
font_family: None,
}
}
}
impl Theme {
pub fn dark() -> Self {
let bg = Color::rgb(0x1C, 0x1C, 0x1C);
let text = Color::rgb(0xE0, 0xE0, 0xE0);
let grid = Color::rgb(0x3A, 0x3A, 0x3A);
let spine = Color::rgb(0x55, 0x55, 0x55);
let cycle = vec![
Color::rgb(0x00, 0xD4, 0xFF), Color::rgb(0xFF, 0x6F, 0x61), Color::rgb(0x7B, 0xED, 0x72), Color::rgb(0xFF, 0xA6, 0x00), Color::rgb(0xD1, 0x7D, 0xFF), Color::rgb(0xFF, 0xE1, 0x00), Color::rgb(0x00, 0xFF, 0xAB), Color::rgb(0xFF, 0x4D, 0xA6), Color::rgb(0x48, 0xBF, 0xE3), Color::rgb(0xE8, 0xE8, 0xE8), ];
Self {
figure_background: bg,
axes_background: bg,
grid_color: grid,
grid_width: 1.0,
show_grid: true,
spine_color: spine,
spine_width: 1.0,
show_top_spine: false,
show_right_spine: false,
show_bottom_spine: true,
show_left_spine: true,
tick_color: text,
tick_length: 4.0,
tick_direction: TickDirection::Outward,
tick_label_size: 9.0,
axis_label_size: 11.0,
title_size: 14.0,
title_weight: FontWeight::Bold,
text_color: text,
line_width: 1.5,
marker_size: 6.0,
marker_alpha: 0.9,
color_cycle: cycle,
font_family: None,
}
}
pub fn seaborn() -> Self {
let text = Color::rgb(0x33, 0x33, 0x33);
let axes_bg = Color::rgb(0xEA, 0xEA, 0xF2);
Self {
figure_background: Color::WHITE,
axes_background: axes_bg,
grid_color: Color::WHITE,
grid_width: 1.0,
show_grid: true,
spine_color: Color::rgb(0xCC, 0xCC, 0xCC),
spine_width: 1.0,
show_top_spine: false,
show_right_spine: false,
show_bottom_spine: true,
show_left_spine: true,
tick_color: text,
tick_length: 0.0, tick_direction: TickDirection::Outward,
tick_label_size: 9.0,
axis_label_size: 11.0,
title_size: 14.0,
title_weight: FontWeight::Bold,
text_color: text,
line_width: 1.5,
marker_size: 6.0,
marker_alpha: 0.8,
color_cycle: TABLEAU_10.to_vec(),
font_family: None,
}
}
pub fn ggplot() -> Self {
let panel = Color::rgb(0xE5, 0xE5, 0xE5);
let text = Color::rgb(0x30, 0x30, 0x30);
let cycle = vec![
Color::rgb(0xF8, 0x76, 0x6D), Color::rgb(0xA3, 0xA5, 0x00), Color::rgb(0x00, 0xBA, 0x38), Color::rgb(0x00, 0xBF, 0xC4), Color::rgb(0x61, 0x9C, 0xFF), Color::rgb(0xF5, 0x64, 0xE3), Color::rgb(0xFF, 0x64, 0xB0), Color::rgb(0xB7, 0x9F, 0x00), ];
Self {
figure_background: Color::WHITE,
axes_background: panel,
grid_color: Color::WHITE,
grid_width: 1.0,
show_grid: true,
spine_color: Color::WHITE,
spine_width: 0.0,
show_top_spine: false,
show_right_spine: false,
show_bottom_spine: false,
show_left_spine: false,
tick_color: text,
tick_length: 0.0, tick_direction: TickDirection::Outward,
tick_label_size: 9.0,
axis_label_size: 11.0,
title_size: 14.0,
title_weight: FontWeight::Bold,
text_color: text,
line_width: 1.0,
marker_size: 5.0,
marker_alpha: 1.0,
color_cycle: cycle,
font_family: None,
}
}
pub fn publication() -> Self {
let ink = Color::rgb(0x1A, 0x1A, 0x1A);
Self {
figure_background: Color::WHITE,
axes_background: Color::WHITE,
grid_color: Color::rgb(0xD0, 0xD0, 0xD0),
grid_width: 0.5,
show_grid: false,
spine_color: ink,
spine_width: 0.5,
show_top_spine: true,
show_right_spine: true,
show_bottom_spine: true,
show_left_spine: true,
tick_color: ink,
tick_length: 3.0,
tick_direction: TickDirection::Inward,
tick_label_size: 8.0,
axis_label_size: 10.0,
title_size: 12.0,
title_weight: FontWeight::Bold,
text_color: ink,
line_width: 1.0,
marker_size: 4.0,
marker_alpha: 1.0,
color_cycle: TABLEAU_10.to_vec(),
font_family: Some("serif".to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_theme_background_is_white() {
let t = Theme::default();
assert_eq!(t.figure_background, Color::WHITE);
assert_eq!(t.axes_background, Color::WHITE);
}
#[test]
fn default_theme_despine_look() {
let t = Theme::default();
assert!(!t.show_top_spine);
assert!(!t.show_right_spine);
assert!(t.show_bottom_spine);
assert!(t.show_left_spine);
}
#[test]
fn default_theme_grid() {
let t = Theme::default();
assert_eq!(t.grid_color, Color::rgb(0xE6, 0xE6, 0xE6));
assert!((t.grid_width - 1.0).abs() < f64::EPSILON);
assert!(t.show_grid);
}
#[test]
fn default_theme_spines() {
let t = Theme::default();
let expected = Color::rgb(0x33, 0x33, 0x33);
assert_eq!(t.spine_color, expected);
assert!((t.spine_width - 1.0).abs() < f64::EPSILON);
}
#[test]
fn default_theme_ticks() {
let t = Theme::default();
assert_eq!(t.tick_color, Color::rgb(0x33, 0x33, 0x33));
assert!((t.tick_length - 4.0).abs() < f64::EPSILON);
assert_eq!(t.tick_direction, TickDirection::Outward);
}
#[test]
fn default_theme_font_sizes() {
let t = Theme::default();
assert!((t.tick_label_size - 9.0).abs() < f64::EPSILON);
assert!((t.axis_label_size - 11.0).abs() < f64::EPSILON);
assert!((t.title_size - 14.0).abs() < f64::EPSILON);
assert_eq!(t.title_weight, FontWeight::Bold);
}
#[test]
fn default_theme_text_color() {
let t = Theme::default();
assert_eq!(t.text_color, Color::rgb(0x33, 0x33, 0x33));
}
#[test]
fn default_theme_data_defaults() {
let t = Theme::default();
assert!((t.line_width - 1.5).abs() < f64::EPSILON);
assert!((t.marker_size - 6.0).abs() < f64::EPSILON);
assert!((t.marker_alpha - 0.8).abs() < f64::EPSILON);
}
#[test]
fn default_theme_tableau_10_cycle() {
let t = Theme::default();
assert_eq!(t.color_cycle.len(), 10);
assert_eq!(t.color_cycle[0], Color::TAB_BLUE);
assert_eq!(t.color_cycle[9], Color::TAB_CYAN);
}
#[test]
fn dark_theme_has_dark_background() {
let t = Theme::dark();
assert_eq!(t.figure_background, Color::rgb(0x1C, 0x1C, 0x1C));
assert_eq!(t.axes_background, Color::rgb(0x1C, 0x1C, 0x1C));
}
#[test]
fn dark_theme_light_text() {
let t = Theme::dark();
assert_eq!(t.text_color, Color::rgb(0xE0, 0xE0, 0xE0));
}
#[test]
fn dark_theme_neon_cycle() {
let t = Theme::dark();
assert_eq!(t.color_cycle.len(), 10);
assert_eq!(t.color_cycle[0], Color::rgb(0x00, 0xD4, 0xFF));
}
#[test]
fn seaborn_theme_tinted_face() {
let t = Theme::seaborn();
assert_eq!(t.axes_background, Color::rgb(0xEA, 0xEA, 0xF2));
}
#[test]
fn seaborn_theme_white_grid() {
let t = Theme::seaborn();
assert_eq!(t.grid_color, Color::WHITE);
assert!(t.show_grid);
}
#[test]
fn ggplot_theme_grey_panel() {
let t = Theme::ggplot();
assert_eq!(t.axes_background, Color::rgb(0xE5, 0xE5, 0xE5));
}
#[test]
fn ggplot_theme_white_grid() {
let t = Theme::ggplot();
assert_eq!(t.grid_color, Color::WHITE);
assert!(t.show_grid);
}
#[test]
fn ggplot_theme_no_spines() {
let t = Theme::ggplot();
assert!(!t.show_top_spine);
assert!(!t.show_right_spine);
assert!(!t.show_bottom_spine);
assert!(!t.show_left_spine);
}
#[test]
fn ggplot_theme_palette() {
let t = Theme::ggplot();
assert_eq!(t.color_cycle.len(), 8);
assert_eq!(t.color_cycle[0], Color::rgb(0xF8, 0x76, 0x6D));
}
#[test]
fn publication_theme_all_spines_visible() {
let t = Theme::publication();
assert!(t.show_top_spine);
assert!(t.show_right_spine);
assert!(t.show_bottom_spine);
assert!(t.show_left_spine);
}
#[test]
fn publication_theme_no_grid() {
let t = Theme::publication();
assert!(!t.show_grid);
}
#[test]
fn publication_theme_thin_spines() {
let t = Theme::publication();
assert!((t.spine_width - 0.5).abs() < f64::EPSILON);
}
#[test]
fn publication_theme_inward_ticks() {
let t = Theme::publication();
assert_eq!(t.tick_direction, TickDirection::Inward);
}
#[test]
fn publication_theme_serif_font() {
let t = Theme::publication();
assert_eq!(t.font_family, Some("serif".to_string()));
}
#[test]
fn publication_theme_white_background() {
let t = Theme::publication();
assert_eq!(t.figure_background, Color::WHITE);
assert_eq!(t.axes_background, Color::WHITE);
}
#[test]
fn grid_axis_default_is_both() {
assert_eq!(GridAxis::default(), GridAxis::Both);
}
}