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}