1use crate::model::{Bar, BarData, TimeScale, next_revision};
8use crate::scales::{PriceScale, PriceScaleMode, PriceScaleOptions, PriceTransform};
9use crate::studies::Indicator;
10use std::sync::Arc;
11#[derive(Debug, Clone, Copy)]
16pub struct ZoomState {
17 pub bar_spacing: f32,
19 pub right_offset: f64,
21 pub price_range: Option<(f64, f64)>,
23}
24
25#[derive(Debug)]
43pub struct ChartState {
44 data: BarData,
46 elements: Option<Arc<[Bar]>>,
48 time_scale: TimeScale,
50 price_scale: PriceScale,
52 zoom_history: Vec<ZoomState>,
54 revision: u64,
56}
57
58impl Clone for ChartState {
59 fn clone(&self) -> Self {
61 Self {
62 data: self.data.clone(),
63 elements: self.elements.clone(),
64 time_scale: self.time_scale.clone(),
65 price_scale: self.price_scale.clone(),
66 zoom_history: self.zoom_history.clone(),
67 revision: next_revision(),
68 }
69 }
70}
71
72impl ChartState {
73 pub fn new(data: BarData) -> Self {
75 let mut time_scale = TimeScale::new();
76 time_scale.set_bar_cnt(data.len());
77
78 Self {
79 data,
80 elements: None,
81 time_scale,
82 price_scale: PriceScale::new(PriceScaleOptions::default()),
83 zoom_history: Vec::new(),
84 revision: next_revision(),
85 }
86 }
87
88 pub fn data(&self) -> &BarData {
90 &self.data
91 }
92
93 pub fn time_scale_mut(&mut self) -> &mut TimeScale {
95 &mut self.time_scale
96 }
97
98 pub fn time_scale(&self) -> &TimeScale {
100 &self.time_scale
101 }
102
103 pub fn revision(&self) -> u64 {
108 self.revision
109 }
110
111 pub fn set_data(&mut self, data: BarData) -> Option<usize> {
115 self.edit_bars(|bars| {
116 let bits = |bar: &Bar| {
118 (
119 bar.time,
120 [bar.open, bar.high, bar.low, bar.close, bar.volume].map(f64::to_bits),
121 )
122 };
123 let first = bars
124 .iter()
125 .zip(&data.bars)
126 .position(|(old, new)| bits(old) != bits(new))
127 .or_else(|| (bars.len() != data.len()).then(|| bars.len().min(data.len())));
128 *bars = data.bars;
129 first
130 })
131 }
132
133 pub(crate) fn edit_bars(
137 &mut self,
138 edit: impl FnOnce(&mut Vec<Bar>) -> Option<usize>,
139 ) -> Option<usize> {
140 let old_len = self.data.len();
141 let first_changed = edit(&mut self.data.bars);
142 if first_changed.is_some_and(|first| first < old_len.saturating_sub(1))
143 || self.data.len() < old_len
144 {
145 self.revision = next_revision();
146 }
147 let shown = self.elements.as_deref().unwrap_or(&self.data.bars);
148 #[cfg(feature = "widget")]
149 self.time_scale
150 .refresh_numeric_positions(shown, self.revision);
151 self.time_scale.set_bar_cnt(shown.len());
152 first_changed
153 }
154
155 pub fn shown(&self) -> &[Bar] {
158 self.elements.as_deref().unwrap_or(&self.data.bars)
159 }
160
161 #[cfg(feature = "widget")]
162 pub(crate) fn shows_elements(&self) -> bool {
163 self.elements.is_some()
164 }
165
166 #[cfg(feature = "widget")]
168 pub(crate) fn set_elements(&mut self, elements: Option<Arc<[Bar]>>, follow: bool) {
169 match (&self.elements, &elements) {
170 (Some(old), Some(new)) if Arc::ptr_eq(old, new) => return,
171 (None, None) => return,
172 _ => {}
173 }
174 let old = self.shown();
175 let new = elements.as_deref().unwrap_or(&self.data.bars);
176 let ts = &self.time_scale;
177 let near_live = ts.is_near_live_edge();
178 let kept = (!old.is_empty() && !new.is_empty()).then(|| {
179 let right = ts.base_idx() as f64 + ts.right_offset_precise();
181 let at = (right.floor().max(0.0) as usize).min(old.len() - 1);
182 let time = old[at].time;
183 let rank = at - old.partition_point(|bar| bar.time < time);
184 let first = new.partition_point(|bar| bar.time < time);
185 let same = new[first..].partition_point(|bar| bar.time == time);
186 let to = (first + rank.min(same.saturating_sub(1))).min(new.len() - 1);
187 let newer = new[new.len() - 1].time > old[old.len() - 1].time;
188 (
189 to as f64 + (right - at as f64) - (new.len() - 1) as f64,
190 newer,
191 )
192 });
193 self.elements = elements;
194 let shown = self.elements.as_deref().unwrap_or(&self.data.bars);
195 self.time_scale
196 .refresh_numeric_positions(shown, self.revision);
197 self.time_scale.set_bar_cnt(shown.len());
198 match kept {
199 Some((_, true)) if follow && near_live => self.time_scale.scroll_to_realtime(),
200 Some(_) if follow && near_live => {}
201 Some((offset, _)) => self.time_scale.set_right_offset(offset),
202 None => {}
203 }
204 }
205
206 pub fn visible_data(&self) -> &[crate::model::Bar] {
209 let (start, end) = self.visible_range();
210 &self.shown()[start..end]
211 }
212
213 pub fn visible_range(&self) -> (usize, usize) {
218 let (start, end) = self.time_scale.visible_logical_range().to_strict_range();
219 let end = end.min(self.shown().len());
220 (start.min(end), end)
221 }
222
223 pub fn price_scale(&self) -> &PriceScale {
225 &self.price_scale
226 }
227
228 pub fn price_scale_mut(&mut self) -> &mut PriceScale {
230 &mut self.price_scale
231 }
232
233 pub fn fit_price_scale<'a>(
237 &mut self,
238 indicators: impl IntoIterator<Item = &'a dyn Indicator>,
239 ) -> PriceTransform {
240 self.fit_price_scale_with_series(
241 indicators,
242 None,
243 true,
244 None,
245 None,
246 std::iter::empty(),
247 std::iter::empty(),
248 )
249 .0
250 }
251
252 pub(crate) fn fit_price_scale_with_series<'a>(
258 &mut self,
259 indicators: impl IntoIterator<Item = &'a dyn Indicator>,
260 main_bars: Option<&[Bar]>,
261 main_visible: bool,
262 autoscale: Option<&crate::model::AutoscaleProvider>,
263 main_range: Option<(f64, f64)>,
264 prices: impl IntoIterator<Item = f64>,
265 series_values: impl IntoIterator<Item = f64>,
266 ) -> (PriceTransform, (f64, f64)) {
267 let mode = self.price_scale.options().mode;
268 let (start, end) = self.visible_range();
269 let visible = main_bars.unwrap_or(&self.shown()[start..end]);
270 let at = start.min(self.shown().len().saturating_sub(1));
273 let base = self.shown()[at..]
274 .iter()
275 .find(|bar| !bar.is_whitespace())
276 .or_else(|| crate::model::last_with_value(&self.shown()[..at]))
277 .map_or(f64::NAN, |bar| bar.close);
278 let transform = PriceTransform::new(mode, base);
279 let (study_start, study_end) = self.time_scale.visible_logical_range().to_strict_range();
281 let study_values = indicators
282 .into_iter()
283 .filter(|study| {
284 study.is_visible()
285 && study.is_overlay()
286 && study.scale() == crate::studies::StudyScale::Main
287 })
288 .flat_map(|study| {
289 #[cfg(not(feature = "drawings"))]
290 let drawing_prices = std::iter::empty();
291 #[cfg(feature = "drawings")]
292 let drawing_prices =
293 crate::studies::plots::drawing_prices(study, study_start..study_end);
294 (0..study.line_count())
295 .filter(|&index| {
296 let plot = study.plot_style(index);
297 plot.visible
298 && !matches!(
299 plot.plot_type,
300 crate::studies::PlotType::BarColor
301 | crate::studies::PlotType::BackgroundColor
302 )
303 })
304 .flat_map(move |index| {
305 let available_end = study
306 .values()
307 .len()
308 .checked_add_signed(study.plot_offset(index))
309 .unwrap_or(0);
310 (study_start..study_end.min(available_end))
311 .filter_map(move |bar| study.plot_value(index, bar))
312 .chain(std::iter::once(0.0).filter(move |_| {
313 matches!(
314 study.plot_style(index).plot_type,
315 crate::studies::PlotType::Histogram
316 | crate::studies::PlotType::Columns
317 | crate::studies::PlotType::Area
318 )
319 }))
320 })
321 .chain(
322 study
323 .candles()
324 .iter()
325 .filter(|candle| {
326 candle.bars.end > study_start
327 && candle.bars.start < study_end
328 && study.plot_style(candle.plot).visible
329 })
330 .flat_map(|candle| [candle.bar.low, candle.bar.high]),
331 )
332 .chain(
333 study
334 .zones()
335 .iter()
336 .filter(|zone| {
337 zone.bars.end > study_start
338 && zone.bars.start < study_end
339 && study.plot_style(zone.plot).visible
340 })
341 .flat_map(|zone| [zone.bottom, zone.top]),
342 )
343 .chain(
344 study
345 .projections()
346 .iter()
347 .filter(|projection| {
348 projection.bars.end > study_start as f64
349 && projection.bars.start < study_end as f64
350 && study.plot_style(projection.plot).visible
351 })
352 .flat_map(|projection| [projection.bottom, projection.top]),
353 )
354 .chain(
355 study
356 .volume_profiles(study_start..study_end)
357 .into_iter()
358 .flat_map(|plot| [plot.profile.low(), plot.profile.high()]),
359 )
360 .chain(drawing_prices)
361 });
362 let main = main_visible
363 .then(|| {
364 let range = main_range.or_else(|| {
365 visible
366 .iter()
367 .flat_map(|bar| [bar.low, bar.high])
368 .chain(
369 visible
370 .iter()
371 .flat_map(|bar| [bar.open, bar.close])
372 .filter(|_| mode == PriceScaleMode::Logarithmic),
373 )
374 .filter(|value| transform.to_scale(*value).is_finite())
375 .fold(None, |range: Option<(f64, f64)>, value| {
376 Some(range.map_or((value, value), |(min, max)| {
377 (min.min(value), max.max(value))
378 }))
379 })
380 });
381 match autoscale {
382 Some(provider) => provider.range(visible, range),
383 None => range,
384 }
385 })
386 .flatten();
387 let (min, max) = main
388 .into_iter()
389 .flat_map(|(min, max)| [min, max])
390 .chain(study_values)
391 .chain(prices)
392 .map(|value| transform.to_scale(value))
393 .chain(series_values)
394 .filter(|value| value.is_finite())
395 .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), value| {
396 (min.min(value), max.max(value))
397 });
398 self.price_scale.rebase(transform);
399 self.price_scale.autoscale(min, max);
400 (transform, (min, max))
401 }
402
403 pub fn push_zoom_state(&mut self) {
405 let (range, t) = (self.price_scale.range(), self.price_scale.transform());
406 let curr_state = ZoomState {
407 bar_spacing: self.time_scale.bar_spacing(),
408 right_offset: self.time_scale.right_offset_precise(),
409 price_range: self
410 .price_scale
411 .is_manual()
412 .then(|| (t.to_price(range.min), t.to_price(range.max))),
413 };
414 self.zoom_history.push(curr_state);
415 }
416
417 pub fn pop_zoom_state(&mut self) -> bool {
419 if let Some(state) = self.zoom_history.pop() {
420 self.time_scale.set_bar_spacing(state.bar_spacing);
421 self.time_scale.set_right_offset(state.right_offset);
422 let t = self.price_scale.transform();
425 let range = state
426 .price_range
427 .map(|(min, max)| (t.to_scale(min), t.to_scale(max)));
428 match range {
429 Some((min, max)) if min.is_finite() && max.is_finite() => {
430 self.price_scale.set_range(min, max)
431 }
432 _ => self.price_scale.reset(),
433 }
434 true
435 } else {
436 false
437 }
438 }
439
440 pub fn clear_zoom_history(&mut self) {
442 self.zoom_history.clear();
443 }
444
445 pub fn has_zoom_history(&self) -> bool {
447 !self.zoom_history.is_empty()
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454 use crate::model::Bar;
455 use chrono::{TimeZone, Utc};
456
457 #[test]
459 #[cfg(feature = "widget")]
460 fn a_new_series_keeps_the_item_at_the_right_edge() {
461 let bars = |minutes: &[i64]| -> Vec<Bar> {
462 minutes
463 .iter()
464 .map(|&minute| {
465 Bar::new(
466 Utc.timestamp_opt(1_700_000_000 + minute * 60, 0).unwrap(),
467 100.0,
468 100.0,
469 100.0,
470 100.0,
471 1.0,
472 )
473 })
474 .collect()
475 };
476 let e: &[i64] = &[1, 3, 3, 3, 6, 9];
477 for (name, old, offset, new, follow, expected) in [
479 ("bars to elements", None, -4.0, Some(e), true, -1.0),
480 ("elements to bars", Some(e), -2.0, None, true, -6.0),
481 (
482 "oldest element dropped",
483 Some(e),
484 -2.0,
485 Some(&[3, 3, 3, 6, 9, 12][..]),
486 true,
487 -3.0,
488 ),
489 (
490 "following off",
491 Some(e),
492 0.5,
493 Some(&[1, 3, 3, 3, 6, 9, 12][..]),
494 false,
495 -0.5,
496 ),
497 (
498 "type change at the live edge",
499 None,
500 0.5,
501 Some(&[1, 3, 6, 9, 9, 9][..]),
502 true,
503 0.5,
504 ),
505 ] {
506 let mut state = ChartState::new(BarData::from_bars(bars(&(0..10).collect::<Vec<_>>())));
507 state.set_elements(old.map(|times| Arc::from(bars(times))), true);
508 state.time_scale_mut().set_right_offset(offset);
509 state.set_elements(new.map(|times| Arc::from(bars(times))), follow);
510 assert_eq!(state.time_scale().right_offset(), expected, "{name}");
511 }
512 }
513
514 #[test]
517 fn flat_data_auto_scales_relative_to_price_level() {
518 for (mode, price) in [
519 (PriceScaleMode::Normal, 100.0),
520 (PriceScaleMode::Logarithmic, 1.0001),
521 ] {
522 let bars = (0..50)
523 .map(|i| {
524 let t = Utc.timestamp_opt(1_700_000_000 + i * 60, 0).unwrap();
525 Bar::new(t, price, price, price, price, 1.0)
526 })
527 .collect();
528 let mut state = ChartState::new(BarData::from_bars(bars));
529 state.time_scale_mut().set_width(800.0);
530 state.price_scale_mut().options_mut().mode = mode;
531 let transform = state.fit_price_scale(std::iter::empty());
532 let range = state.price_scale().range();
533 let (min, max) = (transform.to_price(range.min), transform.to_price(range.max));
534 assert!(min < price && price < max, "{mode:?}: ({min}, {max})");
535 assert!(
537 (price * 1e-4..=price).contains(&(max - min)),
538 "{mode:?}: ({min}, {max})"
539 );
540 }
541 }
542
543 #[test]
547 fn manual_range_keeps_its_prices_while_the_base_moves() {
548 for mode in [PriceScaleMode::Percentage, PriceScaleMode::IndexedTo100] {
549 let bars = (0..300)
550 .map(|i| {
551 let t = Utc.timestamp_opt(1_700_000_000 + i * 60, 0).unwrap();
552 let close = 100.0 + i as f64;
553 Bar::new(t, close, close, close, close, 1.0)
554 })
555 .collect();
556 let mut state = ChartState::new(BarData::from_bars(bars));
557 state.time_scale_mut().set_width(800.0);
558 state.price_scale_mut().options_mut().mode = mode;
559 let y_of_380 = |state: &mut ChartState| {
560 let transform = state.fit_price_scale(std::iter::empty());
561 let map = state.price_scale().map(transform, 0.0, 500.0);
562 map.price_to_y(380.0)
563 };
564 let zoom_in = |state: &mut ChartState| {
565 let range = state.price_scale().range();
566 let centre = (range.min + range.max) / 2.0;
567 state.price_scale_mut().zoom(0.9, centre);
568 };
569 y_of_380(&mut state);
570 zoom_in(&mut state);
571 let y = y_of_380(&mut state);
572 state.push_zoom_state();
573
574 let offset = state.time_scale().right_offset();
575 state.time_scale_mut().set_right_offset(offset - 10.0);
576 let panned = y_of_380(&mut state);
577 assert!(
578 (panned - y).abs() < 0.5,
579 "{mode:?} panned: {panned}, was {y}"
580 );
581
582 zoom_in(&mut state);
583 state.pop_zoom_state();
584 let restored = y_of_380(&mut state);
585 assert!(
586 (restored - y).abs() < 0.5,
587 "{mode:?} restored: {restored}, was {y}"
588 );
589 }
590 }
591
592 #[test]
596 fn visible_range_holds_the_bars_on_screen_past_either_end() {
597 let bars: Vec<_> = (0..300)
598 .map(|i| Bar::from_value(Utc.timestamp_opt(1_700_000_000 + i * 60, 0).unwrap(), 1.0))
599 .collect();
600 let mut state = ChartState::new(BarData::from_bars(bars.clone()));
601 state.time_scale_mut().set_width(800.0);
602 state.time_scale_mut().set_bar_spacing(8.0);
603 for (right, start, end) in [(30.0, 0, 31), (-50.0, 0, 0), (450.0, 300, 300)] {
604 state.time_scale_mut().set_right_offset(right - 299.0);
605 assert_eq!(state.visible_range(), (start, end), "right edge on {right}");
606 assert_eq!(
607 state.visible_data(),
608 &bars[start..end],
609 "right edge on {right}"
610 );
611 }
612 }
613}