Skip to main content

egui_charts/config/
chart.rs

1//! Chart visual configuration.
2//!
3//! [`ChartConfig`] controls everything about the chart's appearance: candle
4//! colors, grid lines, axis visibility, volume panel, session breaks,
5//! watermark, and the "Go to Realtime" button.
6//!
7//! [`PriceAxisOptions`] sets the price axis border, tick marks and label clipping.
8//!
9//! [`SessionConfig`] describes trading session hours for a specific exchange,
10//! with presets for NYSE, LSE, TSE, crypto 24/7, and forex.
11
12use crate::model::{LineStyle, PriceSource};
13use crate::scales::PriceScaleMode;
14use crate::theme::Theme;
15use crate::tokens::DESIGN_TOKENS;
16use egui::Color32;
17
18/// Background style for the chart.
19#[derive(Debug, Clone, Copy, PartialEq, Default, serde::Serialize, serde::Deserialize)]
20pub enum BackgroundStyle {
21    /// Solid single-color background
22    #[default]
23    Solid,
24    /// Vertical gradient from top to bottom
25    VerticalGradient {
26        /// Color at the top edge of the gradient.
27        top_color: Color32,
28        /// Color at the bottom edge of the gradient.
29        bottom_color: Color32,
30    },
31    /// Horizontal gradient from left to right
32    HorizontalGradient {
33        /// Color at the left edge of the gradient.
34        left_color: Color32,
35        /// Color at the right edge of the gradient.
36        right_color: Color32,
37    },
38}
39
40/// When the chart shows a group of buttons: its navigation buttons in the main pane, or the
41/// maximize and menu buttons of its panes.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
43pub enum NavigationButtons {
44    /// Always show the buttons.
45    Always,
46    /// Show the buttons while the pointer is over the main pane, price and volume.
47    OnHover,
48    /// Hide the buttons (the default).
49    #[default]
50    Never,
51}
52
53/// Older saved configurations show the pane buttons while the pointer is over the pane.
54fn pane_buttons_on_hover() -> NavigationButtons {
55    NavigationButtons::OnHover
56}
57
58/// Older saved configurations show the market status, as they showed delayed and end-of-day
59/// data marks.
60fn market_status_shown() -> bool {
61    true
62}
63
64/// Layouts saved before `thousands_sep` existed group thousands like a new chart.
65fn group_thousands_with_comma() -> Option<char> {
66    Some(',')
67}
68
69/// Older saved configurations keep the one-physical-pixel grouping threshold.
70fn pixel_conflation_threshold() -> Option<f32> {
71    Some(1.0)
72}
73
74/// Appearance of the price axis: its border, tick marks and which labels it shows. It applies to
75/// the main and series price axes; value tag settings also apply to study panes.
76///
77/// Tick spacing is a property of each scale, set through
78/// [`PriceScaleOptions::tick_marks`](crate::scales::PriceScaleOptions::tick_marks).
79#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
80#[serde(default)]
81pub struct PriceAxisOptions {
82    /// Allow value tags to overlap instead of moving them to free vertical slots.
83    pub allow_label_overlap: bool,
84    /// Display study values on price axes when study values are enabled.
85    pub show_study_values: bool,
86    /// Study value tag colour; `None` follows each plot's colour.
87    pub study_value_color: Option<Color32>,
88    /// Write the symbol before its last value tag on its price axis, and before the value tags
89    /// of added series on theirs. Independent of the legend title.
90    /// Default: false
91    pub show_symbol_name: bool,
92    /// Write each study's name before its value tags. Independent of the legend title.
93    /// Default: false
94    pub show_study_names: bool,
95    /// Draw a line between the plot and the axis.
96    /// Default: false
97    pub border_visible: bool,
98    /// Colour of the border; `None` uses the theme's border colour.
99    /// Default: None
100    pub border_color: Option<Color32>,
101    /// Draw a short horizontal tick beside each label.
102    /// Default: false
103    pub ticks_visible: bool,
104    /// Label the prices at the top and bottom of the pane, in place of the tick marks next to them.
105    /// Default: false
106    pub edge_ticks_visible: bool,
107    /// Leave out labels that the top or bottom of the pane would cut; with `false` they show clipped.
108    /// Default: true
109    pub hide_clipped_labels: bool,
110}
111
112impl Default for PriceAxisOptions {
113    fn default() -> Self {
114        Self {
115            allow_label_overlap: false,
116            show_study_values: true,
117            study_value_color: None,
118            show_symbol_name: false,
119            show_study_names: false,
120            border_visible: false,
121            border_color: None,
122            ticks_visible: false,
123            edge_ticks_visible: false,
124            hide_clipped_labels: true,
125        }
126    }
127}
128
129/// Trading session configuration.
130///
131/// Presets use exchange time zones with `timezones`, or fixed standard-time offsets without it.
132#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
133pub struct SessionConfig {
134    /// Session start time in HH:MM format (24-hour)
135    pub session_start: String,
136    /// Session end time in HH:MM format (24-hour)
137    pub session_end: String,
138    /// Timezone for the session (e.g., "America/New_York")
139    pub timezone: String,
140    /// Whether to show pre-market session
141    pub show_premarket: bool,
142    /// Whether to show post-market session
143    pub show_postmarket: bool,
144    /// Pre-market start time (e.g., "04:00")
145    pub premarket_start: Option<String>,
146    /// Post-market end time (e.g., "20:00")
147    pub postmarket_end: Option<String>,
148}
149
150impl Default for SessionConfig {
151    fn default() -> Self {
152        // Default to NYSE trading hours
153        Self {
154            session_start: "09:30".to_string(),
155            session_end: "16:00".to_string(),
156            timezone: if cfg!(feature = "timezones") {
157                "America/New_York"
158            } else {
159                "-05:00"
160            }
161            .to_string(),
162            show_premarket: false,
163            show_postmarket: false,
164            premarket_start: Some("04:00".to_string()),
165            postmarket_end: Some("20:00".to_string()),
166        }
167    }
168}
169
170impl SessionConfig {
171    /// Create NYSE session configuration
172    pub fn nyse() -> Self {
173        Self::default()
174    }
175
176    /// Create London Stock Exchange session configuration
177    pub fn lse() -> Self {
178        Self {
179            session_start: "08:00".to_string(),
180            session_end: "16:30".to_string(),
181            timezone: if cfg!(feature = "timezones") {
182                "Europe/London"
183            } else {
184                "+00:00"
185            }
186            .to_string(),
187            show_premarket: false,
188            show_postmarket: false,
189            premarket_start: None,
190            postmarket_end: None,
191        }
192    }
193
194    /// Create Tokyo Stock Exchange session configuration
195    pub fn tse() -> Self {
196        Self {
197            session_start: "09:00".to_string(),
198            session_end: "15:00".to_string(),
199            timezone: if cfg!(feature = "timezones") {
200                "Asia/Tokyo"
201            } else {
202                "+09:00"
203            }
204            .to_string(),
205            show_premarket: false,
206            show_postmarket: false,
207            premarket_start: None,
208            postmarket_end: None,
209        }
210    }
211
212    /// Create 24/7 crypto session (no breaks)
213    pub fn crypto_24_7() -> Self {
214        Self {
215            session_start: "00:00".to_string(),
216            session_end: "23:59".to_string(),
217            timezone: "UTC".to_string(),
218            show_premarket: false,
219            show_postmarket: false,
220            premarket_start: None,
221            postmarket_end: None,
222        }
223    }
224
225    /// Create forex session configuration (Sydney -> Tokyo -> London -> New York)
226    pub fn forex() -> Self {
227        Self {
228            session_start: "17:00".to_string(), // Sunday 5PM EST
229            session_end: "17:00".to_string(),   // Friday 5PM EST
230            timezone: if cfg!(feature = "timezones") {
231                "America/New_York"
232            } else {
233                "-05:00"
234            }
235            .to_string(),
236            show_premarket: false,
237            show_postmarket: false,
238            premarket_start: None,
239            postmarket_end: None,
240        }
241    }
242}
243
244/// Configuration for chart visual appearance.
245///
246/// Controls every aspect of chart styling: candle/bar colors, grid lines,
247/// axis visibility, volume panel sizing, session break rendering, watermark
248/// overlay, and the real-time navigation button.
249///
250/// Defaults follow a common dark-theme charting palette.
251#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
252#[non_exhaustive]
253pub struct ChartConfig {
254    /// Color for bullish (up) bars/candles body
255    pub bullish_color: Color32,
256    /// Color for bearish (down) bars/candles body
257    pub bearish_color: Color32,
258    /// Border color for bullish candles (None = same as body)
259    pub bullish_border_color: Option<Color32>,
260    /// Border color for bearish candles (None = same as body)
261    pub bearish_border_color: Option<Color32>,
262    /// Wick color for bullish candles (None = same as border or body)
263    pub bullish_wick_color: Option<Color32>,
264    /// Wick color for bearish candles (None = same as border or body)
265    pub bearish_wick_color: Option<Color32>,
266    /// Price source for line-based charts (Line, Area, StepLine, etc.)
267    pub price_source: PriceSource,
268    /// Minimum spacing in physical pixels for unmerged main-series samples; `None` disables
269    /// conflation. The default, `Some(1.0)`, preserves the one-pixel grouping. Below the
270    /// threshold, consecutive bars form groups of the smallest power of two whose spacing
271    /// reaches it, capped at 2^30 bars. For example, 0.25 points at two pixels per point gives
272    /// groups of two at a threshold of 1, and four at a threshold of 2. Equality stays unmerged.
273    /// Non-positive and non-finite thresholds use 1. Price-based chart types stay unmerged;
274    /// gaps and per-bar colours keep their source samples even within a group.
275    /// [`Chart::prepare_conflation`](crate::Chart::prepare_conflation) prepares these groups
276    /// before rendering. Saved configurations without this field keep the default.
277    #[serde(default = "pixel_conflation_threshold")]
278    pub conflation_threshold: Option<f32>,
279    /// Background color for the chart (used when background_style is Solid)
280    pub background_color: Color32,
281    /// Background style (Solid, VerticalGradient, HorizontalGradient)
282    pub background_style: BackgroundStyle,
283    /// Grid line color
284    pub grid_color: Color32,
285    /// Vertical grid colour; `None` follows `grid_color`.
286    pub vertical_grid_color: Option<Color32>,
287    /// Grid line style, dotted by default.
288    pub grid_line_style: LineStyle,
289    /// Vertical grid line style; `None` follows `grid_line_style`.
290    pub vertical_grid_line_style: Option<LineStyle>,
291    /// Text color for labels
292    pub text_color: Color32,
293    /// Font size of the price and time axis labels in logical pixels; `None` follows the theme.
294    /// Default: None
295    pub axis_font_size: Option<f32>,
296    /// Character that groups the thousands of the built-in price labels: the ticks and tags of
297    /// price scales, the crosshair label and the legend's prices write `42,541.77` with the
298    /// default `','`. `None` writes plain digits (`42541.77`), and so do `'.'` and the ASCII
299    /// digits, since built-in labels always write `'.'` as the decimal point. Labels written by
300    /// a host formatter ([`Chart::set_price_formatter`](crate::Chart::set_price_formatter)) are
301    /// never grouped; study panes, tooltips and volumes keep their own formats. Layouts saved
302    /// without this field restore with the default.
303    /// Default: `Some(',')`
304    #[serde(default = "group_thousands_with_comma")]
305    pub thousands_sep: Option<char>,
306    /// Width of candlestick/bar bodies as a fraction of available space (0.0 to 1.0)
307    pub candle_width: f32,
308    /// Stroke width for candlestick wicks
309    pub wick_width: f32,
310    /// Stroke width for candle borders (0 = no border)
311    pub candle_border_width: f32,
312    /// Master visibility switch for horizontal and vertical grid lines
313    pub show_grid: bool,
314    /// Whether to show horizontal grid lines
315    pub show_horizontal_grid: bool,
316    /// Whether to show vertical grid lines
317    pub show_vertical_grid: bool,
318    /// Whether to show volume bars
319    pub show_volume: bool,
320    /// Volume height as a fraction of the price pane above `volume_baseline_inset`
321    pub volume_height_fraction: f32,
322    /// Distance in points from the volume baseline to the price pane bottom (12 by default).
323    pub volume_baseline_inset: f32,
324    /// Padding around the plot, excluding the axes and floating legend.
325    pub padding: f32,
326    /// Whether to show time labels on X-axis
327    pub show_time_labels: bool,
328    /// Whether to show OHLC info header
329    pub show_ohlc_info: bool,
330    /// Use the host description rather than the ticker in the legend title.
331    pub title_as_description: bool,
332    /// Show the current bid and ask controls in the legend.
333    pub show_quote_boxes: bool,
334    /// Show absolute bar change in the legend.
335    pub show_ohlc_change: bool,
336    /// Show percentage bar change in the legend.
337    pub show_ohlc_change_percent: bool,
338    /// Show volume in the legend independently of the histogram.
339    pub show_legend_volume: bool,
340    /// Show host-supplied study inputs in the legend.
341    pub show_indicator_inputs: bool,
342    /// Show the symbol's logo before the legend title: the legend badge, else the instrument's
343    /// logo, else the symbol's initial on a neutral disc.
344    pub show_legend_badge: bool,
345    /// Show the market status after the legend title: the session's state and delayed or
346    /// end-of-day data.
347    #[serde(default = "market_status_shown")]
348    pub show_market_status: bool,
349    /// Show the change of the last price from the previous session's close in the legend.
350    pub show_last_day_change: bool,
351    /// Opacity of the study rows' backdrop, from 0 to 1.
352    pub indicator_backdrop_opacity: f32,
353    /// Show the host-supplied currency above the price ticks.
354    pub show_currency: bool,
355    /// Show the host-supplied price unit above the price ticks.
356    pub show_unit: bool,
357    /// Show Auto, Log and Percent buttons in chart navigation.
358    pub show_scale_mode_buttons: bool,
359    /// Enable the crosshair price tag action that opens the host's price menu.
360    pub show_plus_button: bool,
361    /// Show the host-supplied extended-session price tag independently of the last price.
362    pub show_extended_session_price: bool,
363    /// Colour of the extended-session price tag.
364    pub extended_session_price_color: Color32,
365    /// Whether to show right price axis
366    pub show_right_axis: bool,
367    /// Whether to show left price axis
368    pub show_left_axis: bool,
369    /// Minimum right price axis width in pixels; current labels can grow the column.
370    pub right_axis_width: f32,
371    /// Minimum left price axis width in pixels; current labels can grow the column.
372    pub left_axis_width: f32,
373    /// Border, tick marks and label clipping of the price axis
374    pub price_axis: PriceAxisOptions,
375    /// Scale mode for right price axis (Normal, Logarithmic, Percentage, IndexedTo100)
376    pub right_axis_scale_mode: PriceScaleMode,
377    /// Scale mode for left price axis (Normal, Logarithmic, Percentage, IndexedTo100)
378    pub left_axis_scale_mode: PriceScaleMode,
379    /// Whether to show symbol name labels
380    pub show_symbol_labels: bool,
381    /// Whether to show last price value
382    pub show_symbol_last_val: bool,
383    /// Draw a line at the latest close independently of its tag.
384    pub show_price_line: bool,
385    /// Draw a line at the preceding session close independently of its tag.
386    pub show_prev_close_line: bool,
387    /// Whether to show previous close value line
388    pub show_symbol_prev_close: bool,
389    /// Whether to show indicator name labels
390    pub show_indicator_labels: bool,
391    /// Whether to show indicator last values
392    pub show_indicator_last_val: bool,
393    /// Preserve price units per bar when the time scale zooms or the pane resizes.
394    pub lock_price_to_bar_ratio: bool,
395    /// Exclude overlay studies and their primitives from the main price scale's autoscale
396    /// range, without hiding them or changing their individual autoscale settings.
397    #[serde(default)]
398    pub scale_price_chart_only: bool,
399    /// Draw the latest valid bid and ask lines and axis values supplied by the host.
400    pub show_bid_ask: bool,
401    /// Draw the highest high and lowest low of the visible bars on the price scale.
402    pub show_high_low: bool,
403    /// Whether to show countdown to next candle
404    pub show_countdown: bool,
405    /// Whether to show the scroll-to-latest button when scrolled away from the live edge.
406    pub show_realtime_btn: bool,
407    /// Position of the realtime button; bottom-right by default.
408    pub realtime_button_pos: RealtimeButtonPos,
409    /// Custom text for the realtime button (`None` draws a double right chevron).
410    pub realtime_button_text: Option<String>,
411    /// Size of the realtime button (width, height), 28 by 28 points by default.
412    pub realtime_button_size: (f32, f32),
413    /// Background color for the realtime button; `PLACEHOLDER` follows the theme.
414    pub realtime_button_color: Color32,
415    /// Hover background color for the realtime button; `PLACEHOLDER` follows the theme.
416    pub realtime_button_hover_color: Color32,
417    /// Text or icon color for the realtime button; `PLACEHOLDER` follows the theme.
418    pub realtime_button_text_color: Color32,
419    /// When to show the navigation buttons.
420    pub navigation_buttons: NavigationButtons,
421    /// When to show the maximize and menu buttons of the panes.
422    #[serde(default = "pane_buttons_on_hover")]
423    pub pane_buttons: NavigationButtons,
424    /// Whether to show session break vertical lines
425    pub show_session_breaks: bool,
426    /// Color for session break lines
427    pub session_break_color: Color32,
428    /// Style for session break lines
429    pub session_break_style: LineStyle,
430    /// Session configuration (market hours)
431    pub session_config: Option<SessionConfig>,
432    /// Shade bars identified as extended hours by the session metadata.
433    pub show_extended_hours: bool,
434    /// Extended-hours fill. `PLACEHOLDER` uses the active theme accent at low opacity.
435    pub extended_hours_color: Color32,
436    /// Whether to show the symbol watermark.
437    pub show_watermark: bool,
438    /// Text of the symbol watermark; `\n` starts another line. `None` shows the symbol and the
439    /// interval, such as `AAPL, 1D`, which follow symbol and interval changes.
440    pub watermark_text: Option<String>,
441    /// Without `watermark_text`, adds the instrument's description on a second line. Absent in
442    /// configurations saved before it existed, where it reads as `false`.
443    #[serde(default)]
444    pub watermark_description: bool,
445    /// Watermark color (typically semi-transparent)
446    pub watermark_color: Color32,
447    /// Font size of `watermark_text`. The symbol and interval line is twice this size and the
448    /// description line 11/12 of it.
449    pub watermark_font_size: f32,
450    /// Watermark position
451    pub watermark_pos: WatermarkPos,
452    /// Skip background rendering (use when chart is inside a container with its own background)
453    pub skip_background: bool,
454}
455
456/// Pos options for the "Go to Realtime" button
457#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
458pub enum RealtimeButtonPos {
459    /// Top-left of the chart
460    TopLeft,
461    /// Top-center of the chart
462    TopCenter,
463    /// Top-right of the chart
464    TopRight,
465    /// Bottom-left of the chart
466    BottomLeft,
467    /// Bottom-center of the chart
468    BottomCenter,
469    /// Bottom-right of the chart (default)
470    #[default]
471    BottomRight,
472}
473
474/// Pos options for the watermark overlay
475#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
476pub enum WatermarkPos {
477    /// Center of the chart (default)
478    #[default]
479    Center,
480    /// Top-left corner
481    TopLeft,
482    /// Top-right corner
483    TopRight,
484    /// Bottom-left corner
485    BottomLeft,
486    /// Bottom-right corner
487    BottomRight,
488}
489
490impl Default for ChartConfig {
491    fn default() -> Self {
492        let chart = Theme::dark().chart;
493        Self {
494            bullish_color: chart.candle_up,
495            bearish_color: chart.candle_down,
496            // Border colors (None = same as body)
497            bullish_border_color: None,
498            bearish_border_color: None,
499            // Wick colors (None = same as border/body)
500            bullish_wick_color: None,
501            bearish_wick_color: None,
502            // Price source for line charts (default: Close)
503            price_source: PriceSource::default(),
504            conflation_threshold: pixel_conflation_threshold(),
505            // Dark background
506            background_color: chart.bg,
507            // Default to solid background
508            background_style: BackgroundStyle::Solid,
509            // Grid colour from the default theme
510            grid_color: chart.grid_line,
511            vertical_grid_color: None,
512            grid_line_style: LineStyle::Dotted,
513            vertical_grid_line_style: None,
514            // Text color
515            text_color: chart.axis_text,
516            axis_font_size: None,
517            thousands_sep: Some(','),
518            // Candle bodies leave room for the wider volume bars
519            candle_width: 0.6,
520            // Wick - thin crisp lines
521            wick_width: 1.0,
522            // Candle border width (0 = filled candles, >0 = hollow with border)
523            candle_border_width: 0.0,
524            show_grid: true,
525            show_horizontal_grid: true,
526            show_vertical_grid: true,
527            show_volume: true,
528            volume_height_fraction: 0.12,
529            volume_baseline_inset: 12.0,
530            padding: 0.0,
531            show_time_labels: true,
532            show_ohlc_info: true,
533            title_as_description: true,
534            show_quote_boxes: true,
535            show_ohlc_change: true,
536            show_ohlc_change_percent: true,
537            show_legend_volume: true,
538            show_indicator_inputs: true,
539            show_legend_badge: true,
540            show_market_status: true,
541            show_last_day_change: false,
542            indicator_backdrop_opacity: 0.6,
543            show_currency: true,
544            show_unit: true,
545            show_scale_mode_buttons: true,
546            show_plus_button: true,
547            show_extended_session_price: true,
548            extended_session_price_color: Color32::from_rgb(251, 142, 1),
549            show_right_axis: true,
550            show_left_axis: false,
551            right_axis_width: 70.0,
552            left_axis_width: 70.0,
553            price_axis: PriceAxisOptions::default(),
554            right_axis_scale_mode: PriceScaleMode::Normal,
555            left_axis_scale_mode: PriceScaleMode::Normal,
556            show_symbol_labels: true,
557            show_symbol_last_val: true,
558            show_price_line: true,
559            show_prev_close_line: false,
560            show_symbol_prev_close: false,
561            show_indicator_labels: true,
562            show_indicator_last_val: true,
563            lock_price_to_bar_ratio: false,
564            scale_price_chart_only: false,
565            show_bid_ask: false,
566            show_high_low: false,
567            show_countdown: true,
568            show_realtime_btn: true,
569            realtime_button_pos: RealtimeButtonPos::BottomRight,
570            realtime_button_text: None,
571            realtime_button_size: (
572                DESIGN_TOKENS.sizing.charts_ext.realtime_button_width,
573                DESIGN_TOKENS.sizing.button_md,
574            ),
575            realtime_button_color: Color32::PLACEHOLDER,
576            realtime_button_hover_color: Color32::PLACEHOLDER,
577            realtime_button_text_color: Color32::PLACEHOLDER,
578            navigation_buttons: NavigationButtons::default(),
579            pane_buttons: pane_buttons_on_hover(),
580            // Session breaks disabled by default
581            show_session_breaks: false,
582            session_break_color: chart.grid_line_major,
583            session_break_style: LineStyle::Dashed,
584            session_config: None,
585            show_extended_hours: true,
586            extended_hours_color: Color32::PLACEHOLDER,
587            // Watermark disabled by default
588            show_watermark: false,
589            watermark_text: None,
590            watermark_description: false,
591            watermark_color: chart.grid_line,
592            watermark_font_size: 48.0,
593            watermark_pos: WatermarkPos::Center,
594            // Background enabled by default (set false when inside a container)
595            skip_background: false,
596        }
597    }
598}
599
600impl ChartConfig {
601    /// Apply the theme's background, grid, axis text, candle, session break and watermark
602    /// colours to this configuration.
603    ///
604    /// Only the configuration changes. A chart paints its legend, quote boxes, labels and
605    /// overlays from its own theme, which [`ChartBuilder::with_theme`](crate::ChartBuilder::with_theme)
606    /// keeps for that chart, so switch a chart's theme with
607    /// [`Chart::set_theme`](crate::Chart::set_theme) or
608    /// [`Chart::set_theme_override`](crate::Chart::set_theme_override), which apply these
609    /// colours too.
610    pub fn with_theme(mut self, theme: &Theme) -> Self {
611        self.background_color = theme.chart.bg;
612        self.grid_color = theme.chart.grid_line;
613        self.text_color = theme.chart.axis_text;
614        self.bullish_color = theme.chart.candle_up;
615        self.bearish_color = theme.chart.candle_down;
616        self.bullish_border_color = Some(theme.chart.candle_up);
617        self.bearish_border_color = Some(theme.chart.candle_down);
618        self.bullish_wick_color = Some(theme.chart.candle_up);
619        self.bearish_wick_color = Some(theme.chart.candle_down);
620        self.session_break_color = theme.chart.grid_line_major;
621        self.watermark_color = if theme.dark {
622            theme.chart.grid_line
623        } else {
624            // A cool ink: the interface text would give a neutral grey on the white plot.
625            theme
626                .chart
627                .bg
628                .lerp_to_gamma(Color32::from_rgb(10, 16, 33), 0.18)
629        };
630        self
631    }
632}
633
634#[cfg(test)]
635mod tests {
636    use super::*;
637
638    #[test]
639    fn with_theme_recolours_session_breaks_and_watermark() {
640        let config = ChartConfig::default().with_theme(&Theme::light());
641        assert_eq!(config.session_break_color, Color32::from_rgb(229, 229, 229));
642        assert_eq!(config.watermark_color, Color32::from_rgb(211, 212, 215));
643    }
644
645    #[test]
646    fn saved_conflation_threshold_keeps_disabled_custom_and_legacy_modes() {
647        let mut saved = serde_json::to_value(ChartConfig::default()).unwrap();
648        saved
649            .as_object_mut()
650            .unwrap()
651            .remove("conflation_threshold");
652        let restored: ChartConfig = serde_json::from_value(saved.clone()).unwrap();
653        assert_eq!(restored.conflation_threshold, Some(1.0));
654        for threshold in [None, Some(0.5), Some(2.0)] {
655            saved["conflation_threshold"] = serde_json::to_value(threshold).unwrap();
656            let restored: ChartConfig = serde_json::from_value(saved.clone()).unwrap();
657            assert_eq!(restored.conflation_threshold, threshold);
658        }
659    }
660}