1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ChartStyle {
8 pub background: Color,
10
11 pub primary: Color,
13
14 pub palette: Vec<Color>,
16
17 pub axis_color: Color,
19
20 pub grid_color: Color,
22
23 pub text_color: Color,
25
26 pub font_family: String,
28
29 pub title_font_size: f64,
31
32 pub label_font_size: f64,
34
35 pub axis_font_size: f64,
37
38 pub line_width: f64,
40
41 pub point_radius: f64,
43
44 pub show_grid: bool,
46
47 pub show_legend: bool,
49
50 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"), Color::from_hex("#ea4335"), Color::from_hex("#fbbc04"), Color::from_hex("#34a853"), Color::from_hex("#9334a8"), Color::from_hex("#ff6d01"), ],
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 #[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 #[must_use]
107 pub fn minimal() -> Self {
108 Self {
109 show_grid: false,
110 show_legend: false,
111 ..Default::default()
112 }
113 }
114
115 #[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#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
128pub struct Color {
129 pub r: u8,
131 pub g: u8,
133 pub b: u8,
135 pub a: u8,
137}
138
139impl Color {
140 pub const WHITE: Self = Self {
142 r: 255,
143 g: 255,
144 b: 255,
145 a: 255,
146 };
147
148 pub const BLACK: Self = Self {
150 r: 0,
151 g: 0,
152 b: 0,
153 a: 255,
154 };
155
156 #[must_use]
158 pub const fn new(r: u8, g: u8, b: u8) -> Self {
159 Self { r, g, b, a: 255 }
160 }
161
162 #[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 #[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 #[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 #[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#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
218pub struct Padding {
219 pub top: f64,
221 pub right: f64,
223 pub bottom: f64,
225 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 #[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 assert_eq!(style.series_color(0).to_hex(), style.primary.to_hex());
294
295 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}