1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub struct StyleColor(pub u8, pub u8, pub u8);
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum MarkerKind {
19 Dot,
21 Circle,
23 Cross,
25 Plus,
27 Star,
29 Square,
31 Diamond,
33 Triangle,
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum LinestyleKind {
40 Solid,
42 Dashed,
44 Dotted,
46 DashDot,
48}
49
50#[derive(Clone, Debug, PartialEq)]
52pub struct Theme {
53 pub bg: StyleColor,
55 pub text: StyleColor,
57 pub axis: StyleColor,
59 pub grid_bold: StyleColor,
61 pub grid_light: StyleColor,
63}
64
65impl Theme {
66 pub fn light() -> Self {
68 Theme {
69 bg: StyleColor(255, 255, 255),
70 text: StyleColor(0, 0, 0),
71 axis: StyleColor(0, 0, 0),
72 grid_bold: StyleColor(180, 180, 180),
73 grid_light: StyleColor(220, 220, 220),
74 }
75 }
76
77 pub fn dark() -> Self {
79 Theme {
80 bg: StyleColor(0x1E, 0x1E, 0x2E),
81 text: StyleColor(0xCD, 0xD6, 0xF4),
82 axis: StyleColor(0x6C, 0x70, 0x86),
83 grid_bold: StyleColor(0x45, 0x47, 0x5A),
84 grid_light: StyleColor(0x31, 0x32, 0x44),
85 }
86 }
87
88 pub fn from_name(name: &str) -> Result<Self, String> {
92 match name.to_ascii_lowercase().as_str() {
93 "light" => Ok(Theme::light()),
94 "dark" => Ok(Theme::dark()),
95 other => Err(format!(
96 "theme: unknown theme '{other}' — expected 'light' or 'dark'"
97 )),
98 }
99 }
100}
101
102#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub enum AxisMode {
105 Equal,
107 Tight,
109 Off,
111}
112
113#[derive(Clone, Debug, PartialEq)]
115pub struct StyleSpec {
116 pub color: Option<StyleColor>,
118 pub marker: Option<MarkerKind>,
120 pub linestyle: LinestyleKind,
122 pub line_width: Option<f32>,
124 pub marker_size: Option<u32>,
126}
127
128impl Default for StyleSpec {
129 fn default() -> Self {
130 StyleSpec {
131 color: None,
132 marker: None,
133 linestyle: LinestyleKind::Solid,
134 line_width: None,
135 marker_size: None,
136 }
137 }
138}
139
140pub fn parse_color_token(token: &str) -> Option<StyleColor> {
144 match token.to_ascii_lowercase().as_str() {
145 "r" | "red" => Some(StyleColor(255, 0, 0)),
146 "g" | "green" => Some(StyleColor(0, 128, 0)),
147 "b" | "blue" => Some(StyleColor(0, 0, 255)),
148 "c" | "cyan" => Some(StyleColor(0, 255, 255)),
149 "m" | "magenta" => Some(StyleColor(255, 0, 255)),
150 "y" | "yellow" => Some(StyleColor(255, 255, 0)),
151 "k" | "black" => Some(StyleColor(0, 0, 0)),
152 "w" | "white" => Some(StyleColor(255, 255, 255)),
153 "orange" => Some(StyleColor(255, 165, 0)),
154 "purple" => Some(StyleColor(128, 0, 128)),
155 "gray" | "grey" => Some(StyleColor(128, 128, 128)),
156 s if s.starts_with('#') && s.len() == 7 => {
157 let r = u8::from_str_radix(&s[1..3], 16).ok()?;
158 let g = u8::from_str_radix(&s[3..5], 16).ok()?;
159 let b = u8::from_str_radix(&s[5..7], 16).ok()?;
160 Some(StyleColor(r, g, b))
161 }
162 _ => None,
163 }
164}
165
166pub fn looks_like_style_str(s: &str) -> bool {
171 if s.is_empty() {
172 return false;
173 }
174 if s.starts_with('#') {
175 return s.len() == 7;
176 }
177 if parse_color_token(s).is_some() {
178 return true;
179 }
180 s.chars().all(|c| "rgbcmykw.-:osx+*d^".contains(c))
181}
182
183pub fn parse_style_str(s: &str) -> Result<StyleSpec, String> {
204 if s.is_empty() {
205 return Ok(StyleSpec::default());
206 }
207
208 if let Some(sc) = parse_color_token(s) {
210 return Ok(StyleSpec {
211 color: Some(sc),
212 ..StyleSpec::default()
213 });
214 }
215
216 let mut spec = StyleSpec::default();
217 let bytes = s.as_bytes();
218 let mut i = 0;
219
220 while i < bytes.len() {
221 if i + 1 < bytes.len() && bytes[i] == b'-' && bytes[i + 1] == b'-' {
223 spec.linestyle = LinestyleKind::Dashed;
224 i += 2;
225 continue;
226 }
227 if i + 1 < bytes.len() && bytes[i] == b'-' && bytes[i + 1] == b'.' {
228 spec.linestyle = LinestyleKind::DashDot;
229 i += 2;
230 continue;
231 }
232
233 match bytes[i] {
234 b'-' => spec.linestyle = LinestyleKind::Solid,
235 b':' => spec.linestyle = LinestyleKind::Dotted,
236 b'.' => spec.marker = Some(MarkerKind::Dot),
237 b'o' => spec.marker = Some(MarkerKind::Circle),
238 b'x' => spec.marker = Some(MarkerKind::Cross),
239 b'+' => spec.marker = Some(MarkerKind::Plus),
240 b'*' => spec.marker = Some(MarkerKind::Star),
241 b's' => spec.marker = Some(MarkerKind::Square),
242 b'd' => spec.marker = Some(MarkerKind::Diamond),
243 b'^' => spec.marker = Some(MarkerKind::Triangle),
244 b'r' => spec.color = Some(StyleColor(255, 0, 0)),
245 b'g' => spec.color = Some(StyleColor(0, 128, 0)),
246 b'b' => spec.color = Some(StyleColor(0, 0, 255)),
247 b'c' => spec.color = Some(StyleColor(0, 255, 255)),
248 b'm' => spec.color = Some(StyleColor(255, 0, 255)),
249 b'y' => spec.color = Some(StyleColor(255, 255, 0)),
250 b'k' => spec.color = Some(StyleColor(0, 0, 0)),
251 b'w' => spec.color = Some(StyleColor(255, 255, 255)),
252 other => {
253 return Err(format!(
254 "plot: unknown style character '{}' in style string '{s}'",
255 other as char
256 ));
257 }
258 }
259 i += 1;
260 }
261
262 Ok(spec)
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn test_parse_red_dashed() {
271 let spec = parse_style_str("r--").unwrap();
272 assert_eq!(spec.color, Some(StyleColor(255, 0, 0)));
273 assert_eq!(spec.linestyle, LinestyleKind::Dashed);
274 assert_eq!(spec.marker, None);
275 }
276
277 #[test]
278 fn test_parse_blue_dot() {
279 let spec = parse_style_str("b.").unwrap();
280 assert_eq!(spec.color, Some(StyleColor(0, 0, 255)));
281 assert_eq!(spec.marker, Some(MarkerKind::Dot));
282 assert_eq!(spec.linestyle, LinestyleKind::Solid);
283 }
284
285 #[test]
286 fn test_parse_green_solid() {
287 let spec = parse_style_str("g-").unwrap();
288 assert_eq!(spec.color, Some(StyleColor(0, 128, 0)));
289 assert_eq!(spec.linestyle, LinestyleKind::Solid);
290 assert_eq!(spec.marker, None);
291 }
292
293 #[test]
294 fn test_parse_dashdot() {
295 let spec = parse_style_str("-.").unwrap();
296 assert_eq!(spec.linestyle, LinestyleKind::DashDot);
297 assert_eq!(spec.marker, None);
298 }
299
300 #[test]
301 fn test_parse_dot_then_solid() {
302 let spec = parse_style_str(".-").unwrap();
304 assert_eq!(spec.marker, Some(MarkerKind::Dot));
305 assert_eq!(spec.linestyle, LinestyleKind::Solid);
306 }
307
308 #[test]
309 fn test_parse_dotted_line() {
310 let spec = parse_style_str(":").unwrap();
311 assert_eq!(spec.linestyle, LinestyleKind::Dotted);
312 }
313
314 #[test]
315 fn test_parse_empty_returns_default() {
316 let spec = parse_style_str("").unwrap();
317 assert_eq!(spec, StyleSpec::default());
318 }
319
320 #[test]
321 fn test_parse_unknown_char_errors() {
322 let result = parse_style_str("xyz");
323 assert!(result.is_err());
324 let msg = result.unwrap_err();
325 assert!(msg.contains("unknown style character"));
326 }
327
328 #[test]
329 fn test_looks_like_style_str_valid() {
330 assert!(looks_like_style_str("r--"));
331 assert!(looks_like_style_str("b."));
332 assert!(looks_like_style_str("g-"));
333 assert!(looks_like_style_str("ko"));
334 }
335
336 #[test]
337 fn test_looks_like_style_str_invalid() {
338 assert!(!looks_like_style_str(""));
339 assert!(!looks_like_style_str("time"));
340 assert!(!looks_like_style_str("file.svg"));
341 }
342
343 #[test]
344 fn test_style_full_name_red() {
345 let spec = parse_style_str("red").unwrap();
346 assert_eq!(spec.color, Some(StyleColor(255, 0, 0)));
347 assert_eq!(spec.marker, None);
348 assert_eq!(spec.linestyle, LinestyleKind::Solid);
349 }
350
351 #[test]
352 fn test_style_full_name_orange() {
353 let spec = parse_style_str("orange").unwrap();
354 assert_eq!(spec.color, Some(StyleColor(255, 165, 0)));
355 }
356
357 #[test]
358 fn test_style_gray_grey_alias() {
359 let spec_gray = parse_style_str("gray").unwrap();
360 let spec_grey = parse_style_str("grey").unwrap();
361 assert_eq!(spec_gray.color, spec_grey.color);
362 assert_eq!(spec_gray.color, Some(StyleColor(128, 128, 128)));
363 }
364
365 #[test]
366 fn test_style_hex_color() {
367 let spec = parse_style_str("#1A2B3C").unwrap();
368 assert_eq!(spec.color, Some(StyleColor(0x1A, 0x2B, 0x3C)));
369 }
370
371 #[test]
372 fn test_style_hex_bad_format() {
373 let result = parse_style_str("#1A2B3");
376 assert!(result.is_err(), "short hex should error");
377 }
378}