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, Copy, Debug, PartialEq, Eq, Default)]
115pub enum YAxis {
116 #[default]
118 Left,
119 Right,
121}
122
123#[derive(Clone, Debug, PartialEq)]
125pub struct StyleSpec {
126 pub color: Option<StyleColor>,
128 pub marker: Option<MarkerKind>,
130 pub linestyle: LinestyleKind,
132 pub line_width: Option<f32>,
134 pub marker_size: Option<u32>,
136}
137
138impl Default for StyleSpec {
139 fn default() -> Self {
140 StyleSpec {
141 color: None,
142 marker: None,
143 linestyle: LinestyleKind::Solid,
144 line_width: None,
145 marker_size: None,
146 }
147 }
148}
149
150pub fn parse_color_token(token: &str) -> Option<StyleColor> {
154 match token.to_ascii_lowercase().as_str() {
155 "r" | "red" => Some(StyleColor(255, 0, 0)),
156 "g" | "green" => Some(StyleColor(0, 128, 0)),
157 "b" | "blue" => Some(StyleColor(0, 0, 255)),
158 "c" | "cyan" => Some(StyleColor(0, 255, 255)),
159 "m" | "magenta" => Some(StyleColor(255, 0, 255)),
160 "y" | "yellow" => Some(StyleColor(255, 255, 0)),
161 "k" | "black" => Some(StyleColor(0, 0, 0)),
162 "w" | "white" => Some(StyleColor(255, 255, 255)),
163 "orange" => Some(StyleColor(255, 165, 0)),
164 "purple" => Some(StyleColor(128, 0, 128)),
165 "gray" | "grey" => Some(StyleColor(128, 128, 128)),
166 s if s.starts_with('#') && s.len() == 7 => {
167 let r = u8::from_str_radix(&s[1..3], 16).ok()?;
168 let g = u8::from_str_radix(&s[3..5], 16).ok()?;
169 let b = u8::from_str_radix(&s[5..7], 16).ok()?;
170 Some(StyleColor(r, g, b))
171 }
172 _ => None,
173 }
174}
175
176pub fn looks_like_style_str(s: &str) -> bool {
181 if s.is_empty() {
182 return false;
183 }
184 if s.starts_with('#') {
185 return s.len() == 7;
186 }
187 if parse_color_token(s).is_some() {
188 return true;
189 }
190 s.chars().all(|c| "rgbcmykw.-:osx+*d^".contains(c))
191}
192
193pub fn parse_style_str(s: &str) -> Result<StyleSpec, String> {
214 if s.is_empty() {
215 return Ok(StyleSpec::default());
216 }
217
218 if let Some(sc) = parse_color_token(s) {
220 return Ok(StyleSpec {
221 color: Some(sc),
222 ..StyleSpec::default()
223 });
224 }
225
226 let mut spec = StyleSpec::default();
227 let bytes = s.as_bytes();
228 let mut i = 0;
229
230 while i < bytes.len() {
231 if i + 1 < bytes.len() && bytes[i] == b'-' && bytes[i + 1] == b'-' {
233 spec.linestyle = LinestyleKind::Dashed;
234 i += 2;
235 continue;
236 }
237 if i + 1 < bytes.len() && bytes[i] == b'-' && bytes[i + 1] == b'.' {
238 spec.linestyle = LinestyleKind::DashDot;
239 i += 2;
240 continue;
241 }
242
243 match bytes[i] {
244 b'-' => spec.linestyle = LinestyleKind::Solid,
245 b':' => spec.linestyle = LinestyleKind::Dotted,
246 b'.' => spec.marker = Some(MarkerKind::Dot),
247 b'o' => spec.marker = Some(MarkerKind::Circle),
248 b'x' => spec.marker = Some(MarkerKind::Cross),
249 b'+' => spec.marker = Some(MarkerKind::Plus),
250 b'*' => spec.marker = Some(MarkerKind::Star),
251 b's' => spec.marker = Some(MarkerKind::Square),
252 b'd' => spec.marker = Some(MarkerKind::Diamond),
253 b'^' => spec.marker = Some(MarkerKind::Triangle),
254 b'r' => spec.color = Some(StyleColor(255, 0, 0)),
255 b'g' => spec.color = Some(StyleColor(0, 128, 0)),
256 b'b' => spec.color = Some(StyleColor(0, 0, 255)),
257 b'c' => spec.color = Some(StyleColor(0, 255, 255)),
258 b'm' => spec.color = Some(StyleColor(255, 0, 255)),
259 b'y' => spec.color = Some(StyleColor(255, 255, 0)),
260 b'k' => spec.color = Some(StyleColor(0, 0, 0)),
261 b'w' => spec.color = Some(StyleColor(255, 255, 255)),
262 other => {
263 return Err(format!(
264 "plot: unknown style character '{}' in style string '{s}'",
265 other as char
266 ));
267 }
268 }
269 i += 1;
270 }
271
272 Ok(spec)
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn test_parse_red_dashed() {
281 let spec = parse_style_str("r--").unwrap();
282 assert_eq!(spec.color, Some(StyleColor(255, 0, 0)));
283 assert_eq!(spec.linestyle, LinestyleKind::Dashed);
284 assert_eq!(spec.marker, None);
285 }
286
287 #[test]
288 fn test_parse_blue_dot() {
289 let spec = parse_style_str("b.").unwrap();
290 assert_eq!(spec.color, Some(StyleColor(0, 0, 255)));
291 assert_eq!(spec.marker, Some(MarkerKind::Dot));
292 assert_eq!(spec.linestyle, LinestyleKind::Solid);
293 }
294
295 #[test]
296 fn test_parse_green_solid() {
297 let spec = parse_style_str("g-").unwrap();
298 assert_eq!(spec.color, Some(StyleColor(0, 128, 0)));
299 assert_eq!(spec.linestyle, LinestyleKind::Solid);
300 assert_eq!(spec.marker, None);
301 }
302
303 #[test]
304 fn test_parse_dashdot() {
305 let spec = parse_style_str("-.").unwrap();
306 assert_eq!(spec.linestyle, LinestyleKind::DashDot);
307 assert_eq!(spec.marker, None);
308 }
309
310 #[test]
311 fn test_parse_dot_then_solid() {
312 let spec = parse_style_str(".-").unwrap();
314 assert_eq!(spec.marker, Some(MarkerKind::Dot));
315 assert_eq!(spec.linestyle, LinestyleKind::Solid);
316 }
317
318 #[test]
319 fn test_parse_dotted_line() {
320 let spec = parse_style_str(":").unwrap();
321 assert_eq!(spec.linestyle, LinestyleKind::Dotted);
322 }
323
324 #[test]
325 fn test_parse_empty_returns_default() {
326 let spec = parse_style_str("").unwrap();
327 assert_eq!(spec, StyleSpec::default());
328 }
329
330 #[test]
331 fn test_parse_unknown_char_errors() {
332 let result = parse_style_str("xyz");
333 assert!(result.is_err());
334 let msg = result.unwrap_err();
335 assert!(msg.contains("unknown style character"));
336 }
337
338 #[test]
339 fn test_looks_like_style_str_valid() {
340 assert!(looks_like_style_str("r--"));
341 assert!(looks_like_style_str("b."));
342 assert!(looks_like_style_str("g-"));
343 assert!(looks_like_style_str("ko"));
344 }
345
346 #[test]
347 fn test_looks_like_style_str_invalid() {
348 assert!(!looks_like_style_str(""));
349 assert!(!looks_like_style_str("time"));
350 assert!(!looks_like_style_str("file.svg"));
351 }
352
353 #[test]
354 fn test_style_full_name_red() {
355 let spec = parse_style_str("red").unwrap();
356 assert_eq!(spec.color, Some(StyleColor(255, 0, 0)));
357 assert_eq!(spec.marker, None);
358 assert_eq!(spec.linestyle, LinestyleKind::Solid);
359 }
360
361 #[test]
362 fn test_style_full_name_orange() {
363 let spec = parse_style_str("orange").unwrap();
364 assert_eq!(spec.color, Some(StyleColor(255, 165, 0)));
365 }
366
367 #[test]
368 fn test_style_gray_grey_alias() {
369 let spec_gray = parse_style_str("gray").unwrap();
370 let spec_grey = parse_style_str("grey").unwrap();
371 assert_eq!(spec_gray.color, spec_grey.color);
372 assert_eq!(spec_gray.color, Some(StyleColor(128, 128, 128)));
373 }
374
375 #[test]
376 fn test_style_hex_color() {
377 let spec = parse_style_str("#1A2B3C").unwrap();
378 assert_eq!(spec.color, Some(StyleColor(0x1A, 0x2B, 0x3C)));
379 }
380
381 #[test]
382 fn test_style_hex_bad_format() {
383 let result = parse_style_str("#1A2B3");
386 assert!(result.is_err(), "short hex should error");
387 }
388}