Skip to main content

fret_chart/retained/
style.rs

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    // ECharts default palette (concept reference): https://echarts.apache.org/en/option.html#color
11    [
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}