Skip to main content

nova_plot/
style.rs

1//! Chart styling configuration.
2
3use serde::{Deserialize, Serialize};
4
5/// Visual style configuration for charts.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ChartStyle {
8    /// Background color.
9    pub background: Color,
10
11    /// Primary color for data.
12    pub primary: Color,
13
14    /// Secondary colors for additional series.
15    pub palette: Vec<Color>,
16
17    /// Axis color.
18    pub axis_color: Color,
19
20    /// Grid line color.
21    pub grid_color: Color,
22
23    /// Text color.
24    pub text_color: Color,
25
26    /// Font family.
27    pub font_family: String,
28
29    /// Title font size.
30    pub title_font_size: f64,
31
32    /// Label font size.
33    pub label_font_size: f64,
34
35    /// Axis font size.
36    pub axis_font_size: f64,
37
38    /// Line width for line charts.
39    pub line_width: f64,
40
41    /// Point radius for scatter plots.
42    pub point_radius: f64,
43
44    /// Show grid lines.
45    pub show_grid: bool,
46
47    /// Show legend.
48    pub show_legend: bool,
49
50    /// Padding around the chart.
51    pub padding: Padding,
52}
53
54impl Default for ChartStyle {
55    fn default() -> Self {
56        Self {
57            background: Color::WHITE,
58            primary: Color::from_hex("#4285f4"),
59            palette: vec![
60                Color::from_hex("#4285f4"), // Blue
61                Color::from_hex("#ea4335"), // Red
62                Color::from_hex("#fbbc04"), // Yellow
63                Color::from_hex("#34a853"), // Green
64                Color::from_hex("#9334a8"), // Purple
65                Color::from_hex("#ff6d01"), // Orange
66            ],
67            axis_color: Color::from_hex("#333333"),
68            grid_color: Color::from_hex("#e0e0e0"),
69            text_color: Color::from_hex("#333333"),
70            font_family: "Arial, sans-serif".to_string(),
71            title_font_size: 18.0,
72            label_font_size: 14.0,
73            axis_font_size: 12.0,
74            line_width: 2.0,
75            point_radius: 4.0,
76            show_grid: true,
77            show_legend: true,
78            padding: Padding::default(),
79        }
80    }
81}
82
83impl ChartStyle {
84    /// Create a dark theme style.
85    #[must_use]
86    pub fn dark() -> Self {
87        Self {
88            background: Color::from_hex("#1e1e1e"),
89            primary: Color::from_hex("#61afef"),
90            palette: vec![
91                Color::from_hex("#61afef"),
92                Color::from_hex("#e06c75"),
93                Color::from_hex("#e5c07b"),
94                Color::from_hex("#98c379"),
95                Color::from_hex("#c678dd"),
96                Color::from_hex("#d19a66"),
97            ],
98            axis_color: Color::from_hex("#abb2bf"),
99            grid_color: Color::from_hex("#3e4451"),
100            text_color: Color::from_hex("#abb2bf"),
101            ..Default::default()
102        }
103    }
104
105    /// Create a minimal style.
106    #[must_use]
107    pub fn minimal() -> Self {
108        Self {
109            show_grid: false,
110            show_legend: false,
111            ..Default::default()
112        }
113    }
114
115    /// Get color for a series index.
116    #[must_use]
117    pub fn series_color(&self, index: usize) -> &Color {
118        if index == 0 {
119            &self.primary
120        } else {
121            &self.palette[(index - 1) % self.palette.len()]
122        }
123    }
124}
125
126/// RGB color.
127#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
128pub struct Color {
129    /// Red component (0-255).
130    pub r: u8,
131    /// Green component (0-255).
132    pub g: u8,
133    /// Blue component (0-255).
134    pub b: u8,
135    /// Alpha component (0-255).
136    pub a: u8,
137}
138
139impl Color {
140    /// White color.
141    pub const WHITE: Self = Self {
142        r: 255,
143        g: 255,
144        b: 255,
145        a: 255,
146    };
147
148    /// Black color.
149    pub const BLACK: Self = Self {
150        r: 0,
151        g: 0,
152        b: 0,
153        a: 255,
154    };
155
156    /// Create a new color.
157    #[must_use]
158    pub const fn new(r: u8, g: u8, b: u8) -> Self {
159        Self { r, g, b, a: 255 }
160    }
161
162    /// Create a new color with alpha.
163    #[must_use]
164    pub const fn with_alpha(r: u8, g: u8, b: u8, a: u8) -> Self {
165        Self { r, g, b, a }
166    }
167
168    /// Create from hex string (e.g., "#ff0000" or "ff0000").
169    #[must_use]
170    pub fn from_hex(hex: &str) -> Self {
171        let hex = hex.trim_start_matches('#');
172
173        if hex.len() != 6 && hex.len() != 8 {
174            return Self::BLACK;
175        }
176
177        let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
178        let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
179        let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
180        let a = if hex.len() == 8 {
181            u8::from_str_radix(&hex[6..8], 16).unwrap_or(255)
182        } else {
183            255
184        };
185
186        Self { r, g, b, a }
187    }
188
189    /// Convert to hex string.
190    #[must_use]
191    pub fn to_hex(&self) -> String {
192        if self.a == 255 {
193            format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
194        } else {
195            format!("#{:02x}{:02x}{:02x}{:02x}", self.r, self.g, self.b, self.a)
196        }
197    }
198
199    /// Convert to CSS rgba() string.
200    #[must_use]
201    pub fn to_rgba(&self) -> String {
202        if self.a == 255 {
203            format!("rgb({}, {}, {})", self.r, self.g, self.b)
204        } else {
205            format!(
206                "rgba({}, {}, {}, {:.2})",
207                self.r,
208                self.g,
209                self.b,
210                f64::from(self.a) / 255.0
211            )
212        }
213    }
214}
215
216/// Padding configuration.
217#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
218pub struct Padding {
219    /// Top padding.
220    pub top: f64,
221    /// Right padding.
222    pub right: f64,
223    /// Bottom padding.
224    pub bottom: f64,
225    /// Left padding.
226    pub left: f64,
227}
228
229impl Default for Padding {
230    fn default() -> Self {
231        Self {
232            top: 40.0,
233            right: 40.0,
234            bottom: 60.0,
235            left: 60.0,
236        }
237    }
238}
239
240impl Padding {
241    /// Create uniform padding.
242    #[must_use]
243    pub const fn uniform(value: f64) -> Self {
244        Self {
245            top: value,
246            right: value,
247            bottom: value,
248            left: value,
249        }
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn color_from_hex() {
259        let red = Color::from_hex("#ff0000");
260        assert_eq!(red.r, 255);
261        assert_eq!(red.g, 0);
262        assert_eq!(red.b, 0);
263
264        let blue = Color::from_hex("0000ff");
265        assert_eq!(blue.r, 0);
266        assert_eq!(blue.g, 0);
267        assert_eq!(blue.b, 255);
268    }
269
270    #[test]
271    fn color_to_hex() {
272        let color = Color::new(255, 128, 0);
273        assert_eq!(color.to_hex(), "#ff8000");
274
275        let with_alpha = Color::with_alpha(255, 128, 0, 128);
276        assert_eq!(with_alpha.to_hex(), "#ff800080");
277    }
278
279    #[test]
280    fn color_to_rgba() {
281        let color = Color::new(255, 128, 0);
282        assert_eq!(color.to_rgba(), "rgb(255, 128, 0)");
283
284        let with_alpha = Color::with_alpha(255, 128, 0, 128);
285        assert!(with_alpha.to_rgba().starts_with("rgba(255, 128, 0,"));
286    }
287
288    #[test]
289    fn style_series_color() {
290        let style = ChartStyle::default();
291
292        // First series uses primary
293        assert_eq!(style.series_color(0).to_hex(), style.primary.to_hex());
294
295        // Others use palette
296        assert_eq!(style.series_color(1).to_hex(), style.palette[0].to_hex());
297    }
298
299    #[test]
300    fn dark_theme() {
301        let dark = ChartStyle::dark();
302        assert_eq!(dark.background.to_hex(), "#1e1e1e");
303    }
304}