1mod options;
4mod paint;
5mod scales;
6
7pub use options::SeriesOptions;
8pub(super) use options::{PriceSeries, SeriesLegend, base_line};
9
10use super::change_marks::ChangeMarks;
11use super::{Chart, ChartElementId, DataOperation, SeriesId};
12use crate::data::FeedState;
13use crate::data::merge::{Merged, Origin, merge, valid};
14use crate::model::{
15 Bar, BarClock, BarData, ChartType, LogicalRange, Marker, MarkerOptions, PriceLine,
16};
17use chrono::{DateTime, Utc};
18use std::collections::BTreeMap;
19use std::num::NonZeroUsize;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum MismatchDirection {
24 Exact,
26 NearestLeft,
28 NearestRight,
30}
31
32#[derive(Debug, Clone, PartialEq)]
35pub struct BarsInRange {
36 pub bars: std::ops::Range<usize>,
38 pub bars_before: f64,
42 pub bars_after: f64,
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
50pub struct PriceLineId(u64);
51
52impl Chart {
53 pub fn take_indicator_settings_request(&mut self) -> Option<SeriesId> {
57 self.indicator_settings_request.take()
58 }
59
60 pub fn set_data(&mut self, series: SeriesId, bars: Vec<Bar>) {
73 if (series == SeriesId::MAIN && self.main_series_present
74 || self.price_series.iter().any(|item| item.id == series))
75 && self.replay.is_some()
76 {
77 self.replay_set_data(series, bars);
78 self.data_set(series);
79 return;
80 }
81 if let Some(at) = self.price_series.iter().position(|item| item.id == series) {
82 let (mut bars, rejected) = clean(bars, &self.clock());
83 retain_bars(&mut bars, self.bar_limit(series));
84 let item = &mut self.price_series[at];
85 item.bars = bars;
86 item.revision = crate::model::next_revision();
87 if let Some(marks) = &mut item.change_marks {
88 marks.clear();
89 }
90 item.feed.rejected_bars += rejected;
91 self.data_set(series);
92 return;
93 }
94 if !self.main_series_present || !holds_bars(series, "set_data") {
95 return;
96 }
97 self.replace_main_data(bars);
98 self.data_set(series);
99 }
100
101 pub(super) fn replace_main_data(&mut self, bars: Vec<Bar>) {
102 let (mut bars, rejected) = clean(bars, &self.clock());
103 retain_bars(&mut bars, self.bar_limit(SeriesId::MAIN));
104 self.feed.rejected_bars += rejected;
105 self.from_ticks.clear();
106 if let Some(marks) = &mut self.change_marks {
107 marks.clear();
108 }
109 self.pending_box_sizes = self
110 .chart_type_params
111 .size_methods()
112 .map(|method| method != crate::model::BoxSizeMethod::Fixed);
113 let before = self.edge();
114 #[cfg(feature = "drawings")]
115 self.anchor_drawing_points();
116 let first_changed = self.state.set_data(BarData::from_bars(bars));
117 #[cfg(feature = "drawings")]
118 self.project_drawing_points();
119 self.bars_changed(before, first_changed);
120 }
121
122 pub fn update(&mut self, series: SeriesId, bar: Bar) {
134 let operation = if self.host_events.data_operations && valid(&bar) {
135 let clock = self.clock();
136 let slot = clock.bar_open(bar.time);
137 let bars = self
138 .replay
139 .as_ref()
140 .and_then(|replay| replay.bars.get(&series))
141 .map_or_else(|| self.bars(series), Vec::as_slice);
142 Some(
143 match bars.binary_search_by_key(&slot, |bar| clock.bar_open(bar.time)) {
144 Ok(_) => DataOperation::Update(bar.time),
145 Err(at) if at == bars.len() => DataOperation::Append(bar.time),
146 Err(_) => DataOperation::Insert(bar.time),
147 },
148 )
149 } else {
150 None
151 };
152 let changed = if (series == SeriesId::MAIN && self.main_series_present
153 || self.price_series.iter().any(|item| item.id == series))
154 && self.replay.is_some()
155 {
156 self.replay_update(series, bar)
157 } else if let Some(at) = self.price_series.iter().position(|item| item.id == series) {
158 let clock = self.clock();
159 let item = &mut self.price_series[at];
160 let len = item.bars.len();
161 let incoming = vec![(bar, Origin::Live)];
162 let before = ChangeMarks::before(&item.bars, &clock, &incoming);
163 let merged = merge(&mut item.bars, &mut BTreeMap::new(), &clock, incoming);
164 item.feed.rejected_bars += merged.rejected;
165 if let Some(marks) = &mut item.change_marks {
166 marks.record(&item.bars, &clock, before);
167 }
168 if merged.first_changed.is_some_and(|first| first + 1 < len) {
169 item.revision = crate::model::next_revision();
170 }
171 let changed = merged.first_changed.is_some();
172 self.enforce_bar_limit(series);
173 changed
174 } else if self.main_series_present && holds_bars(series, "update") {
175 self.apply(vec![(bar, Origin::Live)])
176 .first_changed
177 .is_some()
178 } else {
179 return;
180 };
181 if changed {
182 self.data_updated(series);
183 }
184 if let Some(operation) = operation {
185 self.data_operation(series, operation);
186 }
187 }
188
189 pub fn pop_bars(&mut self, series: SeriesId, count: usize) -> Vec<Bar> {
196 let present = series == SeriesId::MAIN && self.main_series_present
197 || self.price_series.iter().any(|item| item.id == series);
198 let removed = if present && self.replay.is_some() {
199 self.replay_pop(series, count)
200 } else if let Some(item) = self.price_series.iter_mut().find(|item| item.id == series) {
201 item.revision = crate::model::next_revision();
202 item.bars.split_off(item.bars.len().saturating_sub(count))
203 } else if self.main_series_present && holds_bars(series, "pop_bars") {
204 let before = self.edge();
205 #[cfg(feature = "drawings")]
206 self.anchor_drawing_points();
207 let mut removed = Vec::new();
208 let first_changed = self.state.edit_bars(|bars| {
209 let at = bars.len().saturating_sub(count);
210 removed = bars.split_off(at);
211 (!removed.is_empty()).then_some(at)
212 });
213 if let Some(first) = removed.first() {
214 self.from_ticks.split_off(&first.time);
215 }
216 #[cfg(feature = "drawings")]
217 self.project_drawing_points();
218 self.bars_changed(before, first_changed);
219 removed
220 } else {
221 return Vec::new();
222 };
223 if !removed.is_empty() {
224 self.data_updated(series);
225 }
226 removed
227 }
228
229 pub fn set_bar_limit(&mut self, series: SeriesId, limit: Option<NonZeroUsize>) {
242 if !self.bar_series_present(series, "set_bar_limit") {
243 return;
244 }
245 match limit {
246 Some(limit) => {
247 self.bar_limits.insert(series, limit);
248 }
249 None => {
250 self.bar_limits.remove(&series);
251 }
252 }
253 if self.replay.is_some() {
254 self.apply_replay_window();
255 } else if self.enforce_bar_limit(series) > 0 {
256 self.data_updated(series);
257 }
258 }
259
260 pub fn bar_limit(&self, series: SeriesId) -> Option<NonZeroUsize> {
262 self.bar_limits.get(&series).copied()
263 }
264
265 pub fn remove_oldest_bars(&mut self, series: SeriesId, count: usize) -> Vec<Bar> {
272 let removed = self.take_oldest_bars(series, count);
273 if !removed.is_empty() {
274 self.data_updated(series);
275 }
276 removed
277 }
278
279 fn take_oldest_bars(&mut self, series: SeriesId, count: usize) -> Vec<Bar> {
280 if !self.bar_series_present(series, "remove_oldest_bars") || count == 0 {
281 return Vec::new();
282 }
283 let clock = self
284 .timeframe
285 .map_or_else(BarClock::exact, |_| self.bar_clock());
286 let removed = if let Some(replay) = &mut self.replay {
287 let removed = replay.remove_oldest_bars(series, count, &clock);
288 self.apply_replay_window();
289 removed
290 } else if let Some(item) = self.price_series.iter_mut().find(|item| item.id == series) {
291 let count = count.min(item.bars.len());
292 let removed = item.bars.drain(..count).collect();
293 if count > 0 {
294 item.revision = crate::model::next_revision();
295 shrink_bars(&mut item.bars);
296 if let Some(marks) = &mut item.change_marks {
297 marks.clear();
298 }
299 }
300 removed
301 } else {
302 #[cfg(feature = "drawings")]
303 self.anchor_drawing_points();
304 let mut removed = Vec::new();
305 self.state.edit_bars(|bars| {
306 let count = count.min(bars.len());
307 removed = bars.drain(..count).collect();
308 shrink_bars(bars);
309 (count > 0).then_some(0)
310 });
311 if !removed.is_empty() {
312 if let Some(first) = self.state.data().bars.first() {
313 self.from_ticks = self.from_ticks.split_off(&first.time);
314 } else {
315 self.from_ticks.clear();
316 }
317 if let Some(marks) = &mut self.change_marks {
318 marks.clear();
319 }
320 self.pending_from.fill(Some(0));
321 if let Some(start) = &mut self.pending_start_idx {
322 *start = start.saturating_sub(removed.len());
323 }
324 self.scroll_animation = None;
325 }
326 #[cfg(feature = "drawings")]
327 self.project_drawing_points();
328 removed
329 };
330 if series == SeriesId::MAIN && !removed.is_empty() {
331 self.feed.history_complete = false;
332 }
333 removed
334 }
335
336 fn bar_series_present(&self, series: SeriesId, call: &str) -> bool {
337 self.price_series.iter().any(|item| item.id == series)
338 || self.main_series_present && holds_bars(series, call)
339 }
340
341 fn enforce_bar_limit(&mut self, series: SeriesId) -> usize {
342 let count = self
343 .bar_limit(series)
344 .map_or(0, |limit| evicted_bars(self.bars(series).len(), limit));
345 if count > 0 {
346 self.take_oldest_bars(series, count);
347 }
348 count
349 }
350
351 pub fn bars(&self, series: SeriesId) -> &[Bar] {
355 if series == SeriesId::MAIN && self.main_series_present {
356 &self.state.data().bars
357 } else {
358 self.price_series
359 .iter()
360 .find(|item| item.id == series)
361 .map_or(&[], |item| item.bars.as_slice())
362 }
363 }
364
365 pub fn bar_index_at(
368 &self,
369 series: SeriesId,
370 time: DateTime<Utc>,
371 direction: MismatchDirection,
372 ) -> Option<usize> {
373 let bars = self.bars(series);
374 let at = bars.partition_point(|bar| bar.time < time);
375 if bars.get(at).is_some_and(|bar| bar.time == time) {
376 return Some(at);
377 }
378 match direction {
379 MismatchDirection::Exact => None,
380 MismatchDirection::NearestLeft => at.checked_sub(1),
381 MismatchDirection::NearestRight => (at < bars.len()).then_some(at),
382 }
383 }
384
385 pub fn bars_in_logical_range(
395 &self,
396 series: SeriesId,
397 range: LogicalRange,
398 ) -> Option<BarsInRange> {
399 let bars = self.bars(series);
400 let shown = self.state.shown();
401 let (first, last) = (bars.first()?, bars.last()?);
402 let index = |bar: &Bar| crate::model::index_at(shown, bar.time).unwrap_or(f64::NAN);
403 let (left, right) = (f64::from(range.left), f64::from(range.right));
404 shown.first()?;
405 let start = bars.partition_point(|bar| index(bar) < left.round());
406 let end = bars
407 .partition_point(|bar| index(bar) <= right.round())
408 .max(start);
409 let from = if start > 0 && start < end {
410 index(&bars[start])
411 } else {
412 left
413 };
414 let to = if end < bars.len() && start < end {
415 index(&bars[end - 1])
416 } else {
417 right
418 };
419 Some(BarsInRange {
420 bars: start..end,
421 bars_before: from - index(first),
422 bars_after: index(last) - to,
423 })
424 }
425
426 pub fn set_markers(&mut self, series: SeriesId, markers: Vec<Marker>) {
430 if let Some(item) = self.price_series.iter_mut().find(|item| item.id == series) {
431 item.markers = markers.into();
432 } else if self.main_series_present && holds_bars(series, "set_markers") {
433 self.marks = markers.into();
434 }
435 }
436
437 pub fn markers(&self, series: SeriesId) -> &[Marker] {
440 if let Some(item) = self.price_series.iter().find(|item| item.id == series) {
441 &item.markers
442 } else if series == SeriesId::MAIN && self.main_series_present {
443 &self.marks
444 } else {
445 &[]
446 }
447 }
448
449 pub fn marker_options(&self, series: SeriesId) -> Option<MarkerOptions> {
451 if let Some(item) = self.price_series.iter().find(|item| item.id == series) {
452 Some(item.marker_options)
453 } else if series == SeriesId::MAIN && self.main_series_present {
454 Some(self.marker_options)
455 } else {
456 None
457 }
458 }
459
460 pub fn set_marker_options(&mut self, series: SeriesId, options: MarkerOptions) {
465 if let Some(item) = self.price_series.iter_mut().find(|item| item.id == series) {
466 item.marker_options = options;
467 } else if self.main_series_present && holds_bars(series, "set_marker_options") {
468 self.marker_options = options;
469 }
470 }
471
472 pub fn set_price_lines(&mut self, series: SeriesId, lines: Vec<PriceLine>) {
478 let first = self.next_price_line;
479 self.next_price_line += lines.len() as u64;
480 if let Some(stored) = self.price_lines_mut(series, "set_price_lines") {
481 *stored = (first..).map(PriceLineId).zip(lines).collect();
482 }
483 }
484
485 pub fn add_price_line(&mut self, series: SeriesId, line: PriceLine) -> Option<PriceLineId> {
490 let id = PriceLineId(self.next_price_line);
491 self.price_lines_mut(series, "add_price_line")?
492 .push((id, line));
493 self.next_price_line += 1;
494 Some(id)
495 }
496
497 pub fn price_lines(&self, series: SeriesId) -> impl Iterator<Item = (PriceLineId, &PriceLine)> {
500 let lines = self
501 .price_series
502 .iter()
503 .find(|item| item.id == series)
504 .map(|item| item.lines.as_slice())
505 .or_else(|| {
506 (series == SeriesId::MAIN && self.main_series_present)
507 .then_some(self.price_lines.as_slice())
508 })
509 .unwrap_or_default();
510 lines.iter().map(|(id, line)| (*id, line))
511 }
512
513 pub fn price_line(&self, id: PriceLineId) -> Option<&PriceLine> {
516 std::iter::once(&self.price_lines)
517 .chain(self.price_series.iter().map(|item| &item.lines))
518 .flatten()
519 .find_map(|(line_id, line)| (*line_id == id).then_some(line))
520 }
521
522 pub fn update_price_line(&mut self, id: PriceLineId, line: PriceLine) -> bool {
525 let stored = std::iter::once(&mut self.price_lines)
526 .chain(self.price_series.iter_mut().map(|item| &mut item.lines))
527 .flatten()
528 .find(|(line_id, _)| *line_id == id);
529 stored.map(|(_, stored)| *stored = line).is_some()
530 }
531
532 pub fn remove_price_line(&mut self, id: PriceLineId) -> bool {
534 std::iter::once(&mut self.price_lines)
535 .chain(self.price_series.iter_mut().map(|item| &mut item.lines))
536 .any(|lines| {
537 let len = lines.len();
538 lines.retain(|(line_id, _)| *line_id != id);
539 lines.len() < len
540 })
541 }
542
543 fn price_lines_mut(
544 &mut self,
545 series: SeriesId,
546 call: &str,
547 ) -> Option<&mut Vec<(PriceLineId, PriceLine)>> {
548 if let Some(item) = self.price_series.iter_mut().find(|item| item.id == series) {
549 Some(&mut item.lines)
550 } else if self.main_series_present && holds_bars(series, call) {
551 Some(&mut self.price_lines)
552 } else {
553 None
554 }
555 }
556
557 pub fn feed_state(&self, series: SeriesId) -> Option<&FeedState> {
560 if series == SeriesId::MAIN && self.main_series_present {
561 Some(&self.feed)
562 } else {
563 self.price_series
564 .iter()
565 .find(|item| item.id == series)
566 .map(|item| &item.feed)
567 }
568 }
569
570 pub(crate) fn apply(&mut self, incoming: Vec<(Bar, Origin)>) -> Merged {
573 let clock = self.clock();
574 self.apply_by(&clock, incoming)
575 }
576
577 pub(super) fn apply_by(&mut self, clock: &BarClock, incoming: Vec<(Bar, Origin)>) -> Merged {
579 let before = self.edge();
580 let tail = before.tail.as_ref().map(|bar| bar.time);
581 #[cfg(feature = "drawings")]
582 self.anchor_drawing_points();
583 let from_ticks = &mut self.from_ticks;
584 let closes = self
585 .change_marks
586 .as_ref()
587 .map(|_| ChangeMarks::before(&self.state.data().bars, clock, &incoming));
588 let mut merged = Merged::default();
589 self.state.edit_bars(|bars| {
590 merged = merge(bars, from_ticks, clock, incoming);
591 merged.first_changed
592 });
593 if let (Some(marks), Some(closes)) = (&mut self.change_marks, closes) {
594 marks.record(&self.state.data().bars, clock, closes);
595 }
596 self.feed.rejected_bars += merged.rejected;
597 #[cfg(feature = "drawings")]
598 self.project_drawing_points();
599 if !self.bars_changed(before, merged.first_changed)
600 && !self.state.shows_elements()
601 && let Some(tail) = tail
602 {
603 let bars = &self.state.data().bars;
607 let tail = clock.bar_open(tail);
608 let appended =
609 bars.len() - bars.partition_point(|bar| clock.bar_open(bar.time) <= tail);
610 let time_scale = self.state.time_scale_mut();
611 time_scale.set_right_offset(time_scale.right_offset_precise() - appended as f64);
612 }
613 if self.enforce_bar_limit(SeriesId::MAIN) > 0 {
614 merged.first_changed = Some(0);
615 }
616 merged
617 }
618
619 pub fn remove_series(&mut self, id: SeriesId) -> bool {
626 if id == SeriesId::MAIN {
627 if !self.main_series_present {
628 return false;
629 }
630 self.set_main_series_visible(false);
631 self.remove_main_series();
632 return true;
633 }
634 if let Some(at) = self.price_series.iter().position(|item| item.id == id) {
635 let removed = self.price_series.remove(at);
636 self.retain_removed_series(removed);
637 self.host_events.push(super::ChartEvent::SeriesRemoved(id));
638 if self.selection.selected_id() == Some(ChartElementId::Series(id)) {
639 self.selection.deselect();
640 }
641 self.prune_series_scales();
642 return true;
643 }
644 let Some(indicator) = self.take_study(id) else {
645 return false;
646 };
647 self.retain_removed_study(id, indicator);
648 true
649 }
650
651 pub fn set_main_series_visible(&mut self, visible: bool) {
654 self.main_series_visible = visible && self.main_series_present;
655 if !visible
656 && matches!(
657 self.selection.selected_id(),
658 Some(ChartElementId::Series(_))
659 )
660 {
661 self.selection.deselect();
662 }
663 }
664
665 pub fn is_main_series_visible(&self) -> bool {
667 self.main_series_visible
668 }
669
670 fn bars_changed(&mut self, before: Edge, first_changed: Option<usize>) -> bool {
675 if let Some(from) = first_changed {
676 for pending in &mut self.pending_from {
677 *pending = Some(pending.map_or(from, |pending| pending.min(from)));
678 }
679 }
680 self.follow_live_edge(before)
681 }
682
683 fn edge(&self) -> Edge {
684 let bars = &self.state.data().bars;
685 Edge {
686 near_live: self.state.time_scale().is_near_live_edge(),
689 len: bars.len(),
690 tail: bars.last().cloned(),
691 }
692 }
693
694 pub(super) fn show_elements(&mut self) {
696 #[cfg(feature = "drawings")]
697 self.anchor_drawing_points();
698 self.chart_type_params
699 .resolve_sizes(&self.state.data().bars, &mut self.pending_box_sizes);
700 let cutoff = self.replay_time();
702 let ranges = &mut self.execution_ranges;
703 let executions = self
704 .feed_link
705 .as_ref()
706 .filter(|_| self.chart_type == ChartType::Range)
707 .and_then(|link| {
708 ranges.bars(
709 &self.chart_type_params,
710 link.trades.revision(),
711 link.trades.iter(),
712 cutoff,
713 )
714 });
715 let built = executions.or_else(|| {
716 crate::chart::rendering::candles::price_based::elements(
717 self.chart_type,
718 &self.chart_type_params,
719 &self.state.data().bars,
720 self.state.revision(),
721 )
722 });
723 self.older_bars_drawn = built.as_ref().is_none_or(|(_, trimmed)| !trimmed);
724 if self.chart_type.is_time_independent() {
725 self.element_studies.sync(
726 &self.studies,
727 built
728 .as_ref()
729 .map_or_else(|| std::sync::Arc::from([]), |(bars, _)| bars.clone()),
730 self.timeframe,
731 );
732 }
733 let follow = self.chart_options.time_scale.shift_visible_range_on_new_bar;
734 self.state
735 .set_elements(built.map(|(elements, _)| elements), follow);
736 #[cfg(feature = "drawings")]
737 self.project_drawing_points();
738 }
739
740 fn follow_live_edge(&mut self, before: Edge) -> bool {
744 let bars = &self.state.data().bars;
745 let tail = before.tail.as_ref().zip(bars.last());
746 let appended =
750 bars.len() > before.len && tail.is_some_and(|(old, new)| new.time > old.time);
751 let whitespace_replaced =
752 tail.is_some_and(|(old, new)| old.is_whitespace() && !new.is_whitespace());
753 let options = &self.chart_options.time_scale;
754 let shift_for_whitespace = whitespace_replaced
755 && !appended
756 && options.allow_shift_visible_range_on_whitespace_replacement;
757 let follow = options.shift_visible_range_on_new_bar
758 && before.near_live
759 && (appended || shift_for_whitespace);
760 if follow {
761 self.state.time_scale_mut().scroll_to_realtime();
762 self.pending_start_idx = None;
764 }
765 follow
766 }
767}
768
769struct Edge {
771 near_live: bool,
772 len: usize,
773 tail: Option<Bar>,
774}
775
776pub(super) fn clean(bars: Vec<Bar>, clock: &BarClock) -> (Vec<Bar>, u64) {
779 let slot = |bar: &Bar| clock.bar_open(bar.time);
781 if bars.windows(2).all(|pair| slot(&pair[0]) < slot(&pair[1])) && bars.iter().all(valid) {
782 return (bars, 0);
783 }
784 let mut clean = Vec::with_capacity(bars.len());
785 let incoming = bars.into_iter().map(|bar| (bar, Origin::History)).collect();
786 let merged = merge(&mut clean, &mut BTreeMap::new(), clock, incoming);
787 (clean, merged.rejected)
788}
789
790pub(super) fn retain_bars(bars: &mut Vec<Bar>, limit: Option<NonZeroUsize>) {
793 if let Some(limit) = limit {
794 let count = evicted_bars(bars.len(), limit);
795 if count > 0 {
796 bars.drain(..count);
797 shrink_bars(bars);
798 }
799 }
800}
801
802pub(super) fn kept_bars(limit: NonZeroUsize) -> usize {
806 limit.get() - limit.get() / 8
807}
808
809pub(super) fn evicted_bars(len: usize, limit: NonZeroUsize) -> usize {
812 if len > limit.get() {
813 len - kept_bars(limit)
814 } else {
815 0
816 }
817}
818
819pub(super) fn shrink_bars(bars: &mut Vec<Bar>) {
820 if bars.capacity() / 2 > bars.len() {
821 bars.shrink_to_fit();
822 }
823}
824
825pub(super) fn holds_bars(series: SeriesId, call: &str) -> bool {
827 if series != SeriesId::MAIN {
828 log::warn!("Chart::{call} ignored: {series:?} is not a present bar series");
829 }
830 series == SeriesId::MAIN
831}
832
833#[cfg(all(test, feature = "studies"))]
834mod tests;