1use super::{Chart, ChartLayoutMode, PaneId, SeriesOptions};
5use crate::drawings::persistence::{DrawingStorage, SaveError, SkippedDrawing};
6use crate::model::{ChartType, ChartTypeParams, Marker, MarkerOptions, PriceLine, Timeframe};
7use crate::studies::{IndicatorFactory, StudyTemplate};
8use crate::widget::settings::{ChartSettingsState, WatermarkMode};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12#[derive(Clone, Debug, Serialize, Deserialize)]
14pub struct SavedStudy {
15 pub id: usize,
17 pub study: StudyTemplate,
19 pub caption: String,
21 pub autoscale: bool,
23 pub pane: usize,
25}
26
27#[derive(Clone, Debug, Default, Serialize, Deserialize)]
29pub struct SavedAnnotations {
30 pub markers: Vec<Marker>,
32 pub options: MarkerOptions,
34 pub price_lines: Vec<PriceLine>,
36}
37
38#[derive(Clone, Debug, Serialize, Deserialize)]
43pub struct SavedSeries {
44 pub id: usize,
46 pub options: SeriesOptions,
49 pub visible: bool,
51 #[serde(default)]
54 pub fed: bool,
55 #[serde(default)]
57 pub price_scale: Option<crate::scales::PriceScale>,
58 pub pane: usize,
60 pub annotations: SavedAnnotations,
62}
63
64#[derive(Clone, Debug, Serialize, Deserialize)]
66pub struct SavedPane {
67 pub main: bool,
69 pub stretch: f32,
71 #[serde(default)]
73 pub collapsed: Option<bool>,
74 #[serde(default)]
76 pub preserve: bool,
77 #[serde(default)]
79 pub members: Vec<usize>,
80 #[serde(default)]
82 pub scale: Option<crate::scales::PriceScale>,
83}
84
85#[derive(Clone, Debug, Serialize, Deserialize)]
90pub struct ChartSnapshot {
91 pub symbol: String,
93 pub interval: Option<Timeframe>,
95 pub extended_hours: bool,
97 pub chart_type: ChartType,
99 pub chart_type_params: ChartTypeParams,
101 pub main_series_visible: bool,
103 #[serde(default)]
105 pub main_series_alerts: crate::model::SeriesAlerts,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub main_series_display_format: Option<crate::model::SeriesDisplayFormat>,
109 pub settings: ChartSettingsState,
111 #[serde(default)]
113 pub config: Option<crate::config::ChartConfig>,
114 #[serde(default)]
117 pub palette: Option<crate::config::ChartConfig>,
118 #[serde(default)]
120 pub options: Option<crate::config::ChartOptions>,
121 #[serde(default)]
123 pub price_scale: Option<crate::scales::PriceScale>,
124 #[serde(default)]
126 pub maximized: Option<usize>,
127 pub studies: Vec<SavedStudy>,
129 #[serde(default)]
132 pub series: Option<Vec<SavedSeries>>,
133 #[serde(default)]
135 pub main_annotations: Option<SavedAnnotations>,
136 pub panes: Vec<SavedPane>,
138 pub bar_spacing: f32,
140 pub right_offset: f32,
142 pub drawings: Option<serde_json::Value>,
145}
146
147#[derive(Debug)]
149#[non_exhaustive]
150pub enum RestoreError {
151 Study {
153 index: usize,
155 error: crate::studies::StudyError,
157 },
158 Settings(super::settings::ChartSettingsError),
160 Layout(&'static str),
162 Drawings(crate::drawings::persistence::LoadError),
164}
165
166impl std::fmt::Display for RestoreError {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 match self {
169 Self::Study { index, error } => write!(f, "saved study {index}: {error}"),
170 Self::Settings(error) => write!(f, "{error}"),
171 Self::Layout(reason) => f.write_str(reason),
172 Self::Drawings(error) => write!(f, "saved drawings: {error}"),
173 }
174 }
175}
176impl std::error::Error for RestoreError {
177 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
178 match self {
179 Self::Study { error, .. } => Some(error),
180 Self::Settings(error) => Some(error),
181 Self::Drawings(error) => Some(error),
182 Self::Layout(_) => None,
183 }
184 }
185}
186
187impl Chart {
188 pub fn snapshot(&self) -> Result<ChartSnapshot, SaveError> {
192 self.snapshot_with(&IndicatorFactory::new())
193 }
194
195 pub fn snapshot_with(&self, factory: &IndicatorFactory) -> Result<ChartSnapshot, SaveError> {
198 if self
199 .drawing_store
200 .as_ref()
201 .is_some_and(|store| !store.ready(&self.symbol))
202 {
203 return Err(SaveError::Json(serde::ser::Error::custom(
204 "Drawings are still loading",
205 )));
206 }
207 let drawings =
208 Some(serde_json::to_value(self.save_drawings()?).expect("drawings were validated"));
209 let panes = self.panes();
210 let pane_of = |id| {
211 self.series_pane(id)
212 .and_then(|pane| panes.iter().position(|p| *p == pane))
213 .unwrap_or(0)
214 };
215 let studies: Vec<_> = self
216 .indicators()
217 .enumerate()
218 .map(|(index, (id, study))| SavedStudy {
219 id: id.0,
220 study: StudyTemplate::from_study(study),
221 caption: self.indicator_inputs(id).unwrap_or_default().to_owned(),
222 autoscale: self.study_autoscale[index],
223 pane: pane_of(id),
224 })
225 .collect();
226 let series = self
227 .price_series
228 .iter()
229 .map(|item| SavedSeries {
230 id: item.id.0,
231 options: item.options.clone(),
232 visible: item.visible,
233 fed: self.is_fed(item.id),
234 price_scale: self.series_price_scale(item.id).cloned(),
235 pane: pane_of(item.id),
236 annotations: SavedAnnotations {
237 markers: item.markers.to_vec(),
238 options: item.marker_options,
239 price_lines: item.lines.iter().map(|(_, line)| line.clone()).collect(),
240 },
241 })
242 .collect();
243 let (bar_spacing, right_offset) = self.get_time_scale_state();
244 let snapshot = ChartSnapshot {
245 symbol: self.symbol.clone(),
246 interval: self.timeframe,
247 extended_hours: self.extended_hours,
248 chart_type: self.chart_type,
249 chart_type_params: self.chart_type_params.clone(),
250 main_series_visible: self.main_series_visible,
251 main_series_alerts: self.main_series_style.alerts,
252 main_series_display_format: self.main_series_style.display_format,
253 settings: {
254 let mut settings = ChartSettingsState::from_chart(self);
255 settings.adjust_for_dividends = self.dividend_adjusted();
256 settings
257 },
258 config: Some(self.config.clone()),
259 palette: Some(self.followed_palette()),
260 options: Some(self.chart_options.clone()),
261 price_scale: Some(self.state.price_scale().clone()),
262 maximized: self
263 .pane_layout
264 .maximized
265 .and_then(|id| panes.iter().position(|pane| *pane == id)),
266 studies,
267 series: Some(series),
268 main_annotations: Some(SavedAnnotations {
269 markers: self.markers(super::SeriesId::MAIN).to_vec(),
270 options: self.marker_options,
271 price_lines: self
272 .price_lines
273 .iter()
274 .map(|(_, line)| line.clone())
275 .collect(),
276 }),
277 panes: panes
278 .iter()
279 .map(|id| {
280 let pane = self
281 .pane_layout
282 .panes
283 .iter()
284 .find(|pane| pane.id == *id)
285 .expect("listed pane");
286 SavedPane {
287 main: *id == PaneId::MAIN,
288 stretch: pane.stretch,
289 collapsed: pane.collapsed,
290 preserve: pane.preserve,
291 members: pane.members.iter().map(|id| id.0).collect(),
292 scale: (*id != PaneId::MAIN).then(|| pane.scale.clone()),
293 }
294 })
295 .collect(),
296 bar_spacing,
297 right_offset,
298 drawings,
299 };
300 check(&snapshot, factory)
302 .map_err(|error| SaveError::Json(serde::ser::Error::custom(error)))?;
303 Ok(snapshot)
304 }
305
306 pub fn restore(
321 &mut self,
322 snapshot: &ChartSnapshot,
323 ) -> Result<Vec<SkippedDrawing>, RestoreError> {
324 self.restore_with(snapshot, &IndicatorFactory::new())
325 }
326
327 pub fn restore_with(
331 &mut self,
332 snapshot: &ChartSnapshot,
333 factory: &IndicatorFactory,
334 ) -> Result<Vec<SkippedDrawing>, RestoreError> {
335 let (studies, drawings) = check(snapshot, factory)?;
336 self.set_symbol(snapshot.symbol.clone());
337 if let Some(interval) = snapshot.interval {
338 self.set_timeframe(interval);
339 } else {
340 self.timeframe = None;
341 self.drawings.curr_timeframe.clear();
342 }
343 self.set_extended_hours(snapshot.extended_hours);
344 self.set_dividend_adjusted(snapshot.settings.adjust_for_dividends);
345 self.set_chart_type(snapshot.chart_type);
346 self.set_chart_type_params(snapshot.chart_type_params.clone());
347 self.set_main_series_visible(snapshot.main_series_visible);
348 self.main_series_style.alerts = snapshot.main_series_alerts;
349 self.main_series_style.display_format = snapshot.main_series_display_format;
350 snapshot
351 .settings
352 .apply_to_chart(self)
353 .map_err(RestoreError::Settings)?;
354 if let Some(config) = &snapshot.config {
355 self.config = config.clone();
356 if snapshot.settings.watermark.mode == WatermarkMode::SymbolAndDescription {
358 self.config.watermark_text = None;
359 self.config.watermark_description = true;
360 }
361 self.follow_restored_palette(snapshot.palette.as_ref());
362 }
363 if let Some(options) = &snapshot.options {
364 self.set_chart_options(options.clone());
365 }
366 if let Some(scale) = &snapshot.price_scale {
367 *self.state.price_scale_mut() = scale.clone();
368 }
369 let mut replaced: Vec<_> = self.indicators().map(|(id, _)| id).collect();
370 if snapshot.series.is_some() {
371 replaced.extend(self.series().map(|(id, _)| id));
372 }
373 let show_volume = self.config.show_volume;
374 for id in replaced {
375 self.remove_series(id);
376 }
377 self.config.show_volume = show_volume;
378 self.restored_studies.clear();
379 self.pane_layout
380 .panes
381 .retain(|pane| pane.id == PaneId::MAIN || !pane.members.is_empty());
382 let mut panes: HashMap<usize, PaneId> = HashMap::new();
383 for (at, pane) in snapshot.panes.iter().enumerate() {
384 panes.insert(
385 at,
386 if pane.main {
387 PaneId::MAIN
388 } else {
389 self.add_pane()
390 },
391 );
392 }
393 for (saved, study) in snapshot.studies.iter().zip(studies) {
394 let id = self.add_indicator(study);
395 self.set_indicator_inputs(id, saved.caption.clone());
396 self.set_indicator_autoscale(id, saved.autoscale);
397 self.restored_studies.insert(saved.id, id.0);
398 self.move_series_to_pane(id, panes[&saved.pane]);
399 }
400 for saved in snapshot.series.iter().flatten() {
401 let id = if saved.fed {
402 self.add_feed_series(saved.options.clone())
403 } else {
404 self.add_series(saved.options.clone())
405 };
406 self.set_series_visible(id, saved.visible);
407 self.restore_annotations(id, &saved.annotations);
408 self.restored_studies.insert(saved.id, id.0);
409 self.move_series_to_pane(id, panes[&saved.pane]);
410 if let Some(scale) = &saved.price_scale {
411 *self.series_price_scale_mut(id).expect("restored series") = scale.clone();
412 }
413 }
414 if let Some(main) = &snapshot.main_annotations {
415 self.restore_annotations(super::SeriesId::MAIN, main);
416 }
417 for saved in &snapshot.studies {
418 let id = super::SeriesId(self.restored_studies[&saved.id]);
419 for (input, value) in &saved.study.inputs {
420 if let crate::studies::InputValue::Source(crate::studies::StudySource::Study {
421 id: parent,
422 plot,
423 }) = value
424 {
425 let source = crate::studies::StudySource::Study {
426 id: self.restored_studies[parent],
427 plot: *plot,
428 };
429 self.indicator_mut(id)
430 .expect("restored study")
431 .set_input(input, crate::studies::InputValue::Source(source))
432 .expect("validated study source");
433 }
434 }
435 }
436 self.recalculate_studies();
437 for (at, saved) in snapshot.panes.iter().enumerate() {
438 let id = panes[&at];
439 self.move_pane(id, at);
440 self.set_pane_stretch_factor(id, saved.stretch);
441 let pane = self
442 .pane_layout
443 .panes
444 .iter_mut()
445 .find(|pane| pane.id == id)
446 .expect("restored pane");
447 pane.collapsed = saved.collapsed;
448 pane.preserve = saved.preserve;
449 if !saved.members.is_empty() {
450 pane.members.sort_by_key(|id| {
451 saved
452 .members
453 .iter()
454 .position(|old| self.restored_studies.get(old) == Some(&id.0))
455 .unwrap_or(usize::MAX)
456 });
457 }
458 if let Some(scale) = &saved.scale {
459 pane.scale = scale.clone();
460 }
461 }
462 self.pane_layout.maximized = snapshot.maximized.map(|at| panes[&at]);
463 self.apply_synced_time_scale(snapshot.bar_spacing, snapshot.right_offset);
464 let skipped = match drawings {
465 Some(json) => {
466 let skipped = self
467 .load_drawings_remapped(&json, true)
468 .map_err(RestoreError::Drawings)?;
469 self.history.drawing_edits += 1;
470 self.save_restored_drawings();
471 skipped
472 }
473 None => Vec::new(),
474 };
475 self.history.clear();
476 Ok(skipped)
477 }
478}
479
480impl Chart {
481 fn restore_annotations(&mut self, series: super::SeriesId, saved: &SavedAnnotations) {
482 self.set_markers(series, saved.markers.clone());
483 self.set_marker_options(series, saved.options);
484 self.set_price_lines(series, saved.price_lines.clone());
485 }
486
487 pub fn restored_series_id(&self, saved: usize) -> Option<super::SeriesId> {
490 self.restored_studies
491 .get(&saved)
492 .copied()
493 .map(super::SeriesId)
494 }
495}
496
497type Checked = (Vec<Box<dyn crate::studies::Indicator>>, Option<String>);
499
500fn check(snapshot: &ChartSnapshot, factory: &IndicatorFactory) -> Result<Checked, RestoreError> {
502 let studies = snapshot
503 .studies
504 .iter()
505 .enumerate()
506 .map(|(index, saved)| {
507 saved
508 .study
509 .create_with(factory)
510 .map_err(|error| RestoreError::Study { index, error })
511 })
512 .collect::<Result<Vec<_>, _>>()?;
513 check_source_links(&snapshot.studies, &studies)?;
514 snapshot
515 .settings
516 .validate()
517 .map_err(RestoreError::Settings)?;
518 let valid_scale = |scale: &crate::scales::PriceScale| {
519 let range = scale.range();
520 let margins = scale.options().scale_margins;
521 range.min.is_finite()
522 && range.max.is_finite()
523 && range.min < range.max
524 && margins.top.is_finite()
525 && margins.bottom.is_finite()
526 && margins.top >= 0.0
527 && margins.bottom >= 0.0
528 && margins.top + margins.bottom < 1.0
529 && scale.options().tick_marks.max_marks
530 <= crate::scales::PriceMarkGeneratorConfig::MAX_MARKS
531 };
532 if snapshot
533 .price_scale
534 .as_ref()
535 .is_some_and(|scale| !valid_scale(scale))
536 || snapshot.panes.iter().filter(|pane| pane.main).count() != 1
537 || snapshot
538 .studies
539 .iter()
540 .map(|study| study.id)
541 .chain(snapshot.series.iter().flatten().map(|series| series.id))
542 .collect::<std::collections::HashSet<_>>()
543 .len()
544 != snapshot.studies.len() + snapshot.series.iter().flatten().count()
545 || snapshot
546 .series
547 .iter()
548 .flatten()
549 .any(|series| series.pane >= snapshot.panes.len())
550 || snapshot
551 .panes
552 .iter()
553 .filter_map(|pane| pane.scale.as_ref())
554 .chain(snapshot.series.iter().flatten().filter_map(|series| series.price_scale.as_ref()))
555 .any(|scale| !valid_scale(scale))
556 || snapshot
557 .studies
558 .iter()
559 .any(|study| study.pane >= snapshot.panes.len())
560 || snapshot
561 .panes
562 .iter()
563 .any(|pane| !pane.stretch.is_finite() || pane.stretch <= 0.0)
564 || !snapshot
566 .panes
567 .iter()
568 .map(|pane| pane.stretch)
569 .sum::<f32>()
570 .is_finite()
571 || !snapshot.bar_spacing.is_finite()
572 || snapshot.bar_spacing <= 0.0
573 || !snapshot.right_offset.is_finite()
574 || snapshot
575 .maximized
576 .is_some_and(|pane| pane >= snapshot.panes.len())
577 {
578 return Err(RestoreError::Layout("Invalid layout geometry"));
579 }
580 if serde_json::to_value(snapshot)
583 .and_then(serde_json::from_value::<ChartSnapshot>)
584 .is_err()
585 {
586 return Err(RestoreError::Layout("Layout values must be finite"));
587 }
588 if let Some(options) = &snapshot.options
589 && !(options.crosshair.vert_line_width > 0.0 && options.crosshair.horz_line_width > 0.0)
590 {
591 return Err(RestoreError::Layout("Crosshair width must be above zero"));
592 }
593 if let Some(options) = &snapshot.options
595 && [
596 options.time_scale.min_bar_spacing,
597 options.time_scale.max_bar_spacing,
598 ]
599 .into_iter()
600 .any(|spacing| spacing != 0.0 && !(0.01..).contains(&spacing))
601 {
602 return Err(RestoreError::Layout(
603 "Bar spacing limits must be 0 or at least 0.01",
604 ));
605 }
606 let font_sizes = &crate::styles::typography::FONT_SIZES;
607 if snapshot
608 .options
609 .iter()
610 .map(|options| options.tooltip.font_size)
611 .chain(snapshot.config.iter().flat_map(|config| {
612 std::iter::once(config.watermark_font_size).chain(config.axis_font_size)
613 }))
614 .any(|size| !font_sizes.contains(&size))
615 {
616 return Err(RestoreError::Layout("Font sizes must be between 1 and 256"));
617 }
618 let drawings = match &snapshot.drawings {
619 Some(drawings) => {
620 let json = drawings.to_string();
621 let (storage, _) = DrawingStorage::from_json(&json).map_err(RestoreError::Drawings)?;
622 if storage.symbol != snapshot.symbol {
623 return Err(RestoreError::Drawings(
624 crate::drawings::persistence::LoadError::OtherSymbol,
625 ));
626 }
627 Some(json)
628 }
629 _ => None,
630 };
631 Ok((studies, drawings))
632}
633
634fn check_source_links(
638 studies: &[SavedStudy],
639 built: &[Box<dyn crate::studies::Indicator>],
640) -> Result<(), RestoreError> {
641 let mut parents = vec![Vec::new(); studies.len()];
642 for (at, saved) in studies.iter().enumerate() {
643 for (_, value) in &saved.study.inputs {
644 if let crate::studies::InputValue::Source(crate::studies::StudySource::Study {
645 id,
646 plot,
647 }) = value
648 {
649 let parent = studies.iter().position(|other| other.id == *id).ok_or(
650 RestoreError::Study {
651 index: at,
652 error: crate::studies::StudyError::UnknownSourceStudy(*id),
653 },
654 )?;
655 if *plot >= built[parent].line_count() {
656 return Err(RestoreError::Study {
657 index: at,
658 error: crate::studies::StudyError::UnknownSourcePlot {
659 id: *id,
660 plot: *plot,
661 },
662 });
663 }
664 parents[at].push(parent);
665 }
666 }
667 }
668 let mut resolved = vec![false; studies.len()];
670 while let Some(at) = (0..studies.len())
671 .find(|at| !resolved[*at] && parents[*at].iter().all(|parent| resolved[*parent]))
672 {
673 resolved[at] = true;
674 }
675 if let Some(index) = resolved.iter().position(|done| !done) {
676 return Err(RestoreError::Study {
677 index,
678 error: crate::studies::StudyError::CyclicSource,
679 });
680 }
681 Ok(())
682}
683
684#[derive(Clone, Debug, Serialize, Deserialize)]
687pub struct SavedLayout {
688 pub mode: ChartLayoutMode,
690 pub charts: Vec<ChartSnapshot>,
692}
693
694#[derive(Serialize, Deserialize)]
695struct Document<T> {
696 version: u32,
697 #[serde(flatten)]
698 layout: T,
699}
700
701impl SavedLayout {
702 pub const VERSION: u32 = 3;
704
705 pub fn to_json(&self) -> Result<String, serde_json::Error> {
707 serde_json::to_string(&Document {
708 version: Self::VERSION,
709 layout: self,
710 })
711 }
712
713 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
716 let document: Document<serde_json::Value> = serde_json::from_str(json)?;
717 match document.version {
718 1..=Self::VERSION => serde_json::from_value(document.layout),
719 version => Err(serde::de::Error::custom(format!(
720 "unsupported layout version {version}, expected {}",
721 Self::VERSION
722 ))),
723 }
724 }
725}
726
727#[cfg(test)]
728mod tests {
729 use super::*;
730 use crate::drawings::{ChartPoint, Drawing, DrawingToolType};
731 use crate::model::{Bar, BarData, Marker, PriceLine};
732 use crate::studies::{MACD, RSI, SMA};
733 use chrono::{TimeZone, Utc};
734
735 #[test]
739 fn restore_rebuilds_custom_studies_source_links_and_added_series() {
740 use crate::studies::{CustomIndicator, IndicatorFactory, IndicatorValue, StudySource};
741 let bars: Vec<_> = (0..30)
742 .map(|i| {
743 Bar::new(
744 Utc.timestamp_opt(i * 60, 0).unwrap(),
745 10.0,
746 11.0,
747 9.0,
748 10.0,
749 1.0,
750 )
751 })
752 .collect();
753 let mut factory = IndicatorFactory::new();
754 factory.register("Bar range", || {
755 Box::new(CustomIndicator::new("Bar range", |bars| {
756 bars.iter()
757 .map(|bar| IndicatorValue::Single(bar.high - bar.low))
758 .collect()
759 }))
760 });
761 let mut chart = Chart::new(BarData::from_bars(bars.clone()));
762 chart.set_symbol("A");
763 let spread = chart.add_indicator(factory.create("Bar range").unwrap());
764 let rsi = chart.add_indicator(Box::new(RSI::new(5)));
765 chart
766 .set_indicator_source(
767 rsi,
768 StudySource::Study {
769 id: spread.0,
770 plot: 0,
771 },
772 )
773 .unwrap();
774 let extra = chart.add_series(SeriesOptions {
775 name: "Peer".into(),
776 color: egui::Color32::GOLD,
777 ..Default::default()
778 });
779 chart.set_markers(
780 extra,
781 vec![Marker::new(Utc.timestamp_opt(60, 0).unwrap()).with_color(egui::Color32::RED)],
782 );
783 chart.set_price_lines(extra, vec![PriceLine::new(12.5, egui::Color32::BLUE)]);
784 chart.move_series_to_pane(extra, chart.series_pane(rsi).unwrap());
785 assert!(
786 chart.snapshot().is_err(),
787 "the built-in factory cannot rebuild Bar range"
788 );
789 let json = SavedLayout {
790 mode: ChartLayoutMode::Single,
791 charts: vec![chart.snapshot_with(&factory).unwrap()],
792 }
793 .to_json()
794 .unwrap();
795 let snapshot = SavedLayout::from_json(&json).unwrap().charts.remove(0);
796
797 let mut restored = Chart::new(BarData::from_bars(bars));
798 restored.set_symbol("B");
799 assert!(restored.restore(&snapshot).is_err());
800 assert_eq!(restored.symbol(), "B");
801 let mut dangling = snapshot.clone();
802 dangling.studies[1]
803 .study
804 .inputs
805 .iter_mut()
806 .for_each(|(_, value)| {
807 if let crate::studies::InputValue::Source(StudySource::Study { id, .. }) = value {
808 *id = 999;
809 }
810 });
811 assert!(restored.restore_with(&dangling, &factory).is_err());
812 assert_eq!(restored.symbol(), "B");
813
814 restored.restore_with(&snapshot, &factory).unwrap();
815 assert_eq!(restored.symbol(), "A");
816 let names: Vec<_> = restored
817 .indicators()
818 .map(|(_, study)| study.name().to_owned())
819 .collect();
820 assert_eq!(names, ["Bar range", "RSI"]);
821 let new_spread = restored.restored_series_id(spread.0).unwrap();
822 let new_rsi = restored.restored_series_id(rsi.0).unwrap();
823 assert_eq!(
824 restored.indicator_mut(new_rsi).unwrap().source(),
825 Some(StudySource::Study {
826 id: new_spread.0,
827 plot: 0
828 })
829 );
830 let new_extra = restored.restored_series_id(extra.0).unwrap();
831 let (_, options) = restored.series().next().unwrap();
832 assert_eq!(
833 (options.name.as_str(), options.color),
834 ("Peer", egui::Color32::GOLD)
835 );
836 assert_eq!(restored.markers(new_extra)[0].color, egui::Color32::RED);
837 assert_eq!(
838 restored.series_pane(new_extra),
839 restored.series_pane(new_rsi)
840 );
841 let lines = &restored.price_series[0].lines;
842 assert_eq!(lines.len(), 1);
843 assert_eq!(lines[0].1.price, 12.5);
844 }
845
846 #[test]
847 fn layouts_restore_interval_type_studies_panes_view_and_pane_drawings() {
848 let bars: Vec<_> = (0..60)
849 .map(|i| {
850 Bar::new(
851 Utc.timestamp_opt(i * 60, 0).unwrap(),
852 10.0,
853 11.0,
854 9.0,
855 10.0,
856 1.0,
857 )
858 })
859 .collect();
860 let mut chart = Chart::new(BarData::from_bars(bars.clone()));
861 chart.set_symbol("A");
862 chart.set_timeframe(Timeframe::Min5);
863 chart.set_chart_type(ChartType::Line);
864 let sma = chart.add_indicator(Box::new(SMA::new(7)));
865 chart.set_indicator_inputs(sma, "7 close");
866 chart.set_indicator_autoscale(sma, false);
867 let rsi = chart.add_indicator(Box::new(RSI::new(9)));
868 let macd = chart.add_indicator(Box::new(MACD::new(5, 13, 4)));
869 chart
870 .set_indicator_source(
871 rsi,
872 crate::studies::StudySource::Study { id: sma.0, plot: 0 },
873 )
874 .unwrap();
875 chart.recalculate_studies();
876 chart.move_indicator_to_pane(macd, rsi);
877 let pane = chart.series_pane(rsi).unwrap();
878 chart.move_pane(pane, 0);
879 chart.set_pane_stretch_factor(pane, 2.0);
880 let mut options = chart.chart_options().clone();
881 options.time_scale.min_bar_spacing = 0.1;
882 options.time_scale.max_bar_spacing = 12.0;
883 options.time_scale.right_offset = 7.0;
884 options.tooltip.show_volume_details = true;
885 chart.set_chart_options(options);
886 chart.apply_synced_time_scale(0.25, 4.0);
887 let empty = chart.add_pane();
888 chart.set_preserve_empty_pane(empty, true);
889 chart.pane_layout.panes[0].collapsed = Some(true);
890 chart.pane_layout.panes[0].scale.set_range(25.0, 75.0);
891 chart.pane_layout.maximized = Some(pane);
892 chart.price_scale_mut().set_range(8.0, 12.0);
893 chart.config_mut().candle_width = 0.42;
894 chart.config_mut().watermark_text = Some("Saved chart".into());
895 chart.config_mut().grid_line_style = crate::model::LineStyle::LargeDashed;
896 chart.config_mut().vertical_grid_line_style = Some(crate::model::LineStyle::SparseDotted);
897 chart.chart_options.crosshair.mode = crate::config::CrosshairMode::Magnet;
898 chart.chart_options.crosshair.vert_line_style = crate::model::LineStyle::Dashed;
899 chart.chart_options.crosshair.horz_line_style = crate::model::LineStyle::SparseDotted;
900 chart.chart_options.crosshair.vert_label_background_color = egui::Color32::RED;
901 chart.chart_options.crosshair.horz_label_background_color = egui::Color32::BLUE;
902 chart.chart_options.time_scale.timezone =
903 crate::scales::TimezoneMode::Timezone("+01:00".parse().unwrap());
904 let mut drawing = Drawing::new(1, DrawingToolType::HorizontalLine);
905 drawing.chart_points = vec![ChartPoint::new(3.0, 50.0)];
906 drawing.completed = true;
907 drawing.pane = Some(rsi.0);
908 chart.drawings_mut().drawings.push(drawing);
909 let json = SavedLayout {
910 mode: ChartLayoutMode::Single,
911 charts: vec![chart.snapshot().unwrap()],
912 }
913 .to_json()
914 .unwrap();
915
916 let mut restored = Chart::new(BarData::from_bars(bars));
917 restored.add_indicator(Box::new(SMA::new(3)));
918 let layout = SavedLayout::from_json(&json).unwrap();
919 assert!(restored.restore(&layout.charts[0]).unwrap().is_empty());
920 assert_eq!(restored.symbol(), "A");
921 assert_eq!(restored.timeframe(), Some(Timeframe::Min5));
922 assert_eq!(restored.chart_type(), ChartType::Line);
923 let studies: Vec<_> = restored
924 .indicators()
925 .map(|(id, study)| (id, study.name().to_owned(), study.inputs()[0].value.clone()))
926 .collect();
927 let names: Vec<_> = studies.iter().map(|(_, name, _)| name.as_str()).collect();
928 assert_eq!(names, ["SMA", "RSI", "MACD"]);
929 assert_eq!(
930 studies[0].2,
931 chart.indicators().next().unwrap().1.inputs()[0].value
932 );
933 let [(sma, ..), (rsi, ..), (macd, ..)] = studies.as_slice() else {
934 unreachable!()
935 };
936 assert_eq!(restored.indicator_inputs(*sma), Some("7 close"));
937 assert!(!restored.study_autoscale[0]);
938 assert_eq!(
939 restored.indicator_mut(*rsi).unwrap().source(),
940 Some(crate::studies::StudySource::Study { id: sma.0, plot: 0 })
941 );
942 let original = chart.indicators().nth(1).unwrap().1.values();
943 let output = restored.indicators().nth(1).unwrap().1.values();
944 assert!(!output.is_empty());
945 for at in 0..original.len() {
946 assert_eq!(output.value(0, at), original.value(0, at));
947 }
948 let panes = restored.panes();
949 assert_eq!(panes.len(), 3);
950 assert_eq!(panes[1], PaneId::MAIN);
951 assert_eq!(restored.pane_series(panes[0]), [*rsi, *macd]);
952 assert_eq!(restored.pane_stretch_factor(panes[0]), Some(2.0));
953 assert_eq!(restored.get_time_scale_state(), (0.25, 4.0));
954 restored.scroll_to_real_time(false);
955 assert_eq!(restored.get_time_scale_state(), (0.25, 7.0));
956 restored.apply_synced_time_scale(99.0, 4.0);
957 assert_eq!(restored.get_time_scale_state(), (12.0, 4.0));
958 restored.apply_synced_time_scale(0.25, 4.0);
959 assert_eq!(restored.config(), chart.config());
960 assert_eq!(restored.chart_options(), chart.chart_options());
961 assert_eq!(
962 restored.state().price_scale().range(),
963 chart.state().price_scale().range()
964 );
965 assert!(restored.state().price_scale().is_manual());
966 assert_eq!(restored.pane_layout.panes[0].collapsed, Some(true));
967 assert_eq!(
968 restored.pane_layout.panes[0].scale.range(),
969 chart.pane_layout.panes[0].scale.range()
970 );
971 assert_eq!(restored.pane_layout.maximized, Some(panes[0]));
972 assert!(restored.preserves_empty_pane(panes[2]));
973 let drawings = &restored.drawings().drawings;
974 assert_eq!(drawings.len(), 1);
975 assert_eq!(drawings[0].pane, Some(rsi.0));
976 assert_eq!(drawings[0].chart_points[0].bar_idx, 3.0);
977 assert!(!restored.can_undo());
978
979 let mut snapshot = layout.charts[0].clone();
981 snapshot.interval = None;
982 restored.restore(&snapshot).unwrap();
983 assert_eq!(restored.timeframe(), None);
984 for bad in [0.0, f32::NAN, f32::INFINITY] {
985 snapshot.bar_spacing = bad;
986 snapshot.symbol = "Invalid".into();
987 assert!(restored.restore(&snapshot).is_err());
988 assert_eq!(restored.symbol(), "A");
989 }
990 for key in ["price_scale", "panes"] {
991 let mut invalid: serde_json::Value = serde_json::from_str(&json).unwrap();
992 invalid["charts"][0]["symbol"] = "Invalid".into();
993 invalid["charts"][0]["drawings"] = serde_json::Value::Null;
994 let scale = if key == "price_scale" {
995 &mut invalid["charts"][0]["price_scale"]
996 } else {
997 &mut invalid["charts"][0]["panes"][0]["scale"]
998 };
999 scale["options"]["tick_marks"]["max_marks"] = usize::MAX.into();
1000 let invalid = SavedLayout::from_json(&invalid.to_string()).unwrap();
1001 assert!(restored.restore(&invalid.charts[0]).is_err());
1002 assert_eq!(restored.symbol(), "A");
1003 assert_eq!(restored.timeframe(), None);
1004 }
1005 for theme in [crate::theme::Theme::light(), crate::theme::Theme::dark()] {
1007 chart.set_theme(&egui::Context::default(), &theme);
1008 let snapshot = chart.snapshot().unwrap();
1009 restored.restore(&snapshot).unwrap();
1010 assert_eq!(restored.config(), chart.config());
1011 }
1012 let mut legacy: serde_json::Value = serde_json::from_str(&json).unwrap();
1013 legacy["charts"][0]["options"]["tooltip"]
1014 .as_object_mut()
1015 .unwrap()
1016 .remove("show_volume_details");
1017 let config = &mut legacy["charts"][0]["config"];
1018 config["watermark_text"] = "A ยท Apple".into();
1019 config
1020 .as_object_mut()
1021 .unwrap()
1022 .remove("watermark_description");
1023 legacy["charts"][0]["settings"]["watermark"]["mode"] = "SymbolAndDescription".into();
1024 assert!(
1026 legacy["charts"][0]["config"]
1027 .as_object_mut()
1028 .unwrap()
1029 .remove("thousands_sep")
1030 .is_some()
1031 );
1032 for section in ["config", "settings"] {
1033 let value = if section == "settings" {
1034 &mut legacy["charts"][0][section]["status_line"]
1035 } else {
1036 &mut legacy["charts"][0][section]
1037 };
1038 assert!(
1039 value
1040 .as_object_mut()
1041 .unwrap()
1042 .remove("show_market_status")
1043 .is_some()
1044 );
1045 }
1046 restored.config_mut().show_market_status = false;
1047 restored.config_mut().thousands_sep = None;
1048 let migrated = SavedLayout::from_json(&legacy.to_string()).unwrap();
1049 restored.restore(&migrated.charts[0]).unwrap();
1050 assert!(!restored.chart_options().tooltip.show_volume_details);
1051 assert_eq!(restored.config().thousands_sep, Some(','));
1052 assert!(restored.config().show_market_status);
1053 assert_eq!(restored.config().watermark_text, None);
1054 assert!(restored.config().watermark_description);
1055 legacy["version"] = 1.into();
1056 for key in ["config", "options", "price_scale", "maximized"] {
1057 legacy["charts"][0].as_object_mut().unwrap().remove(key);
1058 }
1059 let migrated = SavedLayout::from_json(&legacy.to_string()).unwrap();
1060 restored.restore(&migrated.charts[0]).unwrap();
1061 assert_eq!(restored.chart_type(), ChartType::Line);
1062
1063 let newer = json.replacen("\"version\":3", "\"version\":99", 1);
1064 assert!(SavedLayout::from_json(&newer).is_err());
1065 }
1066
1067 #[test]
1071 fn every_restored_layout_saves_a_layout_that_loads() {
1072 use crate::widget::test_frames::{chart, frame};
1073 let mut original = chart(&[10.0], &[10.0, 11.0]);
1074 original.set_symbol("A");
1075 original.add_indicator(Box::new(RSI::new(9)));
1076 let document = serde_json::to_value(original.snapshot().unwrap()).unwrap();
1077 for (pointer, value) in [
1078 ("/config/candle_width", 1e39),
1079 ("/options/tooltip/magnifier_size", -1e300),
1080 ("/options/crosshair/horz_line_width", 0.0),
1081 ("/options/crosshair/vert_line_width", -1.0),
1082 ("/options/time_scale/max_bar_spacing", 1e-9),
1083 ("/options/tooltip/font_size", 1e5),
1084 ("/config/watermark_font_size", 1e5),
1085 ("/config/axis_font_size", 0.0),
1086 ] {
1087 let mut invalid = document.clone();
1088 *invalid.pointer_mut(pointer).unwrap() = value.into();
1089 let snapshot: ChartSnapshot = serde_json::from_value(invalid).unwrap();
1090 let mut restored = chart(&[10.0], &[11.0]);
1091 restored.set_symbol("B");
1092 assert!(restored.restore(&snapshot).is_err(), "{pointer} {value}");
1093 assert_eq!(restored.symbol(), "B", "{pointer} {value}");
1094 }
1095
1096 original.add_indicator(Box::new(crate::studies::BollingerBands::new(2, f64::MAX)));
1098 frame(&egui::Context::default(), &mut original, 0.0, Vec::new());
1099 let json = SavedLayout {
1100 mode: ChartLayoutMode::Single,
1101 charts: vec![original.snapshot().unwrap()],
1102 }
1103 .to_json()
1104 .unwrap();
1105 let layout = SavedLayout::from_json(&json).unwrap();
1106 chart(&[10.0], &[11.0]).restore(&layout.charts[0]).unwrap();
1107 }
1108}