Skip to main content

shape_viz_core/
theme.rs

1//! Theme and styling system for charts
2
3use serde::{Deserialize, Serialize};
4
5/// RGBA color representation
6#[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    /// Create a color from a hex string (e.g., "#ff0066" or "ff0066")
33    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    /// Create a color from hex literal (for const contexts)
52    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    /// Lighten the color by a factor (0.0 = no change, 1.0 = white)
68    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    /// Darken the color by a factor (0.0 = no change, 1.0 = black)
79    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    // Common colors
90    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/// Color scheme for different chart themes
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct ColorScheme {
101    // Background colors
102    pub background: Color,
103    pub chart_background: Color,
104    pub axis_background: Color,
105
106    // Grid colors
107    pub grid_major: Color,
108    pub grid_minor: Color,
109
110    // Candlestick colors
111    pub candle_bullish: Color,
112    pub candle_bearish: Color,
113    pub candle_doji: Color,
114    pub wick_color: Color,
115    pub wick_bullish: Color, // Optional separate wick colors
116    pub wick_bearish: Color,
117
118    // Text colors
119    pub text_primary: Color,
120    pub text_secondary: Color,
121    pub text_muted: Color,
122    pub text_accent: Color,
123
124    // Axis colors
125    pub axis_line: Color,
126    pub axis_tick: Color,
127    pub axis_label: Color,
128    pub axis_separator: Color, // Line between price and time axis
129
130    // Interactive elements
131    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    // Volume colors
139    pub volume_bullish: Color,
140    pub volume_bearish: Color,
141    pub volume_neutral: Color,
142
143    // Indicator colors
144    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    // Alert/Status colors
151    pub success: Color,
152    pub warning: Color,
153    pub error: Color,
154    pub info: Color,
155
156    // Additional UI elements
157    pub border: Color,
158    pub shadow: Color,
159    pub overlay: Color,
160}
161
162impl ColorScheme {
163    /// Create a custom theme from primary colors
164    pub fn custom(
165        background: Color,
166        bullish: Color,
167        bearish: Color,
168        text: Color,
169        grid: Color,
170    ) -> Self {
171        Self {
172            // Background colors
173            background,
174            chart_background: background.lighten(0.05),
175            axis_background: background.lighten(0.08),
176
177            // Grid colors
178            grid_major: grid.with_alpha(0.5),
179            grid_minor: grid.with_alpha(0.3),
180
181            // Candlestick colors
182            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 colors
190            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 colors
196            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            // Interactive elements
202            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 colors
210            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 colors
215            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            // Alert/Status colors
222            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            // Additional UI elements
228            border: grid.lighten(0.1),
229            shadow: Color::rgba(0, 0, 0, 128),
230            overlay: background.with_alpha(0.8),
231        }
232    }
233
234    /// Reference chart theme - matches the provided reference image exactly
235    pub fn reference_dark() -> Self {
236        Self {
237            // Background colors - exact dark blue from reference
238            background: Color::hex(0x0d1117), // #0d1117 - Exact dark blue from reference
239            chart_background: Color::hex(0x0d1117), // Same as background
240            axis_background: Color::hex(0x0d1117), // Same as background
241
242            // Grid colors - very visible to match reference
243            grid_major: Color::hex(0x2d3748).with_alpha(1.0), // Full opacity for visibility
244            grid_minor: Color::hex(0x2d3748).with_alpha(0.7), // High opacity
245
246            // Candlestick colors - exact match to reference
247            candle_bullish: Color::hex(0x00d9ff), // #00d9ff - Bright cyan/blue
248            candle_bearish: Color::hex(0xff0080), // #ff0080 - Hot pink/magenta
249            candle_doji: Color::hex(0x9ca3af),    // Neutral gray
250            wick_color: Color::hex(0x9ca3af).with_alpha(0.8),
251            wick_bullish: Color::hex(0x00d9ff),
252            wick_bearish: Color::hex(0xff0080),
253
254            // Text colors - brighter for better visibility
255            text_primary: Color::hex(0xe0e0e0), // #e0e0e0 - Light gray/white
256            text_secondary: Color::hex(0xb0b0b0), // Slightly dimmer
257            text_muted: Color::hex(0x808080),   // Muted gray
258            text_accent: Color::hex(0x00d9ff),  // Match bullish color
259
260            // Axis colors
261            axis_line: Color::hex(0x2a2e3a), // #2a2e3a - Slightly brighter than grid
262            axis_tick: Color::hex(0x2a2e3a),
263            axis_label: Color::hex(0xb0b0b0), // Light gray for better visibility
264            axis_separator: Color::hex(0x2a2e3a),
265
266            // Interactive elements
267            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 colors - match candles with proper transparency
275            volume_bullish: Color::hex(0x00d9ff).with_alpha(0.5), // Same as candle_bullish with 50% alpha
276            volume_bearish: Color::hex(0xff0080).with_alpha(0.5), // Same as candle_bearish with 50% alpha
277            volume_neutral: Color::hex(0x9ca3af).with_alpha(0.3),
278
279            // Indicator colors
280            indicator_primary: Color::hex(0xffd93d), // Yellow
281            indicator_secondary: Color::hex(0x6a5acd), // Purple
282            indicator_tertiary: Color::hex(0x00bcd4), // Cyan
283            indicator_quaternary: Color::hex(0xff5722), // Orange
284            indicator_quinary: Color::hex(0x8bc34a), // Green
285
286            // Alert/Status colors
287            success: Color::hex(0x4caf50),
288            warning: Color::hex(0xff9800),
289            error: Color::hex(0xf44336),
290            info: Color::hex(0x2196f3),
291
292            // Additional UI elements
293            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    /// TradingView-style dark theme (updated)
300    pub fn tradingview_dark() -> Self {
301        Self {
302            // Background colors - deep dark blue/black
303            background: Color::hex(0x131722), // Very dark blue-black
304            chart_background: Color::hex(0x131722), // Same as background
305            axis_background: Color::hex(0x131722), // Same as background for seamless panel
306
307            // Grid colors - subtle gray lines
308            grid_major: Color::hex(0x363a45).with_alpha(0.5), // Solid, slightly more prominent
309            grid_minor: Color::hex(0x242730).with_alpha(0.3), // Solid, very subtle
310
311            // Candlestick colors - bright cyan/teal and red/pink
312            candle_bullish: Color::hex(0x26a69a), // Bright teal/cyan (bullish)
313            candle_bearish: Color::hex(0xef5350), // Bright pink/red (bearish)
314            candle_doji: Color::rgba(120, 123, 134, 255), // Neutral gray for doji
315            wick_color: Color::rgba(120, 123, 134, 255), // Gray wicks
316            wick_bullish: Color::hex(0x26a69a),
317            wick_bearish: Color::hex(0xef5350),
318
319            // Text colors - various shades of gray/white
320            text_primary: Color::rgba(240, 243, 250, 255), // Almost white
321            text_secondary: Color::rgba(180, 185, 195, 255), // Light gray
322            text_muted: Color::rgba(120, 123, 134, 255),   // Muted gray
323            text_accent: Color::rgba(34, 206, 170, 255),
324
325            // Axis colors
326            axis_line: Color::rgba(60, 64, 75, 255), // Subtle axis lines
327            axis_tick: Color::rgba(120, 123, 134, 255), // Tick marks
328            axis_label: Color::rgba(240, 243, 250, 255), // Price/time labels
329            axis_separator: Color::rgba(60, 64, 75, 255),
330
331            // Interactive elements
332            crosshair: Color::rgba(100, 150, 255, 200), // Blue crosshair
333            selection: Color::rgba(100, 150, 255, 100), // Selection highlight
334            highlight: Color::rgba(255, 255, 255, 50),  // Hover highlight
335            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 colors - matching candlestick colors but more subdued
340            volume_bullish: Color::hex(0x26a69a).with_alpha(0.4), // Same as candle_bullish with 40% alpha
341            volume_bearish: Color::hex(0xef5350).with_alpha(0.4), // Same as candle_bearish with 40% alpha
342            volume_neutral: Color::rgba(120, 123, 134, 100),
343
344            // Indicator colors - bright distinguishable colors
345            indicator_primary: Color::rgba(255, 193, 7, 255), // Golden yellow
346            indicator_secondary: Color::rgba(156, 39, 176, 255), // Purple
347            indicator_tertiary: Color::rgba(0, 188, 212, 255), // Cyan
348            indicator_quaternary: Color::rgba(255, 87, 34, 255), // Deep orange
349            indicator_quinary: Color::rgba(139, 195, 74, 255), // Light green
350
351            // Alert colors
352            success: Color::rgba(76, 175, 80, 255), // Green
353            warning: Color::rgba(255, 152, 0, 255), // Orange
354            error: Color::rgba(244, 67, 54, 255),   // Red
355            info: Color::rgba(33, 150, 243, 255),   // Blue
356
357            // Additional UI elements
358            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    /// Classic light theme
365    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), // Green
375            candle_bearish: Color::hex(0xff006e),          // Red
376            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    /// Midnight theme - deep blue/purple dark theme
420    pub fn midnight() -> Self {
421        Self::custom(
422            Color::hex(0x0f0f23), // Deep midnight blue background
423            Color::hex(0x00ff88), // Bright green bullish
424            Color::hex(0xff0055), // Bright red bearish
425            Color::hex(0xc9d1d9), // Light gray text
426            Color::hex(0x30363d), // Dark gray grid
427        )
428    }
429
430    /// Monokai theme - inspired by the popular code editor theme
431    pub fn monokai() -> Self {
432        Self::custom(
433            Color::hex(0x272822), // Monokai background
434            Color::hex(0xa6e22e), // Monokai green
435            Color::hex(0xf92672), // Monokai red
436            Color::hex(0xf8f8f2), // Monokai foreground
437            Color::hex(0x3e3d32), // Monokai comments
438        )
439    }
440
441    /// High contrast dark theme for accessibility
442    pub fn high_contrast_dark() -> Self {
443        Self::custom(
444            Color::hex(0x000000), // Pure black background
445            Color::hex(0x00ff00), // Pure green bullish
446            Color::hex(0xff0000), // Pure red bearish
447            Color::hex(0xffffff), // Pure white text
448            Color::hex(0x404040), // Dark gray grid
449        )
450    }
451}
452
453/// Typography settings
454#[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,          // Larger to match reference
467            secondary_font_size: 11.0,        // Larger axis labels
468            small_font_size: 10.0,            // Larger small text
469            font_family: "Inter".to_string(), // Modern, readable font
470            line_height: 1.2,
471        }
472    }
473}
474
475/// Spacing and sizing constants
476#[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,      // Space for price/time axes
491            tick_length: 5.0,       // Length of axis tick marks
492            label_padding: 8.0,     // Padding around text labels
493            candle_min_width: 1.0,  // Minimum candle width
494            candle_max_width: 20.0, // Maximum candle width
495            grid_spacing_min: 40.0, // Minimum pixels between grid lines
496            crosshair_width: 0.5,   // Crosshair line width
497        }
498    }
499}
500
501/// Complete chart theme
502#[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    /// Create a custom theme with a name and color scheme
512    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    /// Create a custom theme with full control
522    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    /// Create reference dark theme - matches the provided reference image
537    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    /// Create TradingView-style dark theme
547    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    /// Create light theme
557    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    /// Create midnight theme
567    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    /// Create monokai theme
577    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    /// Create high contrast dark theme
587    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    /// Get a list of all available built-in themes
597    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    /// Get a theme by name
609    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() // Use reference dark as default to match the provided image
619    }
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        // Verify some key colors match TradingView style
672        let colors = &theme.colors;
673        // Bullish color should be teal/cyan-ish
674        assert!(colors.candle_bullish.g > 0.6); // High green component
675        assert!(colors.candle_bullish.b > 0.5); // Some blue component
676
677        // Bearish color should be red/pink-ish
678        assert!(colors.candle_bearish.r > 0.8); // High red component
679    }
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        // Test exact color matches – these should mirror the definition in reference_dark()
688        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}