Skip to main content

egui_charts/widget/series/
mod.rs

1//! The chart's bar series, merged by one set of rules for history and live updates.
2
3mod 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/// Which bar [`Chart::bar_index_at`] finds for a time no bar opens at.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum MismatchDirection {
24    /// Only a bar opened at the time.
25    Exact,
26    /// The latest bar opened before the time.
27    NearestLeft,
28    /// The earliest bar opened after the time.
29    NearestRight,
30}
31
32/// The bars of a series inside a range of logical indices, from
33/// [`Chart::bars_in_logical_range`].
34#[derive(Debug, Clone, PartialEq)]
35pub struct BarsInRange {
36    /// Indices into [`Chart::bars`] of the bars inside the range, empty when none is.
37    pub bars: std::ops::Range<usize>,
38    /// The logical distance from the series' first bar to the first selected bar. If no bars
39    /// are selected, or the series' first bar is selected, measured to the unrounded left
40    /// edge instead. Negative when that edge lies before the first bar.
41    pub bars_before: f64,
42    /// The logical distance from the last selected bar to the series' last bar. If no bars
43    /// are selected, or the series' last bar is selected, measured from the unrounded right
44    /// edge instead. Negative when that edge lies after the last bar.
45    pub bars_after: f64,
46}
47
48/// Handle of a price line added with [`Chart::add_price_line`].
49#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
50pub struct PriceLineId(u64);
51
52impl Chart {
53    /// Take the settings request from a legend button or a double-click in a pane: a study, a
54    /// bar series shown in a pane other than the main one, or [`SeriesId::MAIN`] from the main
55    /// series' legend button.
56    pub fn take_indicator_settings_request(&mut self) -> Option<SeriesId> {
57        self.indicator_settings_request.take()
58    }
59
60    /// Replaces the bars of `series`, for history or a replay window. Invalid bars (a non-finite
61    /// value, a high or low on the wrong side of the open and close, negative volume) are dropped
62    /// and counted in [`feed_state`](Self::feed_state), except [whitespace](Bar::whitespace) bars,
63    /// which keep their slots empty. The rest are sorted by time, and of several
64    /// bars with one open time, or in one slot of the timeframe for a chart built with a feed, the
65    /// last is kept. Drawings retain their timestamps across replacements and interval changes.
66    /// The studies are updated from the first bar that changed on the next [`show`](Self::show),
67    /// and near the live edge the view follows a new last bar, as with [`update`](Self::update).
68    ///
69    /// Bar series accept data; unknown ids and study ids are ignored with a warning.
70    /// See [`set_data_operation_notifications`](Self::set_data_operation_notifications) to
71    /// observe every accepted call, including empty and unchanged replacements.
72    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    /// Merges `bar` into the bars of `series` as [`Sink::push_bar`](crate::data::Sink::push_bar)
123    /// does, matched by open time, or by the slot of the timeframe for a chart built with a feed:
124    /// a bar that matches a stored bar replaces it, a newer one is appended and an older one is
125    /// inserted in order. An invalid bar is dropped and counted in
126    /// [`feed_state`](Self::feed_state). Drawings move with their bars, studies follow them on the
127    /// next [`show`](Self::show), and near the live edge the view follows a new last bar. A merged
128    /// bar is reported by [`ChartEvent::DataUpdated`](super::ChartEvent::DataUpdated).
129    ///
130    /// Bar series accept data; unknown ids and study ids are ignored with a warning.
131    /// See [`set_data_operation_notifications`](Self::set_data_operation_notifications) to
132    /// observe every accepted call, including unchanged updates.
133    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    /// Removes the last `count` bars of `series`, or all of them for a larger count, and returns
190    /// them oldest first, leaving the other bars in place. Drawings, studies and the view follow
191    /// as for [`update`](Self::update), and [`ChartEvent::DataUpdated`](super::ChartEvent::DataUpdated)
192    /// reports the removal. During a replay the bars are removed from the retained history.
193    ///
194    /// Bar series accept this; unknown ids and study ids are ignored with a warning.
195    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    /// Keeps at most `limit` bars of `series`. When it holds more, now or after history loads
230    /// and updates, the oldest are evicted down to `limit - limit / 8`, so a series that
231    /// has reached its limit holds between seven eighths of it and all of it. `None` (the
232    /// default) keeps all bars. The limit survives symbol and timeframe changes and also bounds
233    /// retained replay history. Removed data must be supplied again by the host to recover it.
234    /// Feed scroll-back stops once the main series holds `limit - limit / 8` bars, the number an
235    /// eviction keeps; raising or clearing the limit allows paging again.
236    ///
237    /// Each eviction restarts the studies on the retained window, including their warm-up, so a
238    /// stream of new bars restarts them once per `limit / 8 + 1` bars. Drawing timestamps
239    /// are kept, even for evicted bars; markers and raw trades have independent lifetimes.
240    /// Unknown ids and study ids are ignored with a warning.
241    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    /// The retained-bar limit of `series`, or `None` when unlimited or absent.
261    pub fn bar_limit(&self, series: SeriesId) -> Option<NonZeroUsize> {
262        self.bar_limits.get(&series).copied()
263    }
264
265    /// Removes up to `count` oldest bars of `series`, returning them oldest first. Studies
266    /// restart on the retained window; drawings keep their timestamps and the view keeps its
267    /// time position where possible. During replay this removes from the full retained history
268    /// and its matching finer-source slots.
269    /// Emits [`ChartEvent::DataUpdated`](super::ChartEvent::DataUpdated) when bars are removed.
270    /// Unknown ids and study ids are ignored with a warning.
271    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    /// The bars of `series`, oldest first, with their colours and payloads: `bars(id).get(index)`
352    /// is the bar at an index and `bars(id).last()` the last bar, which can be a
353    /// [whitespace](Bar::whitespace) bar. Removed series, volume and studies hold no bars.
354    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    /// The index in [`bars`](Self::bars) of the bar of `series` opened at `time`, or else of its
366    /// nearest neighbour in `direction`. `None` when there is no such bar.
367    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    /// The bars of `series` inside `range`, a range of logical indices of the time scale such as
386    /// [`visible_logical_range`](Self::visible_logical_range) returns, with the distances to the
387    /// ends of the series, for example to request older history before the user scrolls to its
388    /// start. A bar's logical index is the position of its open time on the time scale, so a
389    /// series that skips positions counts them. The range's edges are rounded to whole indices
390    /// to select the bars. An empty intersection returns an empty slice at the insertion point,
391    /// with `bars_before = range.left - first_index` and `bars_after = last_index - range.right`.
392    /// Distances at either end of the series also retain the unrounded requested edge.
393    /// `None` for a series without bars or a chart without a time scale.
394    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    /// Replaces the markers drawn on the bars of `series` (trade signals, events).
427    ///
428    /// Bar series accept data; unknown ids and study ids are ignored with a warning.
429    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    /// The markers of `series` from [`set_markers`](Self::set_markers), without those of a
438    /// [mark source](Self::set_mark_source). Empty for ids that hold no bars.
439    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    /// Marker fitting and layering of a bar series, or `None` for an absent series.
450    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    /// Sets whether the markers of `series` widen its autoscaled price range and where they
461    /// are painted. The main series' options also apply to the bar marks of a mark source.
462    ///
463    /// Bar series accept options; unknown ids and study ids are ignored with a warning.
464    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    /// Replaces the price lines drawn on the scale of `series`. Handles of its previous lines,
473    /// from [`add_price_line`](Self::add_price_line), no longer refer to a line.
474    ///
475    /// Every bar series accepts price lines. The price
476    /// scale does not fit to them: a line whose price is off the scale or not finite is not drawn.
477    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    /// Adds `line` to the price lines drawn on the scale of `series` and returns its handle,
486    /// with which [`update_price_line`](Self::update_price_line) moves or restyles it and
487    /// [`remove_price_line`](Self::remove_price_line) removes it. `None` when `series` holds no
488    /// bars, as with [`set_price_lines`](Self::set_price_lines).
489    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    /// Price lines of a bar series in drawing order, with their handles for editing.
498    /// Unknown ids and study ids yield no lines.
499    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    /// The price line of `id`, or `None` once it is removed, replaced by
514    /// [`set_price_lines`](Self::set_price_lines) or its series is removed.
515    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    /// Replaces the price line of `id`, keeping its place in the drawing order. Returns `false`
523    /// when `id` no longer refers to a line.
524    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    /// Removes the price line of `id`. Returns `false` when `id` no longer refers to a line.
533    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    /// What the chart knows about the feed of `series`, such as the bars dropped as invalid.
558    /// `Some` for every present bar series.
559    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    /// Merges `incoming` into the main bars by the rules of each bar's origin and counts the
571    /// invalid ones in the feed state.
572    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    /// [`apply`](Self::apply) with the slots of `clock`, the chart's.
578    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            // The right edge is the last bar plus the right offset: without this a view scrolled
604            // back would move one bar left per appended bar. A bar stamped later in the slot of
605            // the last bar replaces it and appends nothing.
606            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    /// Removes a bar series or study and clears its selection. Removing the main series pauses
620    /// its feed and retains the time axis for the remaining series and drawings. Undo of the
621    /// [`user_action`](Self::user_action) that removed a bar series restores it with its
622    /// markers, price lines, formatter and bar limit, and for the main series the feed follows
623    /// [`set_feed_paused`](Self::set_feed_paused) again. Returns `false` for an unknown or
624    /// already removed id.
625    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    /// Show or hide the main price series, its volume and its legend without unloading its bars.
652    /// Studies and drawings remain visible and continue to receive data.
653    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    /// Whether the main price series is visible.
666    pub fn is_main_series_visible(&self) -> bool {
667        self.main_series_visible
668    }
669
670    /// Keeps studies and the view in step after the main bars changed. `first_changed` is the
671    /// index of the first bar inserted, changed or removed. The studies are updated once on the
672    /// next frame, for all changes since the previous one. Returns whether the view followed the
673    /// live edge.
674    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            // Within ~1.5 bars of the right edge. With fix_right_edge the right offset is clamped
687            // to <= 0, so near means no farther than 1.5 bars to the left of the edge.
688            near_live: self.state.time_scale().is_near_live_edge(),
689            len: bars.len(),
690            tail: bars.last().cloned(),
691        }
692    }
693
694    /// Shows the cached elements before input, keeping the view in the series' slots.
695    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        // Range bars come from the held trades, as of the replay cutoff, while there are any.
701        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    /// Scrolls to the live edge when the view was near it and a bar was appended, or a
741    /// whitespace tail gained a value while the options allow shifting on whitespace
742    /// replacement. Returns whether it did.
743    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        // Appended only if the last bar moved forward in time: older bars loaded in front make
747        // the series longer and keep its last bar. They need no change of the right offset
748        // either, since the bar indices and the base index both grow by their count.
749        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            // A pending start index would move the view away from the edge again.
763            self.pending_start_idx = None;
764        }
765        follow
766    }
767}
768
769/// The bars and the view before the bars change, for [`Chart::bars_changed`].
770struct Edge {
771    near_live: bool,
772    len: usize,
773    tail: Option<Bar>,
774}
775
776/// `bars` without the invalid ones, sorted by time, with the last of several bars of one slot of
777/// `clock`; and the number of invalid bars dropped.
778pub(super) fn clean(bars: Vec<Bar>, clock: &BarClock) -> (Vec<Bar>, u64) {
779    // Bars already in order pass as they are, without the copy through the merge.
780    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
790/// Evicts the oldest bars [`evicted_bars`] counts and releases excessive capacity after a large
791/// import.
792pub(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
802/// The bars a series keeps when it exceeds `limit`: `limit - limit / 8`, seven eighths of it
803/// rounded up. Evicting an eighth of the limit at once moves the bars and restarts the studies
804/// once per `limit / 8 + 1` new bars instead of on every bar.
805pub(super) fn kept_bars(limit: NonZeroUsize) -> usize {
806    limit.get() - limit.get() / 8
807}
808
809/// How many of the oldest of `len` bars `limit` evicts: none within the limit, otherwise all
810/// but the newest [`kept_bars`].
811pub(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
825/// Warn about an id that was not handled as a secondary bar series.
826pub(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;