1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
7pub struct Color {
8 pub r: f32,
9 pub g: f32,
10 pub b: f32,
11 pub a: f32,
12}
13
14impl Color {
15 pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
16 Self { r, g, b, a }
17 }
18
19 pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
20 Self::new(r, g, b, 1.0)
21 }
22
23 pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
24 Self::new(
25 r as f32 / 255.0,
26 g as f32 / 255.0,
27 b as f32 / 255.0,
28 a as f32 / 255.0,
29 )
30 }
31
32 pub fn from_hex(hex: &str) -> Result<Self, String> {
34 let hex = hex.trim_start_matches('#');
35 if hex.len() != 6 && hex.len() != 8 {
36 return Err("Hex color must be 6 or 8 characters".to_string());
37 }
38
39 let r = u8::from_str_radix(&hex[0..2], 16).map_err(|e| e.to_string())?;
40 let g = u8::from_str_radix(&hex[2..4], 16).map_err(|e| e.to_string())?;
41 let b = u8::from_str_radix(&hex[4..6], 16).map_err(|e| e.to_string())?;
42 let a = if hex.len() == 8 {
43 u8::from_str_radix(&hex[6..8], 16).map_err(|e| e.to_string())?
44 } else {
45 255
46 };
47
48 Ok(Self::rgba(r, g, b, a))
49 }
50
51 pub const fn hex(value: u32) -> Self {
53 let r = ((value >> 16) & 0xFF) as f32 / 255.0;
54 let g = ((value >> 8) & 0xFF) as f32 / 255.0;
55 let b = (value & 0xFF) as f32 / 255.0;
56 Self::new(r, g, b, 1.0)
57 }
58
59 pub fn with_alpha(&self, alpha: f32) -> Self {
60 Self::new(self.r, self.g, self.b, alpha)
61 }
62
63 pub fn to_array(&self) -> [f32; 4] {
64 [self.r, self.g, self.b, self.a]
65 }
66
67 pub fn lighten(&self, factor: f32) -> Self {
69 let factor = factor.clamp(0.0, 1.0);
70 Self::new(
71 self.r + (1.0 - self.r) * factor,
72 self.g + (1.0 - self.g) * factor,
73 self.b + (1.0 - self.b) * factor,
74 self.a,
75 )
76 }
77
78 pub fn darken(&self, factor: f32) -> Self {
80 let factor = factor.clamp(0.0, 1.0);
81 Self::new(
82 self.r * (1.0 - factor),
83 self.g * (1.0 - factor),
84 self.b * (1.0 - factor),
85 self.a,
86 )
87 }
88
89 pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0);
91 pub const WHITE: Self = Self::rgb(1.0, 1.0, 1.0);
92 pub const RED: Self = Self::rgb(1.0, 0.0, 0.0);
93 pub const GREEN: Self = Self::rgb(0.0, 1.0, 0.0);
94 pub const BLUE: Self = Self::rgb(0.0, 0.0, 1.0);
95 pub const TRANSPARENT: Self = Self::new(0.0, 0.0, 0.0, 0.0);
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct ColorScheme {
101 pub background: Color,
103 pub chart_background: Color,
104 pub axis_background: Color,
105
106 pub grid_major: Color,
108 pub grid_minor: Color,
109
110 pub candle_bullish: Color,
112 pub candle_bearish: Color,
113 pub candle_doji: Color,
114 pub wick_color: Color,
115 pub wick_bullish: Color, pub wick_bearish: Color,
117
118 pub text_primary: Color,
120 pub text_secondary: Color,
121 pub text_muted: Color,
122 pub text_accent: Color,
123
124 pub axis_line: Color,
126 pub axis_tick: Color,
127 pub axis_label: Color,
128 pub axis_separator: Color, pub crosshair: Color,
132 pub selection: Color,
133 pub highlight: Color,
134 pub tooltip_background: Color,
135 pub tooltip_text: Color,
136 pub tooltip_border: Color,
137
138 pub volume_bullish: Color,
140 pub volume_bearish: Color,
141 pub volume_neutral: Color,
142
143 pub indicator_primary: Color,
145 pub indicator_secondary: Color,
146 pub indicator_tertiary: Color,
147 pub indicator_quaternary: Color,
148 pub indicator_quinary: Color,
149
150 pub success: Color,
152 pub warning: Color,
153 pub error: Color,
154 pub info: Color,
155
156 pub border: Color,
158 pub shadow: Color,
159 pub overlay: Color,
160}
161
162impl ColorScheme {
163 pub fn custom(
165 background: Color,
166 bullish: Color,
167 bearish: Color,
168 text: Color,
169 grid: Color,
170 ) -> Self {
171 Self {
172 background,
174 chart_background: background.lighten(0.05),
175 axis_background: background.lighten(0.08),
176
177 grid_major: grid.with_alpha(0.5),
179 grid_minor: grid.with_alpha(0.3),
180
181 candle_bullish: bullish,
183 candle_bearish: bearish,
184 candle_doji: text.with_alpha(0.5),
185 wick_color: text.with_alpha(0.5),
186 wick_bullish: bullish.darken(0.2),
187 wick_bearish: bearish.darken(0.2),
188
189 text_primary: text,
191 text_secondary: text.with_alpha(0.8),
192 text_muted: text.with_alpha(0.5),
193 text_accent: bullish,
194
195 axis_line: grid.lighten(0.2),
197 axis_tick: text.with_alpha(0.5),
198 axis_label: text.with_alpha(0.8),
199 axis_separator: grid.lighten(0.3),
200
201 crosshair: text.with_alpha(0.8),
203 selection: bullish.with_alpha(0.3),
204 highlight: text.with_alpha(0.1),
205 tooltip_background: background.lighten(0.1),
206 tooltip_text: text,
207 tooltip_border: grid.lighten(0.2),
208
209 volume_bullish: bullish.with_alpha(0.7),
211 volume_bearish: bearish.with_alpha(0.7),
212 volume_neutral: text.with_alpha(0.3),
213
214 indicator_primary: Color::rgba(255, 193, 7, 255),
216 indicator_secondary: Color::rgba(156, 39, 176, 255),
217 indicator_tertiary: Color::rgba(0, 188, 212, 255),
218 indicator_quaternary: Color::rgba(255, 87, 34, 255),
219 indicator_quinary: Color::rgba(139, 195, 74, 255),
220
221 success: Color::rgba(76, 175, 80, 255),
223 warning: Color::rgba(255, 152, 0, 255),
224 error: Color::rgba(244, 67, 54, 255),
225 info: Color::rgba(33, 150, 243, 255),
226
227 border: grid.lighten(0.1),
229 shadow: Color::rgba(0, 0, 0, 128),
230 overlay: background.with_alpha(0.8),
231 }
232 }
233
234 pub fn reference_dark() -> Self {
236 Self {
237 background: Color::hex(0x0d1117), chart_background: Color::hex(0x0d1117), axis_background: Color::hex(0x0d1117), grid_major: Color::hex(0x2d3748).with_alpha(1.0), grid_minor: Color::hex(0x2d3748).with_alpha(0.7), candle_bullish: Color::hex(0x00d9ff), candle_bearish: Color::hex(0xff0080), candle_doji: Color::hex(0x9ca3af), wick_color: Color::hex(0x9ca3af).with_alpha(0.8),
251 wick_bullish: Color::hex(0x00d9ff),
252 wick_bearish: Color::hex(0xff0080),
253
254 text_primary: Color::hex(0xe0e0e0), text_secondary: Color::hex(0xb0b0b0), text_muted: Color::hex(0x808080), text_accent: Color::hex(0x00d9ff), axis_line: Color::hex(0x2a2e3a), axis_tick: Color::hex(0x2a2e3a),
263 axis_label: Color::hex(0xb0b0b0), axis_separator: Color::hex(0x2a2e3a),
265
266 crosshair: Color::hex(0x9ca3af).with_alpha(0.8),
268 selection: Color::hex(0x00d4aa).with_alpha(0.3),
269 highlight: Color::hex(0xffffff).with_alpha(0.05),
270 tooltip_background: Color::hex(0x1a1e2a),
271 tooltip_text: Color::hex(0x9ca3af),
272 tooltip_border: Color::hex(0x2a2e3a),
273
274 volume_bullish: Color::hex(0x00d9ff).with_alpha(0.5), volume_bearish: Color::hex(0xff0080).with_alpha(0.5), volume_neutral: Color::hex(0x9ca3af).with_alpha(0.3),
278
279 indicator_primary: Color::hex(0xffd93d), indicator_secondary: Color::hex(0x6a5acd), indicator_tertiary: Color::hex(0x00bcd4), indicator_quaternary: Color::hex(0xff5722), indicator_quinary: Color::hex(0x8bc34a), success: Color::hex(0x4caf50),
288 warning: Color::hex(0xff9800),
289 error: Color::hex(0xf44336),
290 info: Color::hex(0x2196f3),
291
292 border: Color::hex(0x2a2e3a),
294 shadow: Color::rgba(0, 0, 0, 180),
295 overlay: Color::hex(0x0a0e1a).with_alpha(0.9),
296 }
297 }
298
299 pub fn tradingview_dark() -> Self {
301 Self {
302 background: Color::hex(0x131722), chart_background: Color::hex(0x131722), axis_background: Color::hex(0x131722), grid_major: Color::hex(0x363a45).with_alpha(0.5), grid_minor: Color::hex(0x242730).with_alpha(0.3), candle_bullish: Color::hex(0x26a69a), candle_bearish: Color::hex(0xef5350), candle_doji: Color::rgba(120, 123, 134, 255), wick_color: Color::rgba(120, 123, 134, 255), wick_bullish: Color::hex(0x26a69a),
317 wick_bearish: Color::hex(0xef5350),
318
319 text_primary: Color::rgba(240, 243, 250, 255), text_secondary: Color::rgba(180, 185, 195, 255), text_muted: Color::rgba(120, 123, 134, 255), text_accent: Color::rgba(34, 206, 170, 255),
324
325 axis_line: Color::rgba(60, 64, 75, 255), axis_tick: Color::rgba(120, 123, 134, 255), axis_label: Color::rgba(240, 243, 250, 255), axis_separator: Color::rgba(60, 64, 75, 255),
330
331 crosshair: Color::rgba(100, 150, 255, 200), selection: Color::rgba(100, 150, 255, 100), highlight: Color::rgba(255, 255, 255, 50), tooltip_background: Color::rgba(30, 34, 45, 240),
336 tooltip_text: Color::rgba(240, 243, 250, 255),
337 tooltip_border: Color::rgba(60, 64, 75, 255),
338
339 volume_bullish: Color::hex(0x26a69a).with_alpha(0.4), volume_bearish: Color::hex(0xef5350).with_alpha(0.4), volume_neutral: Color::rgba(120, 123, 134, 100),
343
344 indicator_primary: Color::rgba(255, 193, 7, 255), indicator_secondary: Color::rgba(156, 39, 176, 255), indicator_tertiary: Color::rgba(0, 188, 212, 255), indicator_quaternary: Color::rgba(255, 87, 34, 255), indicator_quinary: Color::rgba(139, 195, 74, 255), success: Color::rgba(76, 175, 80, 255), warning: Color::rgba(255, 152, 0, 255), error: Color::rgba(244, 67, 54, 255), info: Color::rgba(33, 150, 243, 255), border: Color::rgba(60, 64, 75, 255),
359 shadow: Color::rgba(0, 0, 0, 180),
360 overlay: Color::rgba(16, 21, 30, 230),
361 }
362 }
363
364 pub fn light() -> Self {
366 Self {
367 background: Color::rgba(255, 255, 255, 255),
368 chart_background: Color::rgba(252, 252, 252, 255),
369 axis_background: Color::rgba(248, 248, 248, 255),
370
371 grid_major: Color::rgba(200, 200, 200, 255),
372 grid_minor: Color::rgba(230, 230, 230, 255),
373
374 candle_bullish: Color::rgba(76, 175, 80, 255), candle_bearish: Color::hex(0xff006e), candle_doji: Color::rgba(158, 158, 158, 255),
377 wick_color: Color::rgba(97, 97, 97, 255),
378 wick_bullish: Color::rgba(76, 175, 80, 200),
379 wick_bearish: Color::rgba(244, 67, 54, 200),
380
381 text_primary: Color::rgba(33, 37, 41, 255),
382 text_secondary: Color::rgba(108, 117, 125, 255),
383 text_muted: Color::rgba(173, 181, 189, 255),
384 text_accent: Color::rgba(76, 175, 80, 255),
385
386 axis_line: Color::rgba(200, 200, 200, 255),
387 axis_tick: Color::rgba(150, 150, 150, 255),
388 axis_label: Color::rgba(100, 100, 100, 255),
389 axis_separator: Color::rgba(200, 200, 200, 255),
390
391 crosshair: Color::rgba(0, 123, 255, 200),
392 selection: Color::rgba(0, 123, 255, 100),
393 highlight: Color::rgba(0, 0, 0, 30),
394 tooltip_background: Color::rgba(255, 255, 255, 240),
395 tooltip_text: Color::rgba(33, 37, 41, 255),
396 tooltip_border: Color::rgba(200, 200, 200, 255),
397
398 volume_bullish: Color::rgba(76, 175, 80, 180),
399 volume_bearish: Color::rgba(244, 67, 54, 180),
400 volume_neutral: Color::rgba(158, 158, 158, 100),
401
402 indicator_primary: Color::rgba(255, 193, 7, 255),
403 indicator_secondary: Color::rgba(156, 39, 176, 255),
404 indicator_tertiary: Color::rgba(0, 188, 212, 255),
405 indicator_quaternary: Color::rgba(255, 87, 34, 255),
406 indicator_quinary: Color::rgba(139, 195, 74, 255),
407
408 success: Color::rgba(40, 167, 69, 255),
409 warning: Color::rgba(255, 193, 7, 255),
410 error: Color::rgba(220, 53, 69, 255),
411 info: Color::rgba(23, 162, 184, 255),
412
413 border: Color::rgba(200, 200, 200, 255),
414 shadow: Color::rgba(0, 0, 0, 50),
415 overlay: Color::rgba(255, 255, 255, 230),
416 }
417 }
418
419 pub fn midnight() -> Self {
421 Self::custom(
422 Color::hex(0x0f0f23), Color::hex(0x00ff88), Color::hex(0xff0055), Color::hex(0xc9d1d9), Color::hex(0x30363d), )
428 }
429
430 pub fn monokai() -> Self {
432 Self::custom(
433 Color::hex(0x272822), Color::hex(0xa6e22e), Color::hex(0xf92672), Color::hex(0xf8f8f2), Color::hex(0x3e3d32), )
439 }
440
441 pub fn high_contrast_dark() -> Self {
443 Self::custom(
444 Color::hex(0x000000), Color::hex(0x00ff00), Color::hex(0xff0000), Color::hex(0xffffff), Color::hex(0x404040), )
450 }
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct Typography {
456 pub primary_font_size: f32,
457 pub secondary_font_size: f32,
458 pub small_font_size: f32,
459 pub font_family: String,
460 pub line_height: f32,
461}
462
463impl Default for Typography {
464 fn default() -> Self {
465 Self {
466 primary_font_size: 12.0, secondary_font_size: 11.0, small_font_size: 10.0, font_family: "Inter".to_string(), line_height: 1.2,
471 }
472 }
473}
474
475#[derive(Debug, Clone, Serialize, Deserialize)]
477pub struct Spacing {
478 pub axis_margin: f32,
479 pub tick_length: f32,
480 pub label_padding: f32,
481 pub candle_min_width: f32,
482 pub candle_max_width: f32,
483 pub grid_spacing_min: f32,
484 pub crosshair_width: f32,
485}
486
487impl Default for Spacing {
488 fn default() -> Self {
489 Self {
490 axis_margin: 60.0, tick_length: 5.0, label_padding: 8.0, candle_min_width: 1.0, candle_max_width: 20.0, grid_spacing_min: 40.0, crosshair_width: 0.5, }
498 }
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize)]
503pub struct ChartTheme {
504 pub name: String,
505 pub colors: ColorScheme,
506 pub typography: Typography,
507 pub spacing: Spacing,
508}
509
510impl ChartTheme {
511 pub fn new(name: impl Into<String>, colors: ColorScheme) -> Self {
513 Self {
514 name: name.into(),
515 colors,
516 typography: Typography::default(),
517 spacing: Spacing::default(),
518 }
519 }
520
521 pub fn custom(
523 name: impl Into<String>,
524 colors: ColorScheme,
525 typography: Typography,
526 spacing: Spacing,
527 ) -> Self {
528 Self {
529 name: name.into(),
530 colors,
531 typography,
532 spacing,
533 }
534 }
535
536 pub fn reference_dark() -> Self {
538 Self {
539 name: "Reference Dark".to_string(),
540 colors: ColorScheme::reference_dark(),
541 typography: Typography::default(),
542 spacing: Spacing::default(),
543 }
544 }
545
546 pub fn tradingview_dark() -> Self {
548 Self {
549 name: "TradingView Dark".to_string(),
550 colors: ColorScheme::tradingview_dark(),
551 typography: Typography::default(),
552 spacing: Spacing::default(),
553 }
554 }
555
556 pub fn light() -> Self {
558 Self {
559 name: "Light".to_string(),
560 colors: ColorScheme::light(),
561 typography: Typography::default(),
562 spacing: Spacing::default(),
563 }
564 }
565
566 pub fn midnight() -> Self {
568 Self {
569 name: "Midnight".to_string(),
570 colors: ColorScheme::midnight(),
571 typography: Typography::default(),
572 spacing: Spacing::default(),
573 }
574 }
575
576 pub fn monokai() -> Self {
578 Self {
579 name: "Monokai".to_string(),
580 colors: ColorScheme::monokai(),
581 typography: Typography::default(),
582 spacing: Spacing::default(),
583 }
584 }
585
586 pub fn high_contrast_dark() -> Self {
588 Self {
589 name: "High Contrast Dark".to_string(),
590 colors: ColorScheme::high_contrast_dark(),
591 typography: Typography::default(),
592 spacing: Spacing::default(),
593 }
594 }
595
596 pub fn all_themes() -> Vec<Self> {
598 vec![
599 Self::reference_dark(),
600 Self::tradingview_dark(),
601 Self::light(),
602 Self::midnight(),
603 Self::monokai(),
604 Self::high_contrast_dark(),
605 ]
606 }
607
608 pub fn by_name(name: &str) -> Option<Self> {
610 Self::all_themes()
611 .into_iter()
612 .find(|theme| theme.name.eq_ignore_ascii_case(name))
613 }
614}
615
616impl Default for ChartTheme {
617 fn default() -> Self {
618 Self::reference_dark() }
620}
621
622#[cfg(test)]
623mod tests {
624 use super::*;
625
626 #[test]
627 fn test_color_creation() {
628 let color = Color::rgba(255, 128, 64, 200);
629 assert_eq!(color.r, 1.0);
630 assert_eq!(color.g, 128.0 / 255.0);
631 assert_eq!(color.b, 64.0 / 255.0);
632 assert_eq!(color.a, 200.0 / 255.0);
633 }
634
635 #[test]
636 fn test_color_from_hex() {
637 let color = Color::from_hex("#ff0066").unwrap();
638 assert_eq!(color.r, 1.0);
639 assert_eq!(color.g, 0.0);
640 assert!((color.b - 0.4).abs() < 0.01);
641 assert_eq!(color.a, 1.0);
642
643 let color2 = Color::from_hex("00d4aa").unwrap();
644 assert_eq!(color2.r, 0.0);
645 assert!((color2.g - 0.831).abs() < 0.01);
646 assert!((color2.b - 0.667).abs() < 0.01);
647 }
648
649 #[test]
650 fn test_color_hex_const() {
651 let color = Color::hex(0xff0066);
652 assert_eq!(color.r, 1.0);
653 assert_eq!(color.g, 0.0);
654 assert!((color.b - 0.4).abs() < 0.01);
655 }
656
657 #[test]
658 fn test_color_with_alpha() {
659 let color = Color::RED.with_alpha(0.5);
660 assert_eq!(color.r, 1.0);
661 assert_eq!(color.g, 0.0);
662 assert_eq!(color.b, 0.0);
663 assert_eq!(color.a, 0.5);
664 }
665
666 #[test]
667 fn test_theme_creation() {
668 let theme = ChartTheme::tradingview_dark();
669 assert_eq!(theme.name, "TradingView Dark");
670
671 let colors = &theme.colors;
673 assert!(colors.candle_bullish.g > 0.6); assert!(colors.candle_bullish.b > 0.5); assert!(colors.candle_bearish.r > 0.8); }
680
681 #[test]
682 fn test_reference_theme() {
683 let theme = ChartTheme::reference_dark();
684 assert_eq!(theme.name, "Reference Dark");
685
686 let colors = &theme.colors;
687 assert_eq!(colors.background, Color::hex(0x0d1117));
689 assert_eq!(colors.candle_bullish, Color::hex(0x00d9ff));
690 assert_eq!(colors.candle_bearish, Color::hex(0xff0080));
691 assert_eq!(colors.text_primary, Color::hex(0xe0e0e0));
692 }
693
694 #[test]
695 fn test_theme_by_name() {
696 assert!(ChartTheme::by_name("reference dark").is_some());
697 assert!(ChartTheme::by_name("TRADINGVIEW DARK").is_some());
698 assert!(ChartTheme::by_name("light").is_some());
699 assert!(ChartTheme::by_name("nonexistent").is_none());
700 }
701
702 #[test]
703 fn test_color_modifications() {
704 let color = Color::hex(0xff0066);
705 let lighter = color.lighten(0.2);
706 assert!(lighter.r >= color.r);
707 assert!(lighter.g >= color.g);
708 assert!(lighter.b >= color.b);
709
710 let darker = color.darken(0.2);
711 assert!(darker.r <= color.r);
712 assert!(darker.g <= color.g);
713 assert!(darker.b <= color.b);
714 }
715}