1use crate::chart::selection::SelectionState;
38use crate::config::{ChartConfig, ChartOptions, RealtimeButtonPos};
39use crate::data::{Datafeed, FeedState};
40#[cfg(feature = "drawings")]
41use crate::drawings::DrawingManager;
42use crate::model::{BarClock, BarData, Timeframe};
43use crate::model::{ChartType, ChartTypeParams};
44use crate::scales::{PriceMap, PriceRange, PriceTransform};
45use crate::studies::ChartState;
46use crate::studies::IndicatorRegistry;
47use crate::theme::Theme;
48#[cfg(feature = "drawings")]
49use crate::widget::tool_interaction::cursor_modes::CursorModeState;
50
51use super::Chart;
52use super::state::{BoxZoomState, KineticScrollState};
53use std::collections::BTreeMap;
54use std::sync::Arc;
55
56impl Chart {
57 pub fn new(data: BarData) -> Self {
67 let chart_options = ChartOptions::default();
68 let (bars, rejected_bars) = super::series::clean(data.bars, &BarClock::exact());
69 let mut state = ChartState::new(BarData::from_bars(bars));
70
71 {
73 let ts_opt = &chart_options.time_scale;
74 state.time_scale_mut().apply_options(
75 ts_opt.bar_spacing,
76 ts_opt.min_bar_spacing,
77 ts_opt.max_bar_spacing,
78 ts_opt.fix_left_edge,
79 ts_opt.fix_right_edge,
80 ts_opt.right_offset,
81 ts_opt.right_offset_pixels,
82 );
83 }
84
85 Self {
86 state,
87 replay: None,
88 market_replay: None,
89 config: ChartConfig::default(),
90 applied_config_scale_mode: Default::default(),
91 start_idx: 0,
92 chart_options,
93 keyboard: super::keyboard::KeyboardState::default(),
94 kinetic_scroll: KineticScrollState::new(),
95 scroll_start_pos: None,
96 scroll_start_offset: None,
97 prev_width: None,
98 desired_visible_bars: None,
99 last_visible_bars: 0,
100 apply_visible_bars_once: false,
101 price_axis_drag: None,
102 pending_start_idx: None,
103 pending_scroll_time: None,
104 scroll_animation: None,
105 time_axis_rect: egui::Rect::NOTHING,
106 chart_type: ChartType::Candles,
107 chart_type_params: ChartTypeParams::default(),
108 #[cfg(feature = "studies")]
109 trade_profiles: Default::default(),
110 pending_box_sizes: [false; 4],
111 older_bars_drawn: true,
112 tracking_mode_active: false,
113 mouse_in_chart: false,
114 feed: FeedState {
115 rejected_bars,
116 ..FeedState::default()
117 },
118 from_ticks: BTreeMap::new(),
119 bar_limits: Default::default(),
120 studies: IndicatorRegistry::new(),
121 study_data: Default::default(),
122 element_studies: Default::default(),
123 execution_ranges: Default::default(),
124 #[cfg(feature = "studies")]
125 volume_setting: false,
126 #[cfg(feature = "studies")]
127 volume_study: None,
128 main_series_visible: true,
129 main_series_present: true,
130 main_series_scale: Default::default(),
131 price_series: Vec::new(),
132 heikin_ashi: Default::default(),
133 series_scales: Default::default(),
134 study_ids: Vec::new(),
135 study_limit: None,
136 study_inputs: Vec::new(),
137 study_autoscale: Vec::new(),
138 pending_from: Vec::new(),
139 next_refresh: Vec::new(),
140 next_series: 2,
142 primitives: Vec::new(),
143 next_primitive: 0,
144 #[cfg(feature = "drawings")]
145 drawings: DrawingManager::new(),
146 #[cfg(feature = "drawings")]
147 symbol_drawings: Default::default(),
148 #[cfg(not(feature = "drawings"))]
149 symbol_markers: Default::default(),
150 #[cfg(feature = "drawings")]
151 drawing_store: None,
152 #[cfg(feature = "drawings")]
153 restored_studies: Default::default(),
154 history: super::history::ChartHistory::default(),
155 pane_layout: super::pane::PaneLayout::default(),
156 pane_options: Default::default(),
157 box_zoom: BoxZoomState::new(),
158 zoom_mode_active: false,
159 zoom_just_applied: false,
160 symbol: String::new(),
161 legend_instrument: None,
162 currency_choices: Vec::new(),
163 currency_choice: None,
164 legend_badge: None,
165 watermark: None,
166 legend_statuses: Vec::new(),
167 quote: None,
168 quote_action: None,
169 server_time_offset: None,
170 bar_close_time: None,
171 indicator_settings_request: None,
172 session_quote: None,
173 session_provider: None,
174 horizontal_scale: None,
175 locale: None,
176 date_format: None,
177 time_formatter: None,
178 price_formatter: None,
179 percentage_formatter: None,
180 volume_formatter: None,
181 series_formatters: Default::default(),
182 extended_hours: false,
183 conversion: None,
184 measured_axis_width: 0.0,
185 last_layout: None,
186 last_ratio_lock: false,
187 axis_hover: None,
188 scale_settings_requested: false,
189 theme_override: None,
190 inherited_theme: None,
191 restored_palettes: Vec::new(),
192 host_events: Default::default(),
193 indicator_source_request: None,
194 events: Default::default(),
195 time_scale_events: Default::default(),
196 frozen_source_events: 0,
197 event_visibility: Default::default(),
198 mark_source: None,
199 #[cfg(feature = "drawings")]
200 post_source: None,
201 signal_sources: Vec::new(),
202 targets: Default::default(),
203 show_price_targets: false,
204 hidden_signal_categories: Default::default(),
205 event_click: None,
206 mark_hits: std::cell::RefCell::default(),
207 clickable_marks: Vec::new(),
208 timeframe: None,
209 feed_link: None,
210 #[cfg(feature = "drawings")]
211 cursor_modes: CursorModeState::new(),
212 main_series_style: crate::model::SeriesStyle::default(),
213 last_price_pulse: std::cell::Cell::new(None),
214 last_price_map: PriceMap::new(
215 PriceTransform::default(),
216 PriceRange::new(0.0, 100.0),
217 0.0,
218 0.0,
219 false,
220 ),
221 last_rendered_price_rect: egui::Rect::NOTHING,
222 last_rendered_volume_rect: egui::Rect::NOTHING,
223 last_rendered_indicator_panes: Vec::new(),
224 layout_inactive: false,
226 synced_crosshair_bar_idx: None,
227 synced_crosshair_period: None,
228 synced_crosshair_price: None,
229 last_crosshair: None,
230 last_hover_bar_idx: None,
231 marks: Default::default(),
233 marker_options: Default::default(),
234 change_marks: None,
235 price_lines: Vec::new(),
236 next_price_line: 0,
237 #[cfg(feature = "ui")]
238 trading_tags: Vec::new(),
239 #[cfg(feature = "ui")]
240 trading_outlined: Vec::new(),
241 #[cfg(feature = "ui")]
242 trading_bid_ask: false,
243 #[cfg(feature = "ui")]
244 order_quantity: None,
245 #[cfg(feature = "ui")]
246 quantity_clicked: None,
247 selection: SelectionState::new(),
249 right_click: None,
250 menu_open: false,
251 size: None,
252 merge_groups: Default::default(),
253 }
254 }
255
256 pub fn with_config(data: BarData, config: ChartConfig) -> Self {
266 let mut chart = Self::new(data);
267 chart.update_config(config);
268 chart
269 }
270
271 pub fn with_chart_options(mut self, options: ChartOptions) -> Self {
278 let options = self.normalize_chart_options(options);
279 {
280 let ts_opt = &options.time_scale;
281 self.state.time_scale_mut().apply_options(
282 ts_opt.bar_spacing,
283 ts_opt.min_bar_spacing,
284 ts_opt.max_bar_spacing,
285 ts_opt.fix_left_edge,
286 ts_opt.fix_right_edge,
287 ts_opt.right_offset,
288 ts_opt.right_offset_pixels,
289 );
290 }
291 self.chart_options = options;
292 self
293 }
294
295 pub fn visible_bars(mut self, count: usize) -> Self {
301 self.desired_visible_bars = Some(count);
304 self.apply_visible_bars_once = true;
305 self.start_idx = self.state.data().len().saturating_sub(count);
307 self
308 }
309
310 pub fn start_idx(mut self, index: usize) -> Self {
316 self.start_idx = index;
317 self
318 }
319
320 pub fn get_visible_bars(&self) -> usize {
325 if self.last_visible_bars == 0 {
329 100
330 } else {
331 self.last_visible_bars
332 }
333 }
334
335 pub fn get_start_idx(&self) -> usize {
340 self.start_idx
341 }
342
343 pub(super) fn config_scale_mode(&self) -> crate::scales::PriceScaleMode {
345 if self.config.show_left_axis && !self.config.show_right_axis {
346 self.config.left_axis_scale_mode
347 } else {
348 self.config.right_axis_scale_mode
349 }
350 }
351
352 pub(super) fn apply_config_scale_mode(&mut self) {
354 let mode = self.config_scale_mode();
355 if self.applied_config_scale_mode != mode {
356 if self.state.price_scale().options().mode != mode {
357 self.state.price_scale_mut().reset();
358 self.state.clear_zoom_history();
359 self.state.price_scale_mut().options_mut().mode = mode;
360 }
361 self.applied_config_scale_mode = mode;
362 }
363 }
364
365 pub fn update_config(&mut self, config: ChartConfig) {
370 self.config = config;
371 self.applied_config_scale_mode = self.state.price_scale().options().mode;
372 self.apply_config_scale_mode();
373 }
374
375 pub fn set_visible_bars(&mut self, count: usize) {
380 if self.last_visible_bars == 0 || count != self.last_visible_bars {
382 self.desired_visible_bars = Some(count);
383 self.apply_visible_bars_once = true;
384 }
385 }
386
387 pub fn set_start_idx(&mut self, index: usize) {
393 if index != self.start_idx {
394 self.start_idx = index;
395 self.pending_start_idx = Some(index);
396 self.pending_scroll_time = None;
397 }
398 }
399
400 pub fn scroll_to_time(&mut self, time: chrono::DateTime<chrono::Utc>) {
406 self.pending_scroll_time = Some(time);
407 self.pending_start_idx = None;
408 self.disable_tracking_mode();
409 self.kinetic_scroll = KineticScrollState::new();
410 }
411
412 pub fn set_chart_type(&mut self, chart_type: ChartType) {
420 if self.chart_type != chart_type {
421 self.state.clear_zoom_history();
422 self.selection.deselect();
423 #[cfg(feature = "drawings")]
424 self.drawings.deselect();
425 self.chart_type = chart_type;
426 }
427 }
428
429 pub fn enable_tracking_mode(&mut self) {
434 self.tracking_mode_active = true;
435 self.pending_scroll_time = None;
436 self.state.time_scale_mut().scroll_to_realtime();
438 }
439
440 pub fn disable_tracking_mode(&mut self) {
442 self.tracking_mode_active = false;
443 }
444
445 pub fn toggle_tracking_mode(&mut self) {
447 if self.tracking_mode_active {
448 self.disable_tracking_mode();
449 } else {
450 self.enable_tracking_mode();
451 }
452 }
453
454 pub fn is_tracking_mode_active(&self) -> bool {
456 self.tracking_mode_active
457 }
458
459 pub fn chart_type(&self) -> ChartType {
461 self.chart_type
462 }
463
464 pub fn range_bar_source(&self) -> Option<crate::model::RangeBarSource> {
468 use crate::model::RangeBarSource::{Executions, OhlcApproximation};
469 (self.chart_type == ChartType::Range).then(|| {
470 if self.execution_ranges.in_use() {
471 Executions
472 } else {
473 OhlcApproximation
474 }
475 })
476 }
477
478 pub fn set_chart_type_params(&mut self, params: ChartTypeParams) {
488 if self.chart_type.is_time_independent() && self.chart_type_params != params {
489 self.state.clear_zoom_history();
490 self.selection.deselect();
491 #[cfg(feature = "drawings")]
492 self.drawings.deselect();
493 }
494 self.chart_type_params = params;
495 self.pending_box_sizes = self
496 .chart_type_params
497 .size_methods()
498 .map(|method| method != crate::model::BoxSizeMethod::Fixed);
499 self.chart_type_params
500 .resolve_sizes(&self.state.data().bars, &mut self.pending_box_sizes);
501 }
502
503 pub fn chart_type_params(&self) -> &ChartTypeParams {
505 &self.chart_type_params
506 }
507
508 pub(crate) fn calculate_bar_spacing(&self, width: f32, visible_bars: usize) -> f32 {
511 if visible_bars == 0 {
512 self.chart_options.time_scale.bar_spacing
513 } else {
514 width / visible_bars as f32
515 }
516 }
517
518 pub fn get_rendered_price_rect(&self) -> egui::Rect {
523 self.last_rendered_price_rect
524 }
525
526 pub fn price_axis_width(&self, side: crate::scales::PriceScaleId) -> f32 {
529 let (shown, minimum) = match side {
530 crate::scales::PriceScaleId::Right => {
531 (self.config.show_right_axis, self.config.right_axis_width)
532 }
533 crate::scales::PriceScaleId::Left => {
534 (self.config.show_left_axis, self.config.left_axis_width)
535 }
536 };
537 if shown {
538 minimum.max(self.measured_axis_width)
539 } else {
540 0.0
541 }
542 }
543
544 pub fn get_rendered_volume_rect(&self) -> egui::Rect {
549 self.last_rendered_volume_rect
550 }
551
552 pub fn get_rendered_indicator_panes(&self) -> &[super::RenderedIndicatorPane] {
557 &self.last_rendered_indicator_panes
558 }
559}
560
561pub struct ChartBuilder {
586 feed: Option<Arc<dyn Datafeed>>,
587 symbol: Option<String>,
588 timeframe: Timeframe,
589 theme: Option<Theme>,
590 chart_type: ChartType,
591 config: ChartConfig,
592 chart_options: ChartOptions,
593 indicators: Option<IndicatorRegistry>,
594 #[cfg(feature = "drawings")]
595 drawing_registry: crate::drawings::DrawingToolRegistry,
596 visible_candles: usize,
597 initial_bars: usize,
599 locale: Option<String>,
600 time_formatter: Option<Arc<dyn crate::scales::TimeFormatter>>,
601 price_formatter: Option<Arc<dyn crate::scales::PriceFormatter>>,
602 percentage_formatter: Option<Arc<dyn crate::scales::PriceFormatter>>,
603 volume_formatter: Option<Arc<dyn crate::scales::PriceFormatter>>,
604 #[cfg(feature = "drawings")]
605 storage: Option<Arc<dyn crate::services::Storage>>,
606 #[cfg(feature = "ui")]
607 study_factory: Option<crate::studies::IndicatorFactory>,
608}
609
610impl ChartBuilder {
611 pub fn new() -> Self {
616 Self {
617 feed: None,
618 symbol: None,
619 timeframe: Timeframe::Min1,
620 theme: None,
621 chart_type: ChartType::Candles,
622 config: ChartConfig::default(),
623 chart_options: ChartOptions::default(),
624 indicators: None,
625 #[cfg(feature = "drawings")]
626 drawing_registry: Default::default(),
627 visible_candles: 100,
628 initial_bars: 1000,
629 locale: None,
630 time_formatter: None,
631 price_formatter: None,
632 percentage_formatter: None,
633 volume_formatter: None,
634 #[cfg(feature = "drawings")]
635 storage: None,
636 #[cfg(feature = "ui")]
637 study_factory: None,
638 }
639 }
640
641 pub fn options_chart() -> Self {
661 let mut chart_options = ChartOptions::default();
662 chart_options.time_scale.time_visible = false;
664 chart_options.time_scale.seconds_visible = false;
665 chart_options.time_scale.ticks_visible = false;
666
667 Self::new()
668 .with_config(ChartConfig {
669 show_time_labels: false,
670 ..Default::default()
671 })
672 .with_chart_options(chart_options)
673 .with_chart_type(ChartType::Line)
674 }
675
676 pub fn price_chart() -> Self {
713 use crate::config::CrosshairOptions;
714
715 let config = ChartConfig {
716 show_horizontal_grid: false,
717 show_vertical_grid: false,
718 ..ChartConfig::default()
719 };
720
721 let chart_options = ChartOptions {
722 crosshair: CrosshairOptions {
723 vert_line_visible: false,
724 horz_line_visible: false,
725 label_visible: false,
726 ..CrosshairOptions::default()
727 },
728 ..ChartOptions::default()
729 };
730
731 Self::new()
732 .with_config(config)
733 .with_chart_options(chart_options)
734 .with_chart_type(ChartType::Line)
735 .with_visible_candles(50)
736 }
737
738 #[must_use]
745 pub fn with_feed(mut self, feed: Arc<dyn Datafeed>) -> Self {
746 self.feed = Some(feed);
747 self
748 }
749
750 #[must_use]
755 pub fn with_symbol(mut self, symbol: impl Into<String>) -> Self {
756 self.symbol = Some(symbol.into());
757 self
758 }
759
760 #[must_use]
762 pub fn with_timeframe(mut self, timeframe: Timeframe) -> Self {
763 self.timeframe = timeframe;
764 self
765 }
766
767 #[must_use]
773 pub fn with_theme(mut self, theme: Theme) -> Self {
774 self.theme = Some(theme);
775 self
776 }
777
778 #[must_use]
782 pub fn with_chart_type(mut self, chart_type: ChartType) -> Self {
783 self.chart_type = chart_type;
784 self
785 }
786
787 #[must_use]
789 pub fn with_config(mut self, config: ChartConfig) -> Self {
790 self.config = config;
791 self
792 }
793
794 #[must_use]
796 pub fn with_chart_options(mut self, options: ChartOptions) -> Self {
797 self.chart_options = options;
798 self
799 }
800
801 #[must_use]
803 pub fn with_session_breaks(mut self, show: bool) -> Self {
804 self.config.show_session_breaks = show;
805 self
806 }
807
808 #[must_use]
811 pub fn with_indicators(mut self, registry: IndicatorRegistry) -> Self {
812 self.indicators = Some(registry);
813 self
814 }
815
816 #[cfg(feature = "drawings")]
818 #[cfg_attr(docsrs, doc(cfg(feature = "drawings")))]
819 #[must_use]
820 pub fn with_drawing_registry(mut self, registry: crate::drawings::DrawingToolRegistry) -> Self {
821 self.drawing_registry = registry;
822 self
823 }
824
825 #[must_use]
827 pub fn with_locale(mut self, locale: impl Into<String>) -> Self {
828 self.locale = Some(locale.into());
829 self
830 }
831
832 #[must_use]
834 pub fn with_time_formatter(mut self, formatter: Arc<dyn crate::scales::TimeFormatter>) -> Self {
835 self.time_formatter = Some(formatter);
836 self
837 }
838
839 #[must_use]
841 pub fn with_price_formatter(
842 mut self,
843 formatter: Arc<dyn crate::scales::PriceFormatter>,
844 ) -> Self {
845 self.price_formatter = Some(formatter);
846 self
847 }
848
849 #[must_use]
851 pub fn with_percentage_formatter(
852 mut self,
853 formatter: Arc<dyn crate::scales::PriceFormatter>,
854 ) -> Self {
855 self.percentage_formatter = Some(formatter);
856 self
857 }
858
859 #[must_use]
861 pub fn with_volume_formatter(
862 mut self,
863 formatter: Arc<dyn crate::scales::PriceFormatter>,
864 ) -> Self {
865 self.volume_formatter = Some(formatter);
866 self
867 }
868
869 #[cfg(feature = "drawings")]
871 #[cfg_attr(docsrs, doc(cfg(feature = "drawings")))]
872 #[must_use]
873 pub fn with_storage(mut self, storage: Arc<dyn crate::services::Storage>) -> Self {
874 self.storage = Some(storage);
875 self
876 }
877
878 #[cfg(feature = "ui")]
882 #[cfg_attr(docsrs, doc(cfg(feature = "ui")))]
883 #[must_use]
884 pub fn with_study_factory(mut self, factory: crate::studies::IndicatorFactory) -> Self {
885 self.study_factory = Some(factory);
886 self
887 }
888
889 #[must_use]
894 pub fn with_visible_candles(mut self, count: usize) -> Self {
895 self.visible_candles = count;
896 self
897 }
898
899 #[must_use]
909 pub fn with_initial_bars(mut self, bars: usize) -> Self {
910 self.initial_bars = bars;
911 self
912 }
913
914 #[must_use]
929 pub fn with_right_price_scale(
930 mut self,
931 visible: bool,
932 mode: crate::scales::PriceScaleMode,
933 ) -> Self {
934 self.config.show_right_axis = visible;
935 self.config.right_axis_scale_mode = mode;
936 self
937 }
938
939 #[must_use]
943 pub fn with_realtime_btn(mut self, visible: bool) -> Self {
944 self.config.show_realtime_btn = visible;
945 self
946 }
947
948 #[must_use]
952 pub fn with_realtime_button_pos(mut self, position: RealtimeButtonPos) -> Self {
953 self.config.realtime_button_pos = position;
954 self
955 }
956
957 #[must_use]
961 pub fn with_realtime_button_text(mut self, text: impl Into<String>) -> Self {
962 self.config.realtime_button_text = Some(text.into());
963 self
964 }
965
966 #[must_use]
970 pub fn with_realtime_button_size(mut self, width: f32, height: f32) -> Self {
971 self.config.realtime_button_size = (width, height);
972 self
973 }
974
975 pub fn build(mut self) -> Chart {
981 if let Some(theme) = &self.theme {
982 self.config = self.config.with_theme(theme);
983 }
984
985 let mut chart =
986 Chart::with_config(BarData::new(), self.config).with_chart_options(self.chart_options);
987
988 chart.theme_override = self.theme;
989 chart.set_locale(self.locale);
990 chart.set_time_formatter(self.time_formatter);
991 chart.set_price_formatter(self.price_formatter);
992 chart.set_percentage_formatter(self.percentage_formatter);
993 chart.set_volume_formatter(self.volume_formatter);
994 chart.set_chart_type(self.chart_type);
995 chart.set_visible_bars(self.visible_candles);
996 chart.set_symbol(self.symbol.unwrap_or_default());
997 chart.set_timeframe(self.timeframe);
998 #[cfg(feature = "drawings")]
999 {
1000 chart.drawings.registry = self.drawing_registry;
1001 if let Some(storage) = self.storage {
1002 chart.set_storage(storage);
1003 }
1004 }
1005 if let Some(feed) = self.feed {
1006 chart.attach_feed(feed, self.initial_bars);
1007 }
1008
1009 let mut studies = self.indicators.unwrap_or_default();
1010 while let Some(study) = studies.remove_indicator(0) {
1011 chart.add_indicator(study);
1012 }
1013 chart
1014 }
1015
1016 #[cfg(feature = "ui")]
1025 #[cfg_attr(docsrs, doc(cfg(feature = "ui")))]
1026 pub fn build_restored(
1027 mut self,
1028 snapshot: &super::ChartSnapshot,
1029 ) -> Result<(Chart, Vec<crate::drawings::persistence::SkippedDrawing>), super::RestoreError>
1030 {
1031 let factory = self.study_factory.take().unwrap_or_default();
1032 let mut chart = self.build();
1033 let skipped = chart.restore_with(snapshot, &factory)?;
1034 Ok((chart, skipped))
1035 }
1036}
1037
1038impl Default for ChartBuilder {
1039 fn default() -> Self {
1040 Self::new()
1041 }
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046 use super::*;
1047 use crate::model::Bar;
1048 use chrono::{Duration, TimeZone, Utc};
1049
1050 #[test]
1053 #[cfg(feature = "ui")]
1054 fn build_restored_applies_bindings_then_state_or_fails_without_a_chart() {
1055 let saved = ChartBuilder::new()
1056 .with_symbol("SAVED")
1057 .with_indicators({
1058 let mut studies = IndicatorRegistry::default();
1059 studies.add_indicator(Box::new(crate::studies::SMA::new(5)));
1060 studies
1061 })
1062 .build()
1063 .snapshot()
1064 .unwrap();
1065 let formatter = Arc::new(crate::scales::CustomPriceFormatter::new(|price| {
1066 format!("host {price:.1}")
1067 }));
1068 let (chart, skipped) = ChartBuilder::new()
1069 .with_symbol("OTHER")
1070 .with_price_formatter(formatter)
1071 .build_restored(&saved)
1072 .unwrap();
1073 assert!(skipped.is_empty());
1074 assert_eq!(chart.symbol(), "SAVED");
1075 assert_eq!(chart.indicators().count(), 1);
1076 let labelled = chart
1077 .series_label_formatter(
1078 crate::widget::SeriesId::MAIN,
1079 crate::scales::PriceScaleMode::Normal,
1080 )
1081 .map(|formatter| formatter.format(1.5));
1082 assert_eq!(labelled.as_deref(), Some("host 1.5"));
1083
1084 let mut invalid = saved;
1085 invalid.bar_spacing = 0.0;
1086 assert!(ChartBuilder::new().build_restored(&invalid).is_err());
1087 }
1088
1089 #[test]
1090 #[cfg(feature = "drawings")]
1091 fn drawings_keep_times_through_replacement_streaming_and_symbol_switches() {
1092 use crate::drawings::{ChartPoint, Drawing, DrawingToolType};
1093 use crate::widget::SeriesId;
1094 let bars = |times: &[i64]| {
1095 times
1096 .iter()
1097 .map(|&t| Bar::new(Utc.timestamp_opt(t, 0).unwrap(), 10.0, 11.0, 9.0, 10.0, 1.0))
1098 .collect()
1099 };
1100 let mut chart = Chart::new(BarData::from_bars(bars(&[60, 120, 180])));
1101 chart.set_symbol("A");
1102 chart.user_action(|chart| {
1103 let mut drawing = Drawing::new(7, DrawingToolType::TrendLine);
1104 drawing.chart_points = [-0.5, 0.5, 3.5].map(|i| ChartPoint::new(i, 10.0)).to_vec();
1105 chart.drawings_mut().drawings.push(drawing);
1106 chart
1107 .drawings_mut()
1108 .edit_drawing(7, |d| d.color = [1, 2, 3, 255]);
1109 });
1110 chart.set_markers(
1111 SeriesId::MAIN,
1112 vec![crate::model::Marker::new(Utc.timestamp_opt(90, 0).unwrap())],
1113 );
1114 for (times, expected) in [
1117 (vec![0, 60, 120, 180], [0.5, 1.5, 4.5]),
1118 (vec![0, 120, 240, 360], [0.25, 0.75, 2.25]),
1119 (vec![], [0.25, 0.75, 2.25]),
1120 (vec![0, 60, 180, 300], [0.5, 1.25, 2.75]),
1121 ] {
1122 chart.set_data(SeriesId::MAIN, bars(×));
1123 for (point, want) in chart.drawings().drawings[0]
1124 .chart_points
1125 .iter()
1126 .zip(expected)
1127 {
1128 assert_eq!(point.bar_idx, want, "times {times:?}");
1129 }
1130 }
1131 assert!(chart.undo());
1132 assert_eq!(chart.drawings().drawings[0].chart_points[1].bar_idx, 1.25);
1133 assert!(chart.redo());
1134 chart.update(SeriesId::MAIN, bars(&[120]).pop().unwrap());
1135 assert_eq!(chart.drawings().drawings[0].chart_points[1].bar_idx, 1.5);
1136 chart.drawings_mut().drawings[0].chart_points[1].bar_idx = 2.5;
1138 chart.drawings_mut().stay_in_drawing_mode = true;
1139 chart.drawings_mut().eraser_mode = true;
1140 chart.drawings_mut().start_drag_body(7, egui::Pos2::ZERO);
1141 chart.drawings_mut().registry.set_availability(
1142 DrawingToolType::TrendLine,
1143 crate::drawings::ToolAvailability {
1144 visible: false,
1145 enabled: false,
1146 },
1147 );
1148 chart.set_symbol("B");
1149 assert!(
1150 !chart
1151 .drawings()
1152 .registry
1153 .availability(DrawingToolType::TrendLine)
1154 .enabled
1155 );
1156 assert!(chart.drawings().drawings.is_empty());
1157 assert!(chart.marks.is_empty());
1158 assert!(chart.drawings().stay_in_drawing_mode);
1159 assert!(chart.drawings().eraser_mode);
1160 chart.set_data(SeriesId::MAIN, bars(&[0, 120, 240]));
1161 chart.set_symbol("A");
1162 assert!(chart.drawings().dragging_handle.is_none());
1163 chart.set_data(SeriesId::MAIN, bars(&[0, 60, 120, 180, 240, 300]));
1164 assert_eq!(chart.drawings().drawings[0].chart_points[1].bar_idx, 2.5);
1165 assert_eq!(chart.marks.len(), 1);
1166 }
1167
1168 #[test]
1169 fn configured_price_mode_reaches_constructors_and_mutations() {
1170 use crate::scales::PriceScaleMode;
1171 for entry in 0..4 {
1172 let config = ChartConfig {
1173 right_axis_scale_mode: PriceScaleMode::Logarithmic,
1174 ..Default::default()
1175 };
1176 let mut chart = match entry {
1177 0 => Chart::with_config(BarData::new(), config.clone()),
1178 1 => ChartBuilder::new().with_config(config.clone()).build(),
1179 _ => Chart::new(BarData::new()),
1180 };
1181 if entry == 2 {
1182 chart.update_config(config);
1183 } else if entry == 3 {
1184 chart.config_mut().right_axis_scale_mode = PriceScaleMode::Logarithmic;
1185 }
1186 let ctx = egui::Context::default();
1187 ctx.run_ui(Default::default(), |ui| {
1188 chart.show(ui);
1189 })
1190 .drop_without_applying_deltas();
1191 assert_eq!(
1192 chart.state.price_scale().options().mode,
1193 PriceScaleMode::Logarithmic,
1194 "entry {entry}"
1195 );
1196 chart.state.price_scale_mut().set_range(1.0, 2.0);
1197 chart.update_config(chart.config().clone());
1198 assert!(chart.state.price_scale().is_manual());
1199 chart.config_mut().right_axis_scale_mode = PriceScaleMode::Normal;
1200 ctx.run_ui(Default::default(), |ui| {
1201 chart.show(ui);
1202 })
1203 .drop_without_applying_deltas();
1204 assert_eq!(
1205 chart.state.price_scale().options().mode,
1206 PriceScaleMode::Normal
1207 );
1208 assert!(!chart.state.price_scale().is_manual());
1209 chart.state.price_scale_mut().set_range(10.0, 20.0);
1210 let range = chart.state.price_scale().range();
1211 chart.user_action(|chart| {
1212 chart.config_mut().right_axis_scale_mode = PriceScaleMode::Logarithmic;
1213 });
1214 ctx.run_ui(Default::default(), |ui| {
1215 chart.show(ui);
1216 })
1217 .drop_without_applying_deltas();
1218 assert!(chart.undo());
1219 ctx.run_ui(Default::default(), |ui| {
1220 chart.show(ui);
1221 })
1222 .drop_without_applying_deltas();
1223 assert!(
1224 chart.state.price_scale().is_manual(),
1225 "undo restores the range"
1226 );
1227 assert_eq!(chart.state.price_scale().range(), range);
1228 }
1229 }
1230
1231 #[test]
1233 fn configured_mode_follows_the_axis_of_the_main_scale() {
1234 use crate::scales::PriceScaleMode::{Logarithmic, Normal, Percentage};
1235 use crate::widget::test_frames::{chart, frame};
1236 for (left, expected) in [(false, Percentage), (true, Logarithmic)] {
1237 let ctx = egui::Context::default();
1238 let mut chart = chart(&[100.0], &[110.0]);
1239 frame(&ctx, &mut chart, 0.0, Vec::new());
1240 chart.config_mut().show_left_axis = left;
1241 chart.config_mut().show_right_axis = !left;
1242 chart.config_mut().left_axis_scale_mode = Logarithmic;
1243 chart.config_mut().right_axis_scale_mode = Percentage;
1244 frame(&ctx, &mut chart, 0.1, Vec::new());
1245 assert_eq!(chart.state.price_scale().options().mode, expected);
1246 chart.config_mut().left_axis_scale_mode = Normal;
1247 chart.config_mut().right_axis_scale_mode = Normal;
1248 frame(&ctx, &mut chart, 0.2, Vec::new());
1249 assert_eq!(chart.state.price_scale().options().mode, Normal);
1250 }
1251 }
1252
1253 #[test]
1255 fn visible_bars_matches_the_setter_on_initial_frames() {
1256 let start = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
1257 let data = BarData::from_bars(
1258 (0..600)
1259 .map(|i| Bar::new(start + Duration::days(i), 100.0, 200.0, 100.0, 200.0, 1.0))
1260 .collect(),
1261 );
1262 for width in [600.0, 900.0, 1440.0] {
1263 let mut built = Chart::new(data.clone()).visible_bars(50);
1264 let mut set = Chart::new(data.clone());
1265 set.set_visible_bars(50);
1266 let contexts = [egui::Context::default(), egui::Context::default()];
1267 built.set_timeframe(Timeframe::Day1);
1268 set.set_timeframe(Timeframe::Day1);
1269 for frame in 0..3 {
1270 for (chart, ctx) in [(&mut built, &contexts[0]), (&mut set, &contexts[1])] {
1271 let input = egui::RawInput {
1272 screen_rect: Some(egui::Rect::from_min_size(
1273 egui::Pos2::ZERO,
1274 egui::vec2(width, 500.0),
1275 )),
1276 time: Some(frame as f64 * 0.1),
1277 ..Default::default()
1278 };
1279 ctx.run_ui(input, |ui| {
1280 egui::CentralPanel::default().show(ui, |ui| {
1281 chart.show(ui);
1282 });
1283 })
1284 .drop_without_applying_deltas();
1285 }
1286 assert_eq!(
1287 built.state.time_scale().bar_spacing(),
1288 set.state.time_scale().bar_spacing(),
1289 "width {width}, frame {frame}"
1290 );
1291 assert_eq!(
1292 built.state.time_scale().bar_spacing(),
1293 built.get_rendered_price_rect().width() / 50.0
1294 );
1295 assert_eq!(built.get_visible_bars(), set.get_visible_bars());
1296 assert_eq!(built.get_visible_bars(), 49);
1297 assert_eq!(built.get_start_idx(), set.get_start_idx());
1298 assert!(built.get_start_idx() >= 548);
1299 }
1300 built.state.time_scale_mut().set_bar_spacing(20.0);
1302 built.apply_timescale_config(width - 166.0);
1303 assert_eq!(built.state.time_scale().bar_spacing(), 20.0);
1304 }
1305 }
1306}