use crate::core::color::{DEFAULT_COLOR_CYCLE, Rgba};
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TickDirection {
Out,
In,
InOut,
}
#[must_use]
fn default_prop_cycle() -> Vec<Rgba> {
DEFAULT_COLOR_CYCLE
.iter()
.map(|hex| Rgba::from_hex(hex).expect("DEFAULT_COLOR_CYCLE entries are valid hex"))
.collect()
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct RcParams {
pub figure_figsize: (f64, f64),
pub figure_dpi: f64,
pub figure_facecolor: Rgba,
pub savefig_dpi: f64,
pub axes_facecolor: Rgba,
pub axes_edgecolor: Rgba,
pub axes_linewidth: f64,
pub axes_grid: bool,
pub axes_titlesize: f64,
pub axes_titlepad: f64,
pub axes_labelsize: f64,
pub axes_labelpad: f64,
pub axes_prop_cycle: Vec<Rgba>,
pub lines_linewidth: f64,
pub lines_markersize: f64,
pub xtick_labelsize: f64,
pub ytick_labelsize: f64,
pub xtick_major_size: f64,
pub ytick_major_size: f64,
pub xtick_major_pad: f64,
pub ytick_major_pad: f64,
pub xtick_direction: TickDirection,
pub ytick_direction: TickDirection,
pub font_size: f64,
pub font_family: String,
pub grid_color: Rgba,
pub grid_linewidth: f64,
pub grid_alpha: f64,
}
impl Default for RcParams {
fn default() -> Self {
Self {
figure_figsize: (6.4, 4.8),
figure_dpi: 100.0,
figure_facecolor: Rgba::WHITE,
savefig_dpi: 100.0,
axes_facecolor: Rgba::WHITE,
axes_edgecolor: Rgba::BLACK,
axes_linewidth: 0.8,
axes_grid: false,
axes_titlesize: 12.0,
axes_titlepad: 6.0,
axes_labelsize: 10.0,
axes_labelpad: 4.0,
axes_prop_cycle: default_prop_cycle(),
lines_linewidth: 1.5,
lines_markersize: 6.0,
xtick_labelsize: 10.0,
ytick_labelsize: 10.0,
xtick_major_size: 3.5,
ytick_major_size: 3.5,
xtick_major_pad: 3.5,
ytick_major_pad: 3.5,
xtick_direction: TickDirection::Out,
ytick_direction: TickDirection::Out,
font_size: 10.0,
font_family: "DejaVu Sans".to_string(),
grid_color: Rgba::from_u8(217, 217, 217, 255),
grid_linewidth: 0.8,
grid_alpha: 1.0,
}
}
}
impl RcParams {
#[must_use]
pub fn matplotlib() -> Self {
Self::default()
}
#[must_use]
pub fn dark() -> Self {
let dark = Rgba::from_u8(31, 31, 31, 255);
let light = Rgba::from_u8(230, 230, 230, 255);
Self {
figure_facecolor: dark,
axes_facecolor: dark,
axes_edgecolor: light,
grid_color: Rgba::from_u8(77, 77, 77, 255),
..Self::default()
}
}
#[must_use]
pub fn cycle_color(&self, i: usize) -> Option<Rgba> {
let cycle = &self.axes_prop_cycle;
if cycle.is_empty() {
return None;
}
Some(cycle[i % cycle.len()])
}
#[must_use]
pub fn merge(&self, overrides: &RcParams) -> RcParams {
let base = RcParams::default();
let mut out = self.clone();
macro_rules! merge_field {
($field:ident) => {
if overrides.$field != base.$field {
out.$field = overrides.$field.clone();
}
};
}
merge_field!(figure_figsize);
merge_field!(figure_dpi);
merge_field!(figure_facecolor);
merge_field!(savefig_dpi);
merge_field!(axes_facecolor);
merge_field!(axes_edgecolor);
merge_field!(axes_linewidth);
merge_field!(axes_grid);
merge_field!(axes_titlesize);
merge_field!(axes_titlepad);
merge_field!(axes_labelsize);
merge_field!(axes_labelpad);
merge_field!(axes_prop_cycle);
merge_field!(lines_linewidth);
merge_field!(lines_markersize);
merge_field!(xtick_labelsize);
merge_field!(ytick_labelsize);
merge_field!(xtick_major_size);
merge_field!(ytick_major_size);
merge_field!(xtick_major_pad);
merge_field!(ytick_major_pad);
merge_field!(xtick_direction);
merge_field!(ytick_direction);
merge_field!(font_size);
merge_field!(font_family);
merge_field!(grid_color);
merge_field!(grid_linewidth);
merge_field!(grid_alpha);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_has_documented_values() {
let rc = RcParams::default();
assert_eq!(rc.figure_figsize, (6.4, 4.8));
assert_eq!(rc.figure_dpi, 100.0);
assert_eq!(rc.savefig_dpi, 100.0);
assert_eq!(rc.figure_facecolor, Rgba::WHITE);
assert_eq!(rc.axes_facecolor, Rgba::WHITE);
assert_eq!(rc.axes_edgecolor, Rgba::BLACK);
assert_eq!(rc.axes_linewidth, 0.8);
assert!(!rc.axes_grid);
assert_eq!(rc.axes_titlesize, 12.0);
assert_eq!(rc.axes_titlepad, 6.0);
assert_eq!(rc.axes_labelsize, 10.0);
assert_eq!(rc.axes_labelpad, 4.0);
assert_eq!(rc.lines_linewidth, 1.5);
assert_eq!(rc.lines_markersize, 6.0);
assert_eq!(rc.xtick_labelsize, 10.0);
assert_eq!(rc.ytick_labelsize, 10.0);
assert_eq!(rc.xtick_major_size, 3.5);
assert_eq!(rc.ytick_major_size, 3.5);
assert_eq!(rc.xtick_major_pad, 3.5);
assert_eq!(rc.ytick_major_pad, 3.5);
assert_eq!(rc.xtick_direction, TickDirection::Out);
assert_eq!(rc.font_size, 10.0);
assert_eq!(rc.font_family, "DejaVu Sans");
assert_eq!(rc.grid_linewidth, 0.8);
assert_eq!(rc.grid_alpha, 1.0);
}
#[test]
fn matplotlib_aliases_default() {
assert_eq!(RcParams::matplotlib(), RcParams::default());
}
#[test]
fn default_prop_cycle_has_ten_colors() {
let rc = RcParams::default();
assert_eq!(rc.axes_prop_cycle.len(), 10);
assert_eq!(rc.axes_prop_cycle[0], Rgba::from_hex("#1f77b4").unwrap());
}
#[test]
fn cycle_color_wraps() {
let rc = RcParams::default();
assert_eq!(rc.cycle_color(0), Rgba::from_hex("#1f77b4"));
assert_eq!(rc.cycle_color(10), rc.cycle_color(0));
}
#[test]
fn json_round_trip() {
let rc = RcParams::default();
let json = serde_json::to_string(&rc).expect("serialize");
let back: RcParams = serde_json::from_str(&json).expect("deserialize");
assert_eq!(rc, back);
}
#[test]
fn dark_preset_round_trips() {
let rc = RcParams::dark();
assert_ne!(rc.figure_facecolor, Rgba::WHITE);
let json = serde_json::to_string(&rc).expect("serialize");
let back: RcParams = serde_json::from_str(&json).expect("deserialize");
assert_eq!(rc, back);
}
#[test]
fn merge_applies_overrides() {
let base = RcParams::default();
let overrides = RcParams {
lines_linewidth: 3.0,
axes_grid: true,
..RcParams::default()
};
let merged = base.merge(&overrides);
assert_eq!(merged.lines_linewidth, 3.0);
assert!(merged.axes_grid);
assert_eq!(merged.figure_dpi, base.figure_dpi);
}
}