1use fret_core::{Color, DrawOrder, Edges, Px};
2use fret_ui::Theme;
3use fret_ui_kit::colors;
4
5fn color_from_srgb8(r: u8, g: u8, b: u8) -> Color {
6 colors::linear_from_hex_rgb(((r as u32) << 16) | ((g as u32) << 8) | (b as u32))
7}
8
9fn default_series_palette() -> [Color; 10] {
10 [
12 color_from_srgb8(0x54, 0x70, 0xC6),
13 color_from_srgb8(0x91, 0xCC, 0x75),
14 color_from_srgb8(0xEE, 0x66, 0x66),
15 color_from_srgb8(0x73, 0xC0, 0xDE),
16 color_from_srgb8(0x3B, 0xA2, 0x72),
17 color_from_srgb8(0xFC, 0x84, 0x52),
18 color_from_srgb8(0x9A, 0x60, 0xB4),
19 color_from_srgb8(0xEA, 0x7C, 0xCC),
20 color_from_srgb8(0xFA, 0xC8, 0x58),
21 color_from_srgb8(0x6E, 0x70, 0x74),
22 ]
23}
24
25#[derive(Debug, Clone, Copy)]
26pub struct ChartStyle {
27 pub background: Option<Color>,
28 pub stroke_color: Color,
29 pub stroke_width: Px,
30 pub area_fill_color: Color,
31 pub band_fill_color: Color,
32 pub bar_fill_alpha: f32,
33 pub scatter_point_radius: Px,
34 pub scatter_fill_alpha: f32,
35 pub selection_fill: Color,
36 pub selection_stroke: Color,
37 pub selection_stroke_width: Px,
38
39 pub padding: Edges,
40 pub axis_band_x: Px,
41 pub axis_band_y: Px,
42 pub visual_map_band_x: Px,
43 pub visual_map_padding: Px,
44 pub visual_map_item_gap: Px,
45 pub visual_map_corner_radius: Px,
46 pub visual_map_track_color: Color,
47 pub visual_map_range_fill: Color,
48 pub visual_map_range_stroke: Color,
49 pub visual_map_handle_color: Color,
50 pub axis_line_color: Color,
51 pub axis_tick_color: Color,
52 pub axis_label_color: Color,
53 pub axis_line_width: Px,
54 pub axis_tick_length: Px,
55
56 pub crosshair_color: Color,
57 pub crosshair_width: Px,
58 pub hover_point_color: Color,
59 pub hover_point_size: Px,
60
61 pub tooltip_background: Color,
62 pub tooltip_border_color: Color,
63 pub tooltip_border_width: Px,
64 pub tooltip_text_color: Color,
65 pub tooltip_padding: Edges,
66 pub tooltip_corner_radius: Px,
67 pub tooltip_marker_size: Px,
68 pub tooltip_marker_gap: Px,
69 pub tooltip_column_gap: Px,
70
71 pub legend_background: Color,
72 pub legend_border_color: Color,
73 pub legend_border_width: Px,
74 pub legend_text_color: Color,
75 pub legend_padding: Edges,
76 pub legend_corner_radius: Px,
77 pub legend_item_gap: Px,
78 pub legend_swatch_size: Px,
79 pub legend_swatch_gap: Px,
80 pub legend_hover_background: Color,
81
82 pub series_palette: [Color; 10],
83 pub draw_order: DrawOrder,
84}
85
86impl Default for ChartStyle {
87 fn default() -> Self {
88 let series_palette = default_series_palette();
89
90 Self {
91 background: Some(Color {
92 r: 0.06,
93 g: 0.06,
94 b: 0.07,
95 a: 1.0,
96 }),
97 stroke_color: Color {
98 r: 1.0,
99 g: 1.0,
100 b: 1.0,
101 a: 0.9,
102 },
103 stroke_width: Px(1.0),
104 area_fill_color: Color {
105 r: 0.2,
106 g: 0.6,
107 b: 1.0,
108 a: 0.18,
109 },
110 band_fill_color: Color {
111 r: 0.2,
112 g: 0.6,
113 b: 1.0,
114 a: 0.12,
115 },
116 bar_fill_alpha: 0.7,
117 scatter_point_radius: Px(5.0),
118 scatter_fill_alpha: 0.9,
119 selection_fill: Color {
120 r: 0.2,
121 g: 0.6,
122 b: 1.0,
123 a: 0.12,
124 },
125 selection_stroke: Color {
126 r: 0.2,
127 g: 0.6,
128 b: 1.0,
129 a: 0.75,
130 },
131 selection_stroke_width: Px(1.0),
132 padding: Edges::all(Px(8.0)),
133 axis_band_x: Px(56.0),
134 axis_band_y: Px(36.0),
135 visual_map_band_x: Px(22.0),
136 visual_map_padding: Px(6.0),
137 visual_map_item_gap: Px(8.0),
138 visual_map_corner_radius: Px(4.0),
139 visual_map_track_color: Color {
140 r: 1.0,
141 g: 1.0,
142 b: 1.0,
143 a: 0.18,
144 },
145 visual_map_range_fill: Color {
146 r: 0.2,
147 g: 0.6,
148 b: 1.0,
149 a: 0.12,
150 },
151 visual_map_range_stroke: Color {
152 r: 0.2,
153 g: 0.6,
154 b: 1.0,
155 a: 0.75,
156 },
157 visual_map_handle_color: Color {
158 r: 0.2,
159 g: 0.6,
160 b: 1.0,
161 a: 0.75,
162 },
163 axis_line_color: Color {
164 r: 1.0,
165 g: 1.0,
166 b: 1.0,
167 a: 0.7,
168 },
169 axis_tick_color: Color {
170 r: 1.0,
171 g: 1.0,
172 b: 1.0,
173 a: 0.55,
174 },
175 axis_label_color: Color {
176 r: 1.0,
177 g: 1.0,
178 b: 1.0,
179 a: 0.8,
180 },
181 axis_line_width: Px(1.0),
182 axis_tick_length: Px(6.0),
183 crosshair_color: Color {
184 r: 1.0,
185 g: 1.0,
186 b: 1.0,
187 a: 0.25,
188 },
189 crosshair_width: Px(1.0),
190 hover_point_color: Color {
191 r: 0.9,
192 g: 0.9,
193 b: 0.9,
194 a: 0.9,
195 },
196 hover_point_size: Px(4.0),
197 tooltip_background: Color {
198 r: 0.08,
199 g: 0.08,
200 b: 0.1,
201 a: 0.9,
202 },
203 tooltip_border_color: Color {
204 r: 1.0,
205 g: 1.0,
206 b: 1.0,
207 a: 0.15,
208 },
209 tooltip_border_width: Px(1.0),
210 tooltip_text_color: Color {
211 r: 1.0,
212 g: 1.0,
213 b: 1.0,
214 a: 0.9,
215 },
216 tooltip_padding: Edges::symmetric(Px(8.0), Px(6.0)),
217 tooltip_corner_radius: Px(6.0),
218 tooltip_marker_size: Px(8.0),
219 tooltip_marker_gap: Px(6.0),
220 tooltip_column_gap: Px(10.0),
221 legend_background: Color {
222 r: 0.08,
223 g: 0.08,
224 b: 0.1,
225 a: 0.9,
226 },
227 legend_border_color: Color {
228 r: 1.0,
229 g: 1.0,
230 b: 1.0,
231 a: 0.15,
232 },
233 legend_border_width: Px(1.0),
234 legend_text_color: Color {
235 r: 1.0,
236 g: 1.0,
237 b: 1.0,
238 a: 0.9,
239 },
240 legend_padding: Edges::symmetric(Px(10.0), Px(8.0)),
241 legend_corner_radius: Px(8.0),
242 legend_item_gap: Px(4.0),
243 legend_swatch_size: Px(10.0),
244 legend_swatch_gap: Px(8.0),
245 legend_hover_background: Color {
246 r: 1.0,
247 g: 1.0,
248 b: 1.0,
249 a: 0.06,
250 },
251 series_palette,
252 draw_order: DrawOrder(100),
253 }
254 }
255}
256
257impl ChartStyle {
258 pub fn from_theme(theme: &Theme) -> Self {
259 fn color(theme: &Theme, key: &str) -> Option<Color> {
260 theme.color_by_key(key)
261 }
262
263 fn metric(theme: &Theme, key: &str) -> Option<Px> {
264 theme.metric_by_key(key)
265 }
266
267 fn with_alpha(mut c: Color, a: f32) -> Color {
268 c.a *= a.clamp(0.0, 1.0);
269 c
270 }
271
272 fn pick_color(theme: &Theme, key: &str, fallback: Color) -> Color {
273 color(theme, key).unwrap_or(fallback)
274 }
275
276 fn pick_metric(theme: &Theme, key: &str, fallback: Px) -> Px {
277 metric(theme, key).unwrap_or(fallback)
278 }
279
280 let foreground = theme.color_token("foreground");
281 let muted_foreground = theme.color_token("muted-foreground");
282 let border = theme.color_token("border");
283 let primary = theme.color_token("primary");
284 let popover = theme.color_token("popover");
285
286 let background = pick_color(theme, "chart.background", theme.color_token("card"));
287 let tooltip_background =
288 pick_color(theme, "chart.tooltip.background", with_alpha(popover, 0.9));
289 let tooltip_border = pick_color(theme, "chart.tooltip.border", with_alpha(border, 0.15));
290 let tooltip_text = pick_color(theme, "chart.tooltip.text", with_alpha(foreground, 0.9));
291
292 let legend_background =
293 pick_color(theme, "chart.legend.background", with_alpha(popover, 0.9));
294 let legend_border = pick_color(theme, "chart.legend.border", with_alpha(border, 0.15));
295 let legend_text = pick_color(theme, "chart.legend.text", with_alpha(foreground, 0.9));
296
297 let axis_line_color =
298 pick_color(theme, "chart.axis.line", with_alpha(muted_foreground, 0.7));
299 let axis_tick_color =
300 pick_color(theme, "chart.axis.tick", with_alpha(muted_foreground, 0.55));
301 let axis_label_color = pick_color(theme, "chart.axis.label", with_alpha(foreground, 0.8));
302
303 let crosshair_color = pick_color(theme, "chart.crosshair", with_alpha(foreground, 0.25));
304
305 let selection_fill = pick_color(theme, "chart.selection.fill", with_alpha(primary, 0.12));
306 let selection_stroke =
307 pick_color(theme, "chart.selection.stroke", with_alpha(primary, 0.75));
308
309 let stroke_width = pick_metric(theme, "metric.chart.stroke.width", Px(1.0));
310 let axis_line_width = pick_metric(theme, "metric.chart.axis.line.width", Px(1.0));
311 let axis_tick_length = pick_metric(theme, "metric.chart.axis.tick.length", Px(6.0));
312 let scatter_point_radius = pick_metric(theme, "metric.chart.scatter.point_radius", Px(5.0));
313 let hover_point_size = pick_metric(theme, "metric.chart.hover.point_size", Px(4.0));
314 let tooltip_border_width = pick_metric(theme, "metric.chart.tooltip.border.width", Px(1.0));
315 let legend_border_width = pick_metric(theme, "metric.chart.legend.border.width", Px(1.0));
316 let selection_stroke_width =
317 pick_metric(theme, "metric.chart.selection.stroke.width", Px(1.0));
318
319 let padding_all = metric(theme, "metric.chart.padding")
320 .unwrap_or_else(|| theme.metric_token("metric.padding.sm"));
321 let padding = Edges::all(padding_all);
322
323 let axis_band_x = pick_metric(theme, "metric.chart.axis.band.x", Px(56.0));
324 let axis_band_y = pick_metric(theme, "metric.chart.axis.band.y", Px(36.0));
325 let visual_map_band_x = pick_metric(theme, "metric.chart.visualmap.band.x", Px(22.0));
326 let visual_map_padding = pick_metric(theme, "metric.chart.visualmap.pad", Px(6.0));
327 let visual_map_item_gap = pick_metric(theme, "metric.chart.visualmap.item.gap", Px(8.0));
328 let visual_map_corner_radius =
329 pick_metric(theme, "metric.chart.visualmap.corner_radius", Px(4.0));
330
331 let tooltip_padding_x = pick_metric(theme, "metric.chart.tooltip.padding.x", Px(8.0));
332 let tooltip_padding_y = pick_metric(theme, "metric.chart.tooltip.padding.y", Px(6.0));
333 let tooltip_corner_radius = pick_metric(
334 theme,
335 "metric.chart.tooltip.corner_radius",
336 theme.metric_token("metric.radius.sm"),
337 );
338 let tooltip_marker_size = pick_metric(theme, "metric.chart.tooltip.marker.size", Px(8.0));
339 let tooltip_marker_gap = pick_metric(theme, "metric.chart.tooltip.marker.gap", Px(6.0));
340 let tooltip_column_gap = pick_metric(theme, "metric.chart.tooltip.column.gap", Px(10.0));
341
342 let legend_padding_x = pick_metric(theme, "metric.chart.legend.padding.x", Px(10.0));
343 let legend_padding_y = pick_metric(theme, "metric.chart.legend.padding.y", Px(8.0));
344 let legend_corner_radius = pick_metric(
345 theme,
346 "metric.chart.legend.corner_radius",
347 theme.metric_token("metric.radius.md"),
348 );
349 let legend_item_gap = pick_metric(theme, "metric.chart.legend.item.gap", Px(4.0));
350 let legend_swatch_size = pick_metric(theme, "metric.chart.legend.swatch.size", Px(10.0));
351 let legend_swatch_gap = pick_metric(theme, "metric.chart.legend.swatch.gap", Px(8.0));
352
353 let legend_hover_background = pick_color(
354 theme,
355 "chart.legend.hover.background",
356 with_alpha(foreground, 0.06),
357 );
358
359 let visual_map_track_color = pick_color(
360 theme,
361 "chart.visualmap.track",
362 with_alpha(axis_line_color, 0.25),
363 );
364 let visual_map_range_fill = pick_color(theme, "chart.visualmap.range.fill", selection_fill);
365 let visual_map_range_stroke =
366 pick_color(theme, "chart.visualmap.range.stroke", selection_stroke);
367 let visual_map_handle_color =
368 pick_color(theme, "chart.visualmap.handle", visual_map_range_stroke);
369
370 const PALETTE_KEYS: [&str; 10] = [
371 "chart.palette.0",
372 "chart.palette.1",
373 "chart.palette.2",
374 "chart.palette.3",
375 "chart.palette.4",
376 "chart.palette.5",
377 "chart.palette.6",
378 "chart.palette.7",
379 "chart.palette.8",
380 "chart.palette.9",
381 ];
382 const SHADCN_CHART_KEYS: [&str; 5] =
383 ["chart-1", "chart-2", "chart-3", "chart-4", "chart-5"];
384
385 let fallback_palette = default_series_palette();
386 let mut series_palette = fallback_palette;
387 for (index, key) in PALETTE_KEYS.iter().enumerate() {
388 if let Some(c) = color(theme, key) {
389 series_palette[index] = c;
390 continue;
391 }
392 if index < SHADCN_CHART_KEYS.len()
393 && let Some(c) = color(theme, SHADCN_CHART_KEYS[index])
394 {
395 series_palette[index] = c;
396 }
397 }
398
399 Self {
400 background: Some(background),
401 stroke_color: with_alpha(foreground, 0.9),
402 stroke_width,
403 area_fill_color: with_alpha(primary, 0.18),
404 band_fill_color: with_alpha(primary, 0.12),
405 bar_fill_alpha: 0.7,
406 scatter_point_radius,
407 scatter_fill_alpha: 0.9,
408 selection_fill,
409 selection_stroke,
410 selection_stroke_width,
411 padding,
412 axis_band_x,
413 axis_band_y,
414 visual_map_band_x,
415 visual_map_padding,
416 visual_map_item_gap,
417 visual_map_corner_radius,
418 visual_map_track_color,
419 visual_map_range_fill,
420 visual_map_range_stroke,
421 visual_map_handle_color,
422 axis_line_color,
423 axis_tick_color,
424 axis_label_color,
425 axis_line_width,
426 axis_tick_length,
427 crosshair_color,
428 crosshair_width: Px(1.0),
429 hover_point_color: with_alpha(foreground, 0.9),
430 hover_point_size,
431 tooltip_background,
432 tooltip_border_color: tooltip_border,
433 tooltip_border_width,
434 tooltip_text_color: tooltip_text,
435 tooltip_padding: Edges::symmetric(tooltip_padding_x, tooltip_padding_y),
436 tooltip_corner_radius,
437 tooltip_marker_size,
438 tooltip_marker_gap,
439 tooltip_column_gap,
440 legend_background,
441 legend_border_color: legend_border,
442 legend_border_width,
443 legend_text_color: legend_text,
444 legend_padding: Edges::symmetric(legend_padding_x, legend_padding_y),
445 legend_corner_radius,
446 legend_item_gap,
447 legend_swatch_size,
448 legend_swatch_gap,
449 legend_hover_background,
450 series_palette,
451 draw_order: DrawOrder(100),
452 }
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use fret_app::App;
459 use fret_ui::{Theme, ThemeConfig};
460
461 use super::ChartStyle;
462
463 #[test]
464 fn series_palette_prefers_chart_palette_tokens_over_shadcn_aliases() {
465 let mut app = App::new();
466 let mut cfg = ThemeConfig::default();
467 cfg.colors
468 .insert("chart.palette.0".to_string(), "#FF0000".to_string());
469 cfg.colors
470 .insert("chart-1".to_string(), "#00FF00".to_string());
471 Theme::with_global_mut(&mut app, |theme| theme.apply_config(&cfg));
472
473 let theme = Theme::global(&app);
474 let style = ChartStyle::from_theme(theme);
475 assert_eq!(
476 style.series_palette[0],
477 theme.color_token("chart.palette.0")
478 );
479 }
480}