use serde::Deserialize;
use super::elements::{ElementLine, ElementRect, ElementText};
use super::presets::{
theme_bw, theme_classic, theme_dark, theme_gray, theme_light, theme_linedraw, theme_minimal,
theme_void,
};
use super::{LegendDirection, LegendPosition, TagPosition, Theme, TitlePosition};
use crate::render::backend::{FontFace, Linetype};
use crate::scale::palettes::PaletteName;
#[derive(Deserialize, Default)]
#[serde(default, deny_unknown_fields)]
pub struct ThemeConfig {
pub base: Option<String>,
pub primary: Option<[u8; 3]>,
pub palette: Option<String>,
pub aspect_ratio: Option<f64>,
pub panel_ontop: Option<bool>,
pub axis_minor_ticks: Option<bool>,
pub title_position: Option<String>,
pub tag_position: Option<String>,
pub legend_direction: Option<String>,
pub text: Option<TextCfg>,
pub title: Option<TextCfg>,
pub subtitle: Option<TextCfg>,
pub caption: Option<TextCfg>,
pub axis_title_x: Option<TextCfg>,
pub axis_title_y: Option<TextCfg>,
pub axis_text_x: Option<TextCfg>,
pub axis_text_y: Option<TextCfg>,
pub legend_title: Option<TextCfg>,
pub legend_text: Option<TextCfg>,
pub strip_text: Option<TextCfg>,
pub axis_line: Option<LineCfg>,
pub axis_ticks: Option<LineCfg>,
pub panel_grid_major: Option<LineCfg>,
pub panel_grid_minor: Option<LineCfg>,
pub panel_border: Option<LineCfg>,
pub panel_background: Option<RectCfg>,
pub plot_background: Option<RectCfg>,
pub legend_background: Option<RectCfg>,
pub strip_background: Option<RectCfg>,
pub legend: Option<LegendCfg>,
}
#[derive(Deserialize, Default)]
#[serde(default, deny_unknown_fields)]
pub struct TextCfg {
pub family: Option<String>,
pub face: Option<String>,
pub size: Option<f64>,
pub color: Option<[u8; 3]>,
pub hjust: Option<f64>,
pub vjust: Option<f64>,
pub angle: Option<f64>,
pub visible: Option<bool>,
}
#[derive(Deserialize, Default)]
#[serde(default, deny_unknown_fields)]
pub struct LineCfg {
pub color: Option<[u8; 3]>,
pub width: Option<f64>,
pub visible: Option<bool>,
pub linetype: Option<String>,
}
#[derive(Deserialize, Default)]
#[serde(default, deny_unknown_fields)]
pub struct RectCfg {
pub fill: Option<[u8; 3]>,
pub color: Option<[u8; 3]>,
pub width: Option<f64>,
pub visible: Option<bool>,
}
#[derive(Deserialize, Default)]
#[serde(default, deny_unknown_fields)]
pub struct LegendCfg {
pub position: Option<String>,
pub x: Option<f64>,
pub y: Option<f64>,
}
impl ThemeConfig {
pub fn apply(&self, base: Theme) -> Result<Theme, String> {
let mut t = match &self.base {
Some(name) => preset(name)?,
None => base,
};
text(&self.text, &mut t.text)?;
text(&self.title, &mut t.title)?;
text(&self.subtitle, &mut t.subtitle)?;
text(&self.caption, &mut t.caption)?;
text(&self.axis_title_x, &mut t.axis_title_x)?;
text(&self.axis_title_y, &mut t.axis_title_y)?;
text(&self.axis_text_x, &mut t.axis_text_x)?;
text(&self.axis_text_y, &mut t.axis_text_y)?;
text(&self.legend_title, &mut t.legend_title)?;
text(&self.legend_text, &mut t.legend_text)?;
text(&self.strip_text, &mut t.strip_text)?;
line(&self.axis_line, &mut t.axis_line)?;
line(&self.axis_ticks, &mut t.axis_ticks)?;
line(&self.panel_grid_major, &mut t.panel_grid_major)?;
line(&self.panel_grid_minor, &mut t.panel_grid_minor)?;
line(&self.panel_border, &mut t.panel_border)?;
rect(&self.panel_background, &mut t.panel_background);
rect(&self.plot_background, &mut t.plot_background);
rect(&self.legend_background, &mut t.legend_background);
rect(&self.strip_background, &mut t.strip_background);
if let Some([r, g, b]) = self.primary {
t.primary = Some((r, g, b));
}
if let Some(r) = self.aspect_ratio {
t.aspect_ratio = Some(r);
}
if let Some(b) = self.panel_ontop {
t.panel_ontop = b;
}
if let Some(b) = self.axis_minor_ticks {
t.axis_minor_ticks = b;
}
if let Some(p) = &self.title_position {
t.title_position = match p.to_lowercase().as_str() {
"panel" => TitlePosition::Panel,
"plot" => TitlePosition::Plot,
other => return Err(format!("unknown title_position '{other}'")),
};
}
if let Some(p) = &self.tag_position {
t.tag_position = match p.to_lowercase().as_str() {
"topleft" => TagPosition::TopLeft,
"topright" => TagPosition::TopRight,
"bottomleft" => TagPosition::BottomLeft,
"bottomright" => TagPosition::BottomRight,
other => return Err(format!("unknown tag_position '{other}'")),
};
}
if let Some(d) = &self.legend_direction {
t.legend_direction = Some(match d.to_lowercase().as_str() {
"vertical" => LegendDirection::Vertical,
"horizontal" => LegendDirection::Horizontal,
other => return Err(format!("unknown legend_direction '{other}'")),
});
}
if let Some(l) = &self.legend {
t.legend_position = legend_position(l)?;
}
Ok(t)
}
}
pub fn preset(name: &str) -> Result<Theme, String> {
Ok(match name {
"gray" | "grey" => theme_gray(),
"bw" => theme_bw(),
"minimal" => theme_minimal(),
"classic" => theme_classic(),
"dark" => theme_dark(),
"light" => theme_light(),
"void" => theme_void(),
"linedraw" => theme_linedraw(),
other => return Err(format!("unknown theme preset '{other}'")),
})
}
fn text(cfg: &Option<TextCfg>, el: &mut ElementText) -> Result<(), String> {
if let Some(c) = cfg {
if let Some(v) = &c.family {
el.family = v.clone();
}
if let Some(f) = &c.face {
el.face = match f.to_lowercase().as_str() {
"plain" | "normal" => FontFace::Plain,
"bold" => FontFace::Bold,
"italic" | "oblique" => FontFace::Italic,
other => return Err(format!("unknown face '{other}'")),
};
}
if let Some(v) = c.size {
el.size = v;
}
if let Some([r, g, b]) = c.color {
el.color = (r, g, b);
}
if let Some(v) = c.angle {
el.angle = v;
}
if let Some(v) = c.hjust {
el.hjust = v;
}
if let Some(v) = c.vjust {
el.vjust = v;
}
if let Some(v) = c.visible {
el.visible = v;
}
}
Ok(())
}
fn line(cfg: &Option<LineCfg>, el: &mut ElementLine) -> Result<(), String> {
if let Some(c) = cfg {
if let Some([r, g, b]) = c.color {
el.color = (r, g, b);
}
if let Some(v) = c.width {
el.width = v;
}
if let Some(v) = c.visible {
el.visible = v;
}
if let Some(lt) = &c.linetype {
el.linetype = parse_linetype(lt)?;
}
}
Ok(())
}
fn rect(cfg: &Option<RectCfg>, el: &mut ElementRect) {
if let Some(c) = cfg {
if let Some([r, g, b]) = c.fill {
el.fill = Some((r, g, b));
}
if let Some([r, g, b]) = c.color {
el.color = Some((r, g, b));
}
if let Some(v) = c.width {
el.width = v;
}
if let Some(v) = c.visible {
el.visible = v;
}
}
}
fn legend_position(l: &LegendCfg) -> Result<LegendPosition, String> {
let pos = l.position.as_deref().unwrap_or("right");
Ok(match pos {
"top" => LegendPosition::Top,
"bottom" => LegendPosition::Bottom,
"left" => LegendPosition::Left,
"right" => LegendPosition::Right,
"none" => LegendPosition::None,
"inside" => LegendPosition::Inside(l.x.unwrap_or(0.85), l.y.unwrap_or(0.85)),
other => return Err(format!("unknown legend position '{other}'")),
})
}
fn parse_linetype(s: &str) -> Result<Linetype, String> {
Ok(match s.to_lowercase().as_str() {
"solid" => Linetype::Solid,
"dashed" => Linetype::Dashed,
"dotted" => Linetype::Dotted,
"dashdot" => Linetype::DashDot,
"longdash" => Linetype::LongDash,
"twodash" => Linetype::TwoDash,
other => return Err(format!("unknown linetype '{other}'")),
})
}
pub fn parse_rgb(s: &str) -> Result<(u8, u8, u8), String> {
let parts: Vec<&str> = s.split(',').map(|p| p.trim()).collect();
if parts.len() != 3 {
return Err(format!("expected 'r,g,b', got '{s}'"));
}
let c = |p: &str| {
p.parse::<u8>()
.map_err(|_| format!("bad color component '{p}'"))
};
Ok((c(parts[0])?, c(parts[1])?, c(parts[2])?))
}
pub fn parse_palette(name: &str) -> Result<PaletteName, String> {
use PaletteName::*;
Ok(match name.to_lowercase().as_str() {
"set1" => Set1,
"set2" => Set2,
"set3" => Set3,
"dark2" => Dark2,
"paired" => Paired,
"pastel1" => Pastel1,
"pastel2" => Pastel2,
"accent" => Accent,
"blues" => Blues,
"greens" => Greens,
"reds" => Reds,
"oranges" => Oranges,
"purples" => Purples,
"greys" | "grays" => Greys,
"ylorrd" => YlOrRd,
"ylgnbu" => YlGnBu,
"bugn" => BuGn,
"bupu" => BuPu,
"gnbu" => GnBu,
"orrd" => OrRd,
"purd" => PuRd,
"rdpu" => RdPu,
"ylgn" => YlGn,
"ylorbr" => YlOrBr,
"rdbu" => RdBu,
"spectral" => Spectral,
"piyg" => PiYG,
"prgn" => PRGn,
"brbg" => BrBG,
"puor" => PuOr,
"rdgy" => RdGy,
"rdylbu" => RdYlBu,
"rdylgn" => RdYlGn,
"viridis" => Viridis,
"magma" => Magma,
"plasma" => Plasma,
"inferno" => Inferno,
other => return Err(format!("unknown palette '{other}'")),
})
}