Skip to main content

egui_charts/model/
chart_style.rs

1//! Independent styles retained when switching chart types.
2
3use crate::model::LineStyle;
4use crate::tokens::DESIGN_TOKENS;
5use egui::Color32;
6use serde::{Deserialize, Serialize};
7
8/// How a series line connects its points.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
10pub enum LineType {
11    /// Straight segments between points.
12    #[default]
13    Simple,
14    /// A smooth curve through every point, a Catmull-Rom spline drawn as cubic Bezier segments.
15    Curved,
16}
17
18/// When the Line, Step line, Line with markers, Area and Baseline charts pulse a circle at the
19/// last price.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
21pub enum LastPriceAnimation {
22    /// No animation.
23    #[default]
24    Disabled,
25    /// Pulse continuously.
26    Continuous,
27    /// Pulse once each time the last bar's time or price changes.
28    OnDataUpdate,
29}
30
31/// A series line, with width in logical points.
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33#[serde(default)]
34pub struct LinePlotStyle {
35    /// Line colour.
36    pub color: Color32,
37    /// Width in logical points. Non-positive or non-finite widths hide the line.
38    pub width: f32,
39    /// Solid, dashed or dotted.
40    pub style: LineStyle,
41    /// Straight or curved. The Step line chart ignores it; the Baseline chart follows its
42    /// `above` line on both sides.
43    pub line_type: LineType,
44}
45
46impl Default for LinePlotStyle {
47    fn default() -> Self {
48        Self {
49            color: DESIGN_TOKENS.semantic.brand.accent,
50            width: DESIGN_TOKENS.stroke.thick,
51            style: LineStyle::Solid,
52            line_type: LineType::Simple,
53        }
54    }
55}
56
57/// The Area chart's line and vertical fill stops.
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59#[serde(default)]
60pub struct AreaStyle {
61    /// Line drawn along the edge of the fill.
62    pub line: LinePlotStyle,
63    /// Point marker radius in logical points. Non-positive or non-finite radii hide markers,
64    /// the default. Each marker takes its point's line colour.
65    pub marker_radius: f32,
66    /// Fill at the top of the gradient, including opacity.
67    pub top_fill: Color32,
68    /// Fill at the bottom of the gradient, including opacity.
69    pub bottom_fill: Color32,
70    /// Spread the gradient between the line's visible extreme and the pane edge the fill
71    /// reaches, the default, instead of over the whole price pane.
72    pub relative_gradient: bool,
73    /// Fill from the line up to the pane top instead of down to the pane bottom.
74    pub invert_filled_area: bool,
75}
76
77impl Default for AreaStyle {
78    fn default() -> Self {
79        let color = DESIGN_TOKENS.semantic.brand.accent;
80        Self {
81            line: LinePlotStyle::default(),
82            marker_radius: 0.0,
83            top_fill: Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), 115),
84            bottom_fill: Color32::TRANSPARENT,
85            relative_gradient: true,
86            invert_filled_area: false,
87        }
88    }
89}
90
91/// The Baseline chart's independent colours and vertical fills on either side of its base.
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
93#[serde(default)]
94pub struct BaselineStyle {
95    /// Line above the base in price units, also on an inverted scale.
96    pub above: LinePlotStyle,
97    /// Line below the base in price units.
98    pub below: LinePlotStyle,
99    /// Point marker radius in logical points. Non-positive or non-finite radii hide markers,
100    /// the default. Each marker takes its point's line colour on its side of the base.
101    pub marker_radius: f32,
102    /// Fill at the pane's high-price edge, including opacity, also on an inverted scale.
103    pub above_fill: Color32,
104    /// Fill at the pane's low-price edge, including opacity.
105    pub below_fill: Color32,
106    /// Fill at the base on its higher-price side. `None` uses `above_fill` for a flat fill.
107    /// The gradient interpolates in screen space between the pane edge and the base.
108    pub above_fill_at_base: Option<Color32>,
109    /// Fill at the base on its lower-price side. `None` uses `below_fill` for a flat fill.
110    pub below_fill_at_base: Option<Color32>,
111}
112
113impl Default for BaselineStyle {
114    fn default() -> Self {
115        let up = DESIGN_TOKENS.semantic.extended.bullish;
116        let down = DESIGN_TOKENS.semantic.extended.bearish;
117        Self {
118            above: LinePlotStyle {
119                color: up,
120                ..Default::default()
121            },
122            below: LinePlotStyle {
123                color: down,
124                ..Default::default()
125            },
126            marker_radius: 0.0,
127            above_fill: Color32::from_rgba_unmultiplied(up.r(), up.g(), up.b(), 30),
128            below_fill: Color32::from_rgba_unmultiplied(down.r(), down.g(), down.b(), 30),
129            above_fill_at_base: None,
130            below_fill_at_base: None,
131        }
132    }
133}
134
135/// The HLC area chart's fill and close line. The fills take the chart's bullish and bearish
136/// colours and the close line its bullish colour.
137#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
138#[serde(default)]
139pub struct HlcAreaStyle {
140    /// Opacity of the high-to-close and close-to-low fills, 0 to 1. Values outside the range
141    /// are clamped; a non-finite opacity draws no fill.
142    pub fill_opacity: f32,
143    /// Close line width in logical points. Non-positive or non-finite widths hide the line.
144    pub line_width: f32,
145}
146
147impl Default for HlcAreaStyle {
148    fn default() -> Self {
149        Self {
150            fill_opacity: 30.0 / 255.0,
151            line_width: DESIGN_TOKENS.stroke.thick,
152        }
153    }
154}
155
156/// The Bars chart's OHLC ticks.
157#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158#[serde(default)]
159pub struct BarsStyle {
160    /// Draw the open tick on the left of each bar.
161    pub open_visible: bool,
162    /// Draw bars with the chart's wick width at every zoom level instead of a stroke that
163    /// widens with the bar spacing.
164    pub thin_bars: bool,
165}
166
167impl Default for BarsStyle {
168    fn default() -> Self {
169        Self {
170            open_visible: true,
171            thin_bars: true,
172        }
173    }
174}
175
176/// The Candles chart's bodies, wicks and colouring. Borders are drawn while the chart's candle
177/// border width is above zero.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
179#[serde(default)]
180pub struct CandlesStyle {
181    /// Fill each candle's body between its open and close.
182    pub body_visible: bool,
183    /// Draw each candle's wick from its high to its low.
184    pub wick_visible: bool,
185    /// Colour each candle by its close against the previous close instead of its open: rising
186    /// when the close is at or above the last close before it.
187    pub color_by_previous_close: bool,
188}
189
190impl Default for CandlesStyle {
191    fn default() -> Self {
192        Self {
193            body_visible: true,
194            wick_visible: true,
195            color_by_previous_close: false,
196        }
197    }
198}
199
200/// The Kagi chart's stroke widths in logical points. Non-positive or non-finite widths hide
201/// the lines of that thickness.
202#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
203#[serde(default)]
204pub struct KagiStyle {
205    /// Width of the thick lines of an uptrend.
206    pub thick_width: f32,
207    /// Width of the thin lines of a downtrend.
208    pub thin_width: f32,
209}
210
211impl Default for KagiStyle {
212    fn default() -> Self {
213        Self {
214            thick_width: 2.5,
215            thin_width: 1.5,
216        }
217    }
218}
219
220/// Styles of the candles, bars, line, step, marker, area, HLC area, baseline and Kagi chart types.
221/// Defaults use the design-token palette and remain independent of candle colours.
222#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
223#[serde(default)]
224pub struct ChartTypeStyles {
225    /// Line chart.
226    pub line: LinePlotStyle,
227    /// Step line chart.
228    pub step_line: LinePlotStyle,
229    /// Line with markers chart.
230    pub line_with_markers: LinePlotStyle,
231    /// Marker radius in logical points. Non-positive or non-finite radii hide markers.
232    pub marker_radius: f32,
233    /// Area chart.
234    pub area: AreaStyle,
235    /// Baseline chart.
236    pub baseline: BaselineStyle,
237    /// HLC area chart.
238    pub hlc_area: HlcAreaStyle,
239    /// Candles chart.
240    pub candles: CandlesStyle,
241    /// Bars chart.
242    pub bars: BarsStyle,
243    /// Kagi chart.
244    pub kagi: KagiStyle,
245    /// Radius in logical points of the marker drawn on the line at the crosshair's bar in the
246    /// Line, Step line, Line with markers, Area and Baseline charts. Non-positive or non-finite
247    /// radii hide it, the default.
248    pub crosshair_marker_radius: f32,
249    /// Pulse at the last price of the line-based charts.
250    pub last_price_animation: LastPriceAnimation,
251}
252
253impl Default for ChartTypeStyles {
254    fn default() -> Self {
255        Self {
256            line: LinePlotStyle::default(),
257            step_line: LinePlotStyle::default(),
258            line_with_markers: LinePlotStyle::default(),
259            marker_radius: 3.0,
260            area: AreaStyle::default(),
261            baseline: BaselineStyle::default(),
262            hlc_area: HlcAreaStyle::default(),
263            candles: CandlesStyle::default(),
264            bars: BarsStyle::default(),
265            kagi: KagiStyle::default(),
266            crosshair_marker_radius: 0.0,
267            last_price_animation: LastPriceAnimation::Disabled,
268        }
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use crate::model::BarColors;
276
277    #[test]
278    fn baseline_saved_colours_keep_legacy_flat_fills_and_gradient_endpoints() {
279        for gradient in [false, true] {
280            let mut saved = serde_json::json!({
281                "above_fill": [0, 200, 0, 200], "below_fill": [200, 0, 0, 200]
282            });
283            let mut point = serde_json::json!({
284                "top_fill": [0, 0, 160, 160], "bottom_fill": [160, 160, 0, 160]
285            });
286            if gradient {
287                saved["above_fill_at_base"] = serde_json::json!([0, 40, 0, 40]);
288                saved["below_fill_at_base"] = serde_json::json!([40, 0, 0, 40]);
289                point["top_fill_at_base"] = serde_json::json!([0, 0, 0, 0]);
290                point["bottom_fill_at_base"] = serde_json::json!([0, 0, 0, 0]);
291            }
292            let style: BaselineStyle = serde_json::from_value(saved.clone()).unwrap();
293            let own: BarColors = serde_json::from_value(point.clone()).unwrap();
294            let restored = serde_json::to_value(&style).unwrap();
295            let restored_point = serde_json::to_value(&own).unwrap();
296            for (original, restored) in [(&saved, &restored), (&point, &restored_point)] {
297                for (key, color) in original.as_object().unwrap() {
298                    assert_eq!(&restored[key], color, "{key}");
299                }
300            }
301            for key in ["above_fill_at_base", "below_fill_at_base"] {
302                assert_eq!(restored[key], saved[key]);
303            }
304            for key in ["top_fill_at_base", "bottom_fill_at_base"] {
305                assert_eq!(restored_point[key], point[key]);
306            }
307        }
308    }
309}