#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StyleColor(pub u8, pub u8, pub u8);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MarkerKind {
Dot,
Circle,
Cross,
Plus,
Star,
Square,
Diamond,
Triangle,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LinestyleKind {
Solid,
Dashed,
Dotted,
DashDot,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Theme {
pub bg: StyleColor,
pub text: StyleColor,
pub axis: StyleColor,
pub grid_bold: StyleColor,
pub grid_light: StyleColor,
}
impl Theme {
pub fn light() -> Self {
Theme {
bg: StyleColor(255, 255, 255),
text: StyleColor(0, 0, 0),
axis: StyleColor(0, 0, 0),
grid_bold: StyleColor(180, 180, 180),
grid_light: StyleColor(220, 220, 220),
}
}
pub fn dark() -> Self {
Theme {
bg: StyleColor(0x1E, 0x1E, 0x2E),
text: StyleColor(0xCD, 0xD6, 0xF4),
axis: StyleColor(0x6C, 0x70, 0x86),
grid_bold: StyleColor(0x45, 0x47, 0x5A),
grid_light: StyleColor(0x31, 0x32, 0x44),
}
}
pub fn from_name(name: &str) -> Result<Self, String> {
match name.to_ascii_lowercase().as_str() {
"light" => Ok(Theme::light()),
"dark" => Ok(Theme::dark()),
other => Err(format!(
"theme: unknown theme '{other}' — expected 'light' or 'dark'"
)),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AxisMode {
Equal,
Tight,
Off,
}
#[derive(Clone, Debug, PartialEq)]
pub struct StyleSpec {
pub color: Option<StyleColor>,
pub marker: Option<MarkerKind>,
pub linestyle: LinestyleKind,
pub line_width: Option<f32>,
pub marker_size: Option<u32>,
}
impl Default for StyleSpec {
fn default() -> Self {
StyleSpec {
color: None,
marker: None,
linestyle: LinestyleKind::Solid,
line_width: None,
marker_size: None,
}
}
}
pub fn parse_color_token(token: &str) -> Option<StyleColor> {
match token.to_ascii_lowercase().as_str() {
"r" | "red" => Some(StyleColor(255, 0, 0)),
"g" | "green" => Some(StyleColor(0, 128, 0)),
"b" | "blue" => Some(StyleColor(0, 0, 255)),
"c" | "cyan" => Some(StyleColor(0, 255, 255)),
"m" | "magenta" => Some(StyleColor(255, 0, 255)),
"y" | "yellow" => Some(StyleColor(255, 255, 0)),
"k" | "black" => Some(StyleColor(0, 0, 0)),
"w" | "white" => Some(StyleColor(255, 255, 255)),
"orange" => Some(StyleColor(255, 165, 0)),
"purple" => Some(StyleColor(128, 0, 128)),
"gray" | "grey" => Some(StyleColor(128, 128, 128)),
s if s.starts_with('#') && s.len() == 7 => {
let r = u8::from_str_radix(&s[1..3], 16).ok()?;
let g = u8::from_str_radix(&s[3..5], 16).ok()?;
let b = u8::from_str_radix(&s[5..7], 16).ok()?;
Some(StyleColor(r, g, b))
}
_ => None,
}
}
pub fn looks_like_style_str(s: &str) -> bool {
if s.is_empty() {
return false;
}
if s.starts_with('#') {
return s.len() == 7;
}
if parse_color_token(s).is_some() {
return true;
}
s.chars().all(|c| "rgbcmykw.-:osx+*d^".contains(c))
}
pub fn parse_style_str(s: &str) -> Result<StyleSpec, String> {
if s.is_empty() {
return Ok(StyleSpec::default());
}
if let Some(sc) = parse_color_token(s) {
return Ok(StyleSpec {
color: Some(sc),
..StyleSpec::default()
});
}
let mut spec = StyleSpec::default();
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if i + 1 < bytes.len() && bytes[i] == b'-' && bytes[i + 1] == b'-' {
spec.linestyle = LinestyleKind::Dashed;
i += 2;
continue;
}
if i + 1 < bytes.len() && bytes[i] == b'-' && bytes[i + 1] == b'.' {
spec.linestyle = LinestyleKind::DashDot;
i += 2;
continue;
}
match bytes[i] {
b'-' => spec.linestyle = LinestyleKind::Solid,
b':' => spec.linestyle = LinestyleKind::Dotted,
b'.' => spec.marker = Some(MarkerKind::Dot),
b'o' => spec.marker = Some(MarkerKind::Circle),
b'x' => spec.marker = Some(MarkerKind::Cross),
b'+' => spec.marker = Some(MarkerKind::Plus),
b'*' => spec.marker = Some(MarkerKind::Star),
b's' => spec.marker = Some(MarkerKind::Square),
b'd' => spec.marker = Some(MarkerKind::Diamond),
b'^' => spec.marker = Some(MarkerKind::Triangle),
b'r' => spec.color = Some(StyleColor(255, 0, 0)),
b'g' => spec.color = Some(StyleColor(0, 128, 0)),
b'b' => spec.color = Some(StyleColor(0, 0, 255)),
b'c' => spec.color = Some(StyleColor(0, 255, 255)),
b'm' => spec.color = Some(StyleColor(255, 0, 255)),
b'y' => spec.color = Some(StyleColor(255, 255, 0)),
b'k' => spec.color = Some(StyleColor(0, 0, 0)),
b'w' => spec.color = Some(StyleColor(255, 255, 255)),
other => {
return Err(format!(
"plot: unknown style character '{}' in style string '{s}'",
other as char
));
}
}
i += 1;
}
Ok(spec)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_red_dashed() {
let spec = parse_style_str("r--").unwrap();
assert_eq!(spec.color, Some(StyleColor(255, 0, 0)));
assert_eq!(spec.linestyle, LinestyleKind::Dashed);
assert_eq!(spec.marker, None);
}
#[test]
fn test_parse_blue_dot() {
let spec = parse_style_str("b.").unwrap();
assert_eq!(spec.color, Some(StyleColor(0, 0, 255)));
assert_eq!(spec.marker, Some(MarkerKind::Dot));
assert_eq!(spec.linestyle, LinestyleKind::Solid);
}
#[test]
fn test_parse_green_solid() {
let spec = parse_style_str("g-").unwrap();
assert_eq!(spec.color, Some(StyleColor(0, 128, 0)));
assert_eq!(spec.linestyle, LinestyleKind::Solid);
assert_eq!(spec.marker, None);
}
#[test]
fn test_parse_dashdot() {
let spec = parse_style_str("-.").unwrap();
assert_eq!(spec.linestyle, LinestyleKind::DashDot);
assert_eq!(spec.marker, None);
}
#[test]
fn test_parse_dot_then_solid() {
let spec = parse_style_str(".-").unwrap();
assert_eq!(spec.marker, Some(MarkerKind::Dot));
assert_eq!(spec.linestyle, LinestyleKind::Solid);
}
#[test]
fn test_parse_dotted_line() {
let spec = parse_style_str(":").unwrap();
assert_eq!(spec.linestyle, LinestyleKind::Dotted);
}
#[test]
fn test_parse_empty_returns_default() {
let spec = parse_style_str("").unwrap();
assert_eq!(spec, StyleSpec::default());
}
#[test]
fn test_parse_unknown_char_errors() {
let result = parse_style_str("xyz");
assert!(result.is_err());
let msg = result.unwrap_err();
assert!(msg.contains("unknown style character"));
}
#[test]
fn test_looks_like_style_str_valid() {
assert!(looks_like_style_str("r--"));
assert!(looks_like_style_str("b."));
assert!(looks_like_style_str("g-"));
assert!(looks_like_style_str("ko"));
}
#[test]
fn test_looks_like_style_str_invalid() {
assert!(!looks_like_style_str(""));
assert!(!looks_like_style_str("time"));
assert!(!looks_like_style_str("file.svg"));
}
#[test]
fn test_style_full_name_red() {
let spec = parse_style_str("red").unwrap();
assert_eq!(spec.color, Some(StyleColor(255, 0, 0)));
assert_eq!(spec.marker, None);
assert_eq!(spec.linestyle, LinestyleKind::Solid);
}
#[test]
fn test_style_full_name_orange() {
let spec = parse_style_str("orange").unwrap();
assert_eq!(spec.color, Some(StyleColor(255, 165, 0)));
}
#[test]
fn test_style_gray_grey_alias() {
let spec_gray = parse_style_str("gray").unwrap();
let spec_grey = parse_style_str("grey").unwrap();
assert_eq!(spec_gray.color, spec_grey.color);
assert_eq!(spec_gray.color, Some(StyleColor(128, 128, 128)));
}
#[test]
fn test_style_hex_color() {
let spec = parse_style_str("#1A2B3C").unwrap();
assert_eq!(spec.color, Some(StyleColor(0x1A, 0x2B, 0x3C)));
}
#[test]
fn test_style_hex_bad_format() {
let result = parse_style_str("#1A2B3");
assert!(result.is_err(), "short hex should error");
}
}