1use super::{Chart, ChartElementId, SeriesId, VisibleTimeRange};
4use crate::chart::rendering::StudyValue;
5use crate::model::Bar;
6use chrono::{DateTime, Utc};
7use egui::{Modifiers, PointerButton, Pos2, Ui};
8
9#[derive(Debug, Clone)]
12#[non_exhaustive]
13pub struct ChartPointer {
14 pub time: DateTime<Utc>,
17 pub price: f64,
19 pub series: SeriesId,
22 pub index: isize,
25 pub pos: Pos2,
27 pub pane: usize,
30 pub bar: Option<Bar>,
34 pub previous_close: Option<f64>,
37 pub series_bars: Vec<(SeriesId, Bar)>,
40 pub studies: Vec<(SeriesId, StudyValue)>,
43 pub target: Option<PointerTarget>,
45 pub modifiers: Modifiers,
47 pub kind: PointerKind,
49}
50
51impl PartialEq for ChartPointer {
52 fn eq(&self, other: &Self) -> bool {
53 self.time == other.time
54 && self.price == other.price
55 && self.series == other.series
56 && self.index == other.index
57 && self.pos == other.pos
58 && self.pane == other.pane
59 && self.previous_close == other.previous_close
60 && self.studies == other.studies
61 && self.target == other.target
62 && self.modifiers == other.modifiers
63 && self.kind == other.kind
64 && match (&self.bar, &other.bar) {
65 (Some(left), Some(right)) => same_bar(left, right),
66 (None, None) => true,
67 _ => false,
68 }
69 && self.series_bars.len() == other.series_bars.len()
70 && self.series_bars.iter().zip(&other.series_bars).all(
71 |((left_id, left), (right_id, right))| left_id == right_id && same_bar(left, right),
72 )
73 }
74}
75
76fn same_bar(left: &Bar, right: &Bar) -> bool {
77 left == right
79 || (left.is_whitespace()
80 && right.is_whitespace()
81 && left.time == right.time
82 && left.colors == right.colors
83 && left.payload == right.payload)
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88#[non_exhaustive]
89pub enum PointerTarget {
90 Element(ChartElementId),
92 Drawing(usize),
94 Primitive(super::PrimitiveId),
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103#[non_exhaustive]
104pub enum PointerKind {
105 Mouse,
107 Touch,
109 Pen,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116#[non_exhaustive]
117pub enum DataOperation {
118 SetData,
120 Append(DateTime<Utc>),
122 Update(DateTime<Utc>),
124 Insert(DateTime<Utc>),
126}
127
128#[derive(Debug, Clone, PartialEq)]
130#[non_exhaustive]
131pub enum ChartEvent {
132 EventsDropped(usize),
135 VisibleRangeChanged(Option<VisibleTimeRange>),
137 VisibleLogicalRangeChanged(Option<crate::model::LogicalRange>),
140 TimeScaleResized(egui::Vec2),
143 CrosshairMoved(Option<ChartPointer>),
148 Click {
150 position: ChartPointer,
152 button: PointerButton,
154 },
155 DoubleClick {
157 position: ChartPointer,
159 },
160 MarkClick {
163 id: String,
165 series: SeriesId,
167 time: DateTime<Utc>,
169 },
170 #[cfg(feature = "drawings")]
174 #[cfg_attr(docsrs, doc(cfg(feature = "drawings")))]
175 LinkClick {
176 drawing: usize,
178 link: String,
180 },
181 #[cfg(feature = "drawings")]
184 #[cfg_attr(docsrs, doc(cfg(feature = "drawings")))]
185 ImageRequested(ImageRequest),
186 DataLoaded {
189 series: SeriesId,
191 bars: usize,
193 },
194 DataUpdated {
197 series: SeriesId,
199 bars: usize,
201 },
202 DataOperation {
207 series: SeriesId,
209 operation: DataOperation,
211 bars: usize,
213 },
214 SymbolChanged(String),
216 TimeframeChanged(crate::model::Timeframe),
218 SeriesAdded(SeriesId),
225 SeriesRemoved(SeriesId),
230}
231
232#[cfg(feature = "drawings")]
240#[cfg_attr(docsrs, doc(cfg(feature = "drawings")))]
241#[derive(Debug, Clone)]
242#[non_exhaustive]
243pub struct ImageRequest {
244 pub drawing: usize,
246 pub file: Option<egui::DroppedFileHandle>,
248}
249
250#[cfg(feature = "drawings")]
251impl PartialEq for ImageRequest {
252 fn eq(&self, other: &Self) -> bool {
253 self.drawing == other.drawing
254 && match (&self.file, &other.file) {
255 (None, None) => true,
256 (Some(a), Some(b)) => std::sync::Arc::ptr_eq(a, b),
257 _ => false,
258 }
259 }
260}
261
262#[derive(Default)]
263pub(super) struct HostEvents {
264 queue: std::collections::VecDeque<ChartEvent>,
265 dropped: usize,
266 pub(super) data_operations: bool,
267 range: Option<VisibleTimeRange>,
268 logical_range: Option<crate::model::LogicalRange>,
269 time_scale_size: egui::Vec2,
270 pointer: Option<ChartPointer>,
271 kind: Option<PointerKind>,
272 click_frames: [Option<u64>; 3],
273 pub target: Option<PointerTarget>,
275 pub volume_map: Option<crate::scales::PriceMap>,
276 #[cfg(feature = "drawings")]
277 dropped_images: Vec<egui::DroppedFileHandle>,
278}
279
280impl HostEvents {
281 pub(super) fn push(&mut self, event: ChartEvent) {
282 if self.queue.len() == 1024 {
283 self.queue.pop_front();
284 self.dropped = self.dropped.saturating_add(1);
285 }
286 self.queue.push_back(event);
287 }
288}
289
290impl Chart {
291 pub fn set_data_operation_notifications(&mut self, enabled: bool) {
304 self.host_events.data_operations = enabled;
305 }
306
307 pub fn set_pointer_kind(&mut self, kind: Option<PointerKind>) {
311 self.host_events.kind = kind;
312 }
313
314 pub fn drain_events(&mut self) -> impl Iterator<Item = ChartEvent> + '_ {
336 let dropped = std::mem::take(&mut self.host_events.dropped);
337 (dropped > 0)
338 .then_some(ChartEvent::EventsDropped(dropped))
339 .into_iter()
340 .chain(self.host_events.queue.drain(..))
341 }
342
343 pub(super) fn data_loaded(&mut self, series: SeriesId) {
344 self.host_events.push(ChartEvent::DataLoaded {
345 series,
346 bars: self.bars(series).len(),
347 });
348 }
349
350 pub(super) fn data_set(&mut self, series: SeriesId) {
351 self.data_loaded(series);
352 self.data_operation(series, DataOperation::SetData);
353 }
354
355 pub(super) fn data_operation(&mut self, series: SeriesId, operation: DataOperation) {
356 if self.host_events.data_operations {
357 self.host_events.push(ChartEvent::DataOperation {
358 series,
359 operation,
360 bars: self.bars(series).len(),
361 });
362 }
363 }
364
365 pub(super) fn data_updated(&mut self, series: SeriesId) {
366 let bars = self.bars(series).len();
367 if let Some(ChartEvent::DataUpdated {
368 series: last,
369 bars: count,
370 }) = self
371 .host_events
372 .queue
373 .iter_mut()
374 .rev()
375 .find(|event| !matches!(event, ChartEvent::DataOperation { .. }))
376 && *last == series
377 {
378 *count = bars;
379 } else {
380 self.host_events
381 .push(ChartEvent::DataUpdated { series, bars });
382 }
383 }
384
385 fn pointer_at(&self, ui: &Ui, pos: Pos2) -> Option<ChartPointer> {
386 let plot = self.last_rendered_price_rect;
387 let shown = self.state.shown();
388 let coords = self.get_chart_mapping();
389 let selected = self.crosshair_position(pos, &coords)?;
390 let index = coords.x_to_idx_f64(selected.x).round();
391 let time = crate::model::time_at(shown, index)?;
392 let bar_at = (index >= 0.0 && index < shown.len() as f64).then_some(index as usize);
393 let magnet = |map: crate::scales::PriceMap, values: Vec<f64>| {
394 if self.chart_options.crosshair.mode.snaps() {
395 crate::chart::rendering::snap_to_values(pos.y, &map, values)
396 .unwrap_or_else(|| map.y_to_price(pos.y))
397 } else {
398 map.y_to_price(pos.y)
399 }
400 };
401 let pane_index = |id: super::PaneId| {
403 (self.pane_layout.panes.iter())
404 .filter(|pane| pane.id == super::PaneId::MAIN || self.pane_shown(pane))
405 .position(|pane| pane.id == id)
406 .unwrap_or_default()
407 };
408 let (series, price, pane) = if let Some(group) =
409 self.pane_layout.panes.iter().find(|group| {
410 group.id != super::PaneId::MAIN
411 && group.rect.contains(pos)
412 && plot.x_range().contains(pos.x)
413 }) {
414 let owner = *self.pane_series(group.id).first()?;
415 let map = group.price_map();
416 let closes = group.members.iter().filter_map(|id| {
417 let item =
418 (self.price_series.iter()).find(|item| item.id == *id && item.visible)?;
419 bar_at.and_then(|index| self.series_close_at(item, index))
420 });
421 let values = group
422 .members
423 .iter()
424 .filter_map(|id| self.study_ids.iter().position(|study| study == id))
425 .map(|index| &self.rendered_studies().indicators()[index])
426 .filter(|study| study.is_visible())
427 .flat_map(|study| {
428 (0..study.line_count())
429 .filter(|line| {
430 let plot = study.plot_style(*line);
431 plot.visible
432 && !matches!(
433 plot.plot_type,
434 crate::studies::PlotType::BarColor
435 | crate::studies::PlotType::BackgroundColor
436 )
437 })
438 .filter_map(move |line| {
439 bar_at.and_then(|index| study.plot_value(line, index))
440 })
441 })
442 .chain(closes)
443 .collect::<Vec<_>>();
444 (owner, magnet(map, values), pane_index(group.id))
445 } else if self.last_rendered_volume_rect.contains(pos)
446 && let Some(map) = self.host_events.volume_map
447 {
448 let values = bar_at
449 .map(|index| shown[index].volume)
450 .into_iter()
451 .collect();
452 (
453 SeriesId::VOLUME,
454 magnet(map, values),
455 pane_index(super::PaneId::MAIN),
456 )
457 } else if plot.contains(pos) {
458 let snapped = bar_at.map_or(pos, |index| {
459 self.snap_crosshair(pos, &self.last_price_map, index)
460 });
461 let price = self.last_price_map.y_to_price(snapped.y);
462 (SeriesId::MAIN, price, pane_index(super::PaneId::MAIN))
463 } else {
464 return None;
465 };
466 price.is_finite().then(|| ChartPointer {
467 time,
468 price,
469 series,
470 index: index as isize,
471 pos,
472 pane,
473 bar: bar_at.map(|index| shown[index].clone()),
474 previous_close: bar_at
475 .and_then(|index| crate::model::last_with_value(&shown[..index]))
476 .map(|bar| bar.close),
477 series_bars: self
478 .price_series
479 .iter()
480 .filter_map(|item| {
481 let at = item.bars.binary_search_by_key(&time, |bar| bar.time).ok()?;
482 Some((item.id, item.bars[at].clone()))
483 })
484 .collect(),
485 studies: bar_at.map_or_else(Vec::new, |index| self.study_rows_at(index)),
486 target: self.host_events.target,
487 modifiers: ui.input(|input| input.modifiers),
488 kind: self.host_events.kind.unwrap_or_else(|| {
489 if ui.input(|input| {
490 input.any_touches()
491 || input
492 .events
493 .iter()
494 .any(|event| matches!(event, egui::Event::Touch { .. }))
495 }) {
496 PointerKind::Touch
497 } else {
498 PointerKind::Mouse
499 }
500 }),
501 })
502 }
503
504 pub(super) fn record_host_events(&mut self, ui: &Ui) {
505 #[cfg(feature = "drawings")]
506 self.record_image_requests(ui);
507 let range = self.visible_time_range();
508 if range != self.host_events.range {
509 self.host_events.range = range;
510 self.host_events
511 .push(ChartEvent::VisibleRangeChanged(range));
512 }
513 let logical_range = self.visible_logical_range();
514 if logical_range != self.host_events.logical_range {
515 self.host_events.logical_range = logical_range;
516 self.host_events
517 .push(ChartEvent::VisibleLogicalRangeChanged(logical_range));
518 }
519 let size = self.time_scale_size();
520 if size != self.host_events.time_scale_size {
521 self.host_events.time_scale_size = size;
522 self.host_events.push(ChartEvent::TimeScaleResized(size));
523 }
524 let position = ui.input(|input| input.pointer.hover_pos()).and_then(|pos| {
525 ui.rect_contains_pointer(egui::Rect::from_min_max(
526 egui::pos2(self.last_rendered_price_rect.left(), ui.max_rect().top()),
527 egui::pos2(
528 self.last_rendered_price_rect.right(),
529 ui.max_rect().bottom(),
530 ),
531 ))
532 .then(|| self.pointer_at(ui, pos))
533 .flatten()
534 });
535 if position != self.host_events.pointer {
536 self.host_events.pointer = position.clone();
537 self.host_events.push(ChartEvent::CrosshairMoved(position));
538 }
539 }
540
541 #[cfg(feature = "drawings")]
542 fn record_image_requests(&mut self, ui: &Ui) {
543 use crate::drawings::DrawingToolType;
544
545 if let Some(drawing) = self.drawings.image_request.take() {
546 self.host_events
547 .push(ChartEvent::ImageRequested(ImageRequest {
548 drawing,
549 file: None,
550 }));
551 }
552 let pos = ui.input(|input| {
553 for file in &input.raw.dropped_files {
554 let supported = file
555 .path()
556 .extension()
557 .and_then(|ext| ext.to_str())
558 .is_some_and(|ext| {
559 ["png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"]
560 .iter()
561 .any(|image| ext.eq_ignore_ascii_case(image))
562 });
563 if supported && self.host_events.dropped_images.len() < 16 {
564 self.host_events.dropped_images.push(file.clone());
565 }
566 }
567 input.pointer.hover_pos()
568 });
569 let Some(pos) = pos.filter(|_| !self.host_events.dropped_images.is_empty()) else {
572 return;
573 };
574 let files = std::mem::take(&mut self.host_events.dropped_images);
575 let plot = self.last_rendered_price_rect;
576 if !ui.rect_contains_pointer(plot) {
577 return;
578 }
579 let index = self
580 .state
581 .time_scale()
582 .coord_to_idx_precise(pos.x, plot.left(), plot.width())
583 .round();
584 let Some(time) = crate::model::time_at(self.state.shown(), index) else {
585 return;
586 };
587 let price = self.last_price_map.y_to_price(pos.y);
588 if !price.is_finite() {
589 return;
590 }
591 let requests = self.user_action(|chart| {
592 files
593 .into_iter()
594 .filter_map(|file| {
595 let mut drawing = chart.drawings.new_drawing(0, DrawingToolType::Image);
596 drawing.z_order = chart.drawings.top_z_order();
597 let drawing = chart.create_drawing(drawing, &[(time, price)]).ok()?;
598 Some(ImageRequest {
599 drawing,
600 file: Some(file),
601 })
602 })
603 .collect::<Vec<_>>()
604 });
605 if let Some(request) = requests.last() {
606 self.drawings.select(request.drawing);
607 }
608 for request in requests {
609 self.host_events.push(ChartEvent::ImageRequested(request));
610 }
611 }
612
613 pub(super) fn record_host_click(&mut self, ui: &Ui, response: &egui::Response) {
614 let Some(position) = response
615 .interact_pointer_pos()
616 .and_then(|pos| self.pointer_at(ui, pos))
617 else {
618 return;
619 };
620 let frame = ui.ctx().cumulative_frame_nr();
621 for (index, button) in [PointerButton::Primary, PointerButton::Secondary]
622 .into_iter()
623 .enumerate()
624 {
625 if response.clicked_by(button) && self.host_events.click_frames[index] != Some(frame) {
626 self.host_events.click_frames[index] = Some(frame);
627 self.host_events.push(ChartEvent::Click {
628 position: position.clone(),
629 button,
630 });
631 if button == PointerButton::Primary {
632 self.click_mark(position.pos);
633 #[cfg(feature = "drawings")]
635 if !response.double_clicked() && !response.triple_clicked() {
636 self.click_link(ui.ctx(), position.pos);
637 }
638 }
639 }
640 }
641 if response.double_clicked() && self.host_events.click_frames[2] != Some(frame) {
642 self.host_events.click_frames[2] = Some(frame);
643 self.host_events.push(ChartEvent::DoubleClick { position });
644 }
645 }
646}
647
648#[cfg(test)]
649mod tests {
650 #[cfg(feature = "drawings")]
652 #[test]
653 fn image_requests_reach_the_host() {
654 use crate::drawings::{Drawing, DrawingToolType};
655 use crate::widget::test_frames::{chart, frame};
656 use egui::{Context, Event, RawInput, Rect, Vec2, pos2, vec2};
657 use std::sync::Arc;
658
659 #[derive(Debug)]
660 struct File(&'static str);
661 impl egui::DroppedFile for File {
662 fn path(&self) -> &std::path::Path {
663 self.0.as_ref()
664 }
665 #[cfg(not(target_arch = "wasm32"))]
666 fn bytes(&self) -> Result<Vec<u8>, String> {
667 Ok(Vec::new())
668 }
669 #[cfg(target_arch = "wasm32")]
670 fn bytes_async(
671 &self,
672 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Vec<u8>, String>> + '_>>
673 {
674 Box::pin(async { Ok(Vec::new()) })
675 }
676 }
677 let requests = |chart: &mut Chart| {
678 chart
679 .drain_events()
680 .filter_map(|event| {
681 if let ChartEvent::ImageRequested(request) = event {
682 Some(request)
683 } else {
684 None
685 }
686 })
687 .collect::<Vec<_>>()
688 };
689 let ctx = Context::default();
690 let mut chart = chart(&[10.0], &[20.0]);
691 frame(&ctx, &mut chart, 0.0, vec![]);
692 let map = chart.get_chart_mapping();
693 let p = pos2(map.idx_to_x(30), map.price_to_y(16.0));
694 chart
695 .drawings_mut()
696 .set_active_tool(Some(DrawingToolType::Image));
697 frame(&ctx, &mut chart, 0.1, vec![Event::PointerMoved(p)]);
698 for (time, pressed) in [(0.2, true), (0.3, false)] {
699 frame(
700 &ctx,
701 &mut chart,
702 time,
703 vec![Event::PointerButton {
704 pos: p,
705 button: PointerButton::Primary,
706 pressed,
707 modifiers: Modifiers::NONE,
708 }],
709 );
710 }
711 let id = chart.drawings().drawings[0].id;
712 assert_eq!(
713 requests(&mut chart),
714 [ImageRequest {
715 drawing: id,
716 file: None
717 }]
718 );
719 assert_eq!(chart.drawings().text_editor.as_ref().unwrap().id, id);
720 assert_eq!(
721 chart.set_drawing_image(id, "test://a", vec2(1920.0, 1080.0)),
722 Ok(())
723 );
724 let picture = &chart.drawings().drawings[0];
725 assert_eq!(picture.text.as_deref(), Some("test://a"));
726 assert_eq!(picture.image.unwrap().size, Some([240.0, 135.0]));
727 assert!(chart.drawings().text_editor.is_none());
728 assert!(chart.undo());
729 assert_eq!(chart.drawings().drawings[0].text, None);
730 assert_eq!(chart.drawings().drawings[0].image, None);
731 for (size, expected) in [
732 (vec2(100.0, 50.0), [100.0, 50.0]),
733 (vec2(400.0, 200.0), [240.0, 120.0]),
734 (vec2(480.0, 720.0), [120.0, 180.0]),
735 (vec2(10_000.0, 1.0), [240.0, 1.0]),
736 ] {
737 chart.set_drawing_image(id, "test://a", size).unwrap();
738 let actual = chart.drawings().drawings[0].image.unwrap().size.unwrap();
739 for (actual, expected) in actual.into_iter().zip(expected) {
740 assert!((actual - expected).abs() < 1e-3);
741 }
742 assert!(actual[0] <= 240.0 && actual[1] <= 180.0, "{actual:?}");
743 assert!(chart.undo());
744 }
745 let line = chart
746 .create_drawing(
747 Drawing::new(0, DrawingToolType::TrendLine),
748 &[
749 (chart.bars(SeriesId::MAIN)[0].time, 15.0),
750 (chart.bars(SeriesId::MAIN)[1].time, 16.0),
751 ],
752 )
753 .unwrap();
754 for (target, locked, uri, size, error) in [
755 (usize::MAX, false, "a", vec2(10.0, 10.0), "Unknown drawing"),
756 (id, true, "a", vec2(10.0, 10.0), "Drawing is locked"),
757 (
758 line,
759 false,
760 "a",
761 vec2(10.0, 10.0),
762 "Drawing is not an image",
763 ),
764 (id, false, " ", vec2(10.0, 10.0), "Image address is empty"),
765 (
766 id,
767 false,
768 "a",
769 vec2(0.0, 10.0),
770 "Image size must be positive and finite",
771 ),
772 (
773 id,
774 false,
775 "a",
776 vec2(f32::NAN, 1.0),
777 "Image size must be positive and finite",
778 ),
779 ] {
780 chart.drawings_mut().drawings[0].locked = locked;
781 let before = chart.drawings().drawings.clone();
782 assert_eq!(chart.set_drawing_image(target, uri, size), Err(error));
783 assert_eq!(
784 serde_json::to_value(&chart.drawings().drawings).unwrap(),
785 serde_json::to_value(&before).unwrap()
786 );
787 }
788 chart.drawings_mut().drawings.clear();
789 frame(&ctx, &mut chart, 0.4, vec![Event::PointerGone]);
790 let files: Vec<egui::DroppedFileHandle> = vec![
791 Arc::new(File("a.PNG")),
792 Arc::new(File("b.jpg")),
793 Arc::new(File("notes.txt")),
794 ];
795 let drop_files = |chart: &mut Chart, time, dropped_files| {
796 let output = ctx.run_ui(
797 RawInput {
798 screen_rect: Some(Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0))),
799 time: Some(time),
800 dropped_files,
801 ..Default::default()
802 },
803 |ui| {
804 egui::CentralPanel::default().show(ui, |ui| {
805 chart.show(ui);
806 });
807 },
808 );
809 output.drop_without_applying_deltas();
810 };
811 drop_files(&mut chart, 0.5, files.clone());
812 assert!(chart.drawings().drawings.is_empty());
813 assert!(requests(&mut chart).is_empty());
814 frame(&ctx, &mut chart, 0.6, vec![Event::PointerMoved(p)]);
815 let dropped = requests(&mut chart);
816 assert_eq!(dropped.len(), 2);
817 assert_eq!(chart.drawings().drawings.len(), 2);
818 for (request, file) in dropped.iter().zip(&files) {
819 assert!(Arc::ptr_eq(request.file.as_ref().unwrap(), file));
820 let anchors = chart.drawing_points(request.drawing).unwrap();
821 assert_eq!(anchors.len(), 1);
822 assert_eq!(anchors[0].0.timestamp(), 1_702_592_000);
823 assert!((anchors[0].1 - 16.0).abs() < 1e-6, "{anchors:?}");
824 }
825 assert_eq!(chart.drawings().sel_drawing(), Some(dropped[1].drawing));
826 assert!(chart.undo());
827 assert!(chart.drawings().drawings.is_empty());
828 frame(&ctx, &mut chart, 0.7, vec![Event::PointerGone]);
829 drop_files(&mut chart, 0.8, vec![files[0].clone()]);
830 let rect = chart.get_rendered_price_rect();
831 frame(
832 &ctx,
833 &mut chart,
834 0.9,
835 vec![Event::PointerMoved(pos2(
836 rect.center().x,
837 rect.bottom() + 5.0,
838 ))],
839 );
840 assert!(chart.host_events.dropped_images.is_empty());
841 frame(&ctx, &mut chart, 1.0, vec![Event::PointerMoved(p)]);
842 assert!(chart.drawings().drawings.is_empty());
843 assert!(requests(&mut chart).is_empty());
844 }
845
846 use super::*;
847 use crate::widget::test_frames::{chart, frame};
848 use egui::{Context, Event};
849
850 #[test]
853 fn data_operation_notifications_keep_accepted_setter_order() {
854 let start = "2024-01-01T00:00:00Z".parse::<DateTime<Utc>>().unwrap();
855 let bar = |minute, price| {
856 Bar::new(
857 start + chrono::TimeDelta::minutes(minute),
858 price,
859 price,
860 price,
861 price,
862 1.0,
863 )
864 };
865 for secondary in [false, true] {
866 for replay in [false, true] {
867 for enabled in [false, true] {
868 let mut chart = Chart::new(crate::model::BarData::new());
869 let series = if secondary {
870 chart.add_series(Default::default())
871 } else {
872 SeriesId::MAIN
873 };
874 if replay {
875 chart.set_replay_time(Some(bar(2, 1.0).time));
876 }
877 chart.drain_events().for_each(drop);
878 if enabled {
879 chart.set_data_operation_notifications(true);
880 }
881 chart.set_data(series, vec![bar(0, 10.0), bar(2, 20.0)]);
882 chart.update(series, bar(3, 30.0));
883 chart.update(series, bar(3, 30.0));
884 chart.update(series, bar(0, 11.0));
885 chart.update(series, bar(1, 15.0));
886 chart.update(series, bar(4, f64::INFINITY));
887 chart.update(SeriesId(usize::MAX), bar(4, 40.0));
888 chart.set_data(SeriesId(usize::MAX), vec![bar(4, 40.0)]);
889 assert_eq!(chart.feed_state(series).unwrap().rejected_bars, 1);
890 let events = chart.drain_events().collect::<Vec<_>>();
891 let operations = events
892 .iter()
893 .filter(|event| matches!(event, ChartEvent::DataOperation { .. }))
894 .cloned()
895 .collect::<Vec<_>>();
896 let operation = |operation, bars| ChartEvent::DataOperation {
897 series,
898 operation,
899 bars,
900 };
901 let expected = if enabled {
902 vec![
903 operation(DataOperation::SetData, if replay { 1 } else { 2 }),
904 operation(
905 DataOperation::Append(bar(3, 30.0).time),
906 if replay { 1 } else { 3 },
907 ),
908 operation(
909 DataOperation::Update(bar(3, 30.0).time),
910 if replay { 1 } else { 3 },
911 ),
912 operation(DataOperation::Update(start), if replay { 1 } else { 3 }),
913 operation(
914 DataOperation::Insert(bar(1, 15.0).time),
915 if replay { 2 } else { 4 },
916 ),
917 ]
918 } else {
919 Vec::new()
920 };
921 assert_eq!(
922 operations, expected,
923 "secondary={secondary}, replay={replay}, enabled={enabled}"
924 );
925 let compact = events
926 .into_iter()
927 .filter(|event| !matches!(event, ChartEvent::DataOperation { .. }))
928 .collect::<Vec<_>>();
929 assert_eq!(
930 compact,
931 [
932 ChartEvent::DataLoaded {
933 series,
934 bars: if replay { 1 } else { 2 }
935 },
936 ChartEvent::DataUpdated {
937 series,
938 bars: if replay { 2 } else { 4 }
939 },
940 ]
941 );
942 chart.set_data(series, Vec::new());
943 chart.update(series, bar(0, 10.0));
944 chart.set_data_operation_notifications(false);
945 let events = chart.drain_events().collect::<Vec<_>>();
946 assert_eq!(events.len(), if enabled { 4 } else { 2 });
947 if enabled {
948 assert_eq!(events[1], operation(DataOperation::SetData, 0));
949 assert_eq!(events[3], operation(DataOperation::Append(start), 1));
950 }
951 chart.update(series, bar(0, 10.0));
952 assert_eq!(chart.drain_events().count(), 0);
953 chart.set_replay_time(None);
954 chart.set_data_operation_notifications(true);
955 for _ in 0..1100 {
956 chart.update(series, bar(0, 10.0));
957 }
958 let events = chart.drain_events().collect::<Vec<_>>();
959 assert_eq!(events.len(), 1025);
960 assert_eq!(events[0], ChartEvent::EventsDropped(76));
961 assert!(
962 events[1..]
963 .iter()
964 .all(|event| *event == operation(DataOperation::Update(start), 1))
965 );
966 }
967 }
968 }
969 }
970
971 #[test]
974 fn host_events_report_changes_and_plot_coordinates_once() {
975 let ctx = Context::default();
976 let mut chart = chart(&[100.0], &[200.0]);
977 assert_eq!(chart.drain_events().count(), 0);
978 for i in 0..1100 {
979 chart.set_symbol(i.to_string());
980 }
981 let events = chart.drain_events().collect::<Vec<_>>();
982 assert_eq!(events.len(), 1025);
983 assert_eq!(events[0], ChartEvent::EventsDropped(76));
984 assert_eq!(
985 events.last(),
986 Some(&ChartEvent::SymbolChanged("1099".into()))
987 );
988 chart.set_symbol("A");
989 chart.set_symbol("A");
990 chart.set_timeframe(crate::model::Timeframe::Day1);
991 chart.set_timeframe(crate::model::Timeframe::Day1);
992 let mut bars = chart.bars(SeriesId::MAIN).to_vec();
993 bars[70].payload = Some(std::sync::Arc::new(serde_json::json!({ "id": 70 })));
996 bars[69] = Bar::whitespace(bars[69].time);
997 chart.set_data(SeriesId::MAIN, bars.clone());
998 chart.update(SeriesId::MAIN, bars[99].clone());
999 assert_eq!(
1000 chart.drain_events().collect::<Vec<_>>(),
1001 vec![
1002 ChartEvent::SymbolChanged("A".into()),
1003 ChartEvent::TimeframeChanged(crate::model::Timeframe::Day1),
1004 ChartEvent::DataLoaded {
1005 series: SeriesId::MAIN,
1006 bars: 100
1007 },
1008 ]
1009 );
1010 chart.config.show_volume = false;
1011 chart.config.show_ohlc_info = false;
1012 chart.config.show_symbol_labels = false;
1013 for t in [0.0, 0.1, 0.2] {
1014 frame(&ctx, &mut chart, t, vec![]);
1015 }
1016 assert!(
1017 chart
1018 .drain_events()
1019 .any(|event| matches!(event, ChartEvent::VisibleRangeChanged(Some(_))))
1020 );
1021 frame(&ctx, &mut chart, 0.3, vec![]);
1022 assert_eq!(chart.drain_events().count(), 0);
1023 use crate::config::CrosshairMode;
1028 for (mode, inverted, crosshair, price) in [
1029 (
1030 crate::scales::PriceScaleMode::Normal,
1031 false,
1032 CrosshairMode::Normal,
1033 125.0,
1034 ),
1035 (
1036 crate::scales::PriceScaleMode::Normal,
1037 true,
1038 CrosshairMode::Normal,
1039 175.0,
1040 ),
1041 (
1042 crate::scales::PriceScaleMode::Logarithmic,
1043 false,
1044 CrosshairMode::Normal,
1045 100.0 * 2.0_f64.powf(0.25),
1046 ),
1047 (
1048 crate::scales::PriceScaleMode::Normal,
1049 false,
1050 CrosshairMode::Magnet,
1051 100.0,
1052 ),
1053 (
1054 crate::scales::PriceScaleMode::Normal,
1055 false,
1056 CrosshairMode::MagnetClose,
1057 200.0,
1058 ),
1059 ] {
1060 chart.set_crosshair_mode(crosshair);
1061 let scale = chart.state.price_scale_mut();
1062 scale.options_mut().mode = mode;
1063 scale.options_mut().invert_scale = inverted;
1064
1065 let transform = crate::scales::PriceTransform { mode, base: 0.0 };
1066 scale.rebase(transform);
1067 scale.set_range(transform.to_scale(100.0), transform.to_scale(200.0));
1068 frame(&ctx, &mut chart, 1.0, vec![]);
1069 chart.drain_events().for_each(drop);
1070 let rect = chart.last_rendered_price_rect;
1071 let pos = Pos2::new(
1072 chart.get_chart_mapping().idx_to_x(70),
1073 rect.bottom() - rect.height() * 0.25,
1074 );
1075 frame(&ctx, &mut chart, 1.1, vec![Event::PointerMoved(pos)]);
1076 let events = chart.drain_events().collect::<Vec<_>>();
1077 let Some(ChartEvent::CrosshairMoved(Some(pointer))) = events.last() else {
1078 panic!("{events:?}")
1079 };
1080 assert_eq!(pointer.time, bars[70].time);
1081 assert_eq!(pointer.series, SeriesId::MAIN);
1082 assert!(
1083 (pointer.price - price).abs() < 0.001,
1084 "{} != {price}",
1085 pointer.price
1086 );
1087 assert_eq!((pointer.index, pointer.pos, pointer.pane), (70, pos, 0));
1089 assert_eq!(pointer.bar.as_ref(), Some(&bars[70]));
1090 assert_eq!(pointer.previous_close, Some(bars[68].close));
1091 assert!(pointer.series_bars.is_empty() && pointer.studies.is_empty());
1092 assert_eq!(
1093 pointer.target,
1094 Some(PointerTarget::Element(ChartElementId::Series(
1095 SeriesId::MAIN
1096 )))
1097 );
1098 assert_eq!(
1099 (pointer.modifiers, pointer.kind),
1100 (Modifiers::NONE, PointerKind::Mouse)
1101 );
1102 let expected = pointer.clone();
1103 frame(&ctx, &mut chart, 1.2, vec![]);
1104 assert_eq!(chart.drain_events().count(), 0);
1105 for button in [PointerButton::Primary, PointerButton::Secondary] {
1106 for pressed in [true, false] {
1107 frame(
1108 &ctx,
1109 &mut chart,
1110 1.3,
1111 vec![Event::PointerButton {
1112 pos,
1113 button,
1114 pressed,
1115 modifiers: Default::default(),
1116 }],
1117 );
1118 }
1119 assert!(chart.drain_events().any(|event| event
1120 == ChartEvent::Click {
1121 position: expected.clone(),
1122 button
1123 }));
1124 assert_eq!(chart.drain_events().count(), 0);
1125 }
1126 frame(&ctx, &mut chart, 1.4, vec![Event::PointerGone]);
1127 assert!(
1128 chart
1129 .drain_events()
1130 .any(|event| event == ChartEvent::CrosshairMoved(None))
1131 );
1132 }
1133 chart.set_crosshair_mode(CrosshairMode::Normal);
1134 #[cfg(feature = "studies")]
1135 {
1136 let id = chart.add_indicator(Box::new(crate::studies::RSI::default()));
1137 chart.sync_panes();
1138 let pane_id = chart.series_pane(id).unwrap();
1139 for (above, maximized) in [(false, false), (false, true), (true, true), (true, false)] {
1140 chart.move_pane(pane_id, if above { 0 } else { 1 });
1141 chart.pane_layout.maximized = maximized.then_some(pane_id);
1142 frame(&ctx, &mut chart, 1.5, vec![]);
1143 chart.drain_events().for_each(drop);
1144 let pane = &chart.last_rendered_indicator_panes[0];
1145 let pos = Pos2::new(pane.mapping.idx_to_x(70), pane.chart_rect.center().y);
1146 frame(&ctx, &mut chart, 1.6, vec![Event::PointerMoved(pos)]);
1147 let events = chart.drain_events().collect::<Vec<_>>();
1148 let Some(ChartEvent::CrosshairMoved(Some(pointer))) = events.last() else {
1149 panic!("above={above}, maximized={maximized}: {events:?}")
1150 };
1151 assert_eq!(pointer.series, id);
1152 assert_eq!(pointer.time, bars[70].time);
1153 assert!((pointer.price - 50.0).abs() < 0.001);
1154 assert_eq!(pointer.pane, usize::from(!above));
1155 assert_eq!(
1156 pointer.studies,
1157 chart
1158 .study_values_at(70)
1159 .into_iter()
1160 .map(|row| (id, row))
1161 .collect::<Vec<_>>()
1162 );
1163 let expected = pointer.clone();
1164 for pressed in [true, false] {
1165 frame(
1166 &ctx,
1167 &mut chart,
1168 1.7,
1169 vec![Event::PointerButton {
1170 pos,
1171 button: PointerButton::Primary,
1172 pressed,
1173 modifiers: Default::default(),
1174 }],
1175 );
1176 }
1177 let events = chart.drain_events().collect::<Vec<_>>();
1178 assert!(
1179 events.iter().any(|event| *event
1180 == ChartEvent::Click {
1181 position: expected.clone(),
1182 button: PointerButton::Primary
1183 }),
1184 "above={above}, maximized={maximized}: {events:?}, expected {expected:?}"
1185 );
1186 frame(&ctx, &mut chart, 1.75, vec![Event::PointerGone]);
1187 }
1188 chart.move_pane(pane_id, 1);
1189 let other = chart.add_indicator(Box::new(
1190 crate::studies::CustomIndicator::new("Other", |bars| {
1191 bars.iter()
1192 .map(|_| crate::studies::IndicatorValue::Single(75.0))
1193 .collect()
1194 })
1195 .with_overlay(false),
1196 ));
1197 assert!(chart.move_indicator_to_pane(other, id));
1198 chart.set_crosshair_mode(crate::config::CrosshairMode::Magnet);
1199 frame(&ctx, &mut chart, 1.8, vec![Event::PointerGone]);
1200 chart.drain_events().for_each(drop);
1201 let pane = &chart.last_rendered_indicator_panes[0];
1202 let pos = Pos2::new(pane.mapping.idx_to_x(70), pane.mapping.price_to_y(74.0));
1203 frame(&ctx, &mut chart, 1.9, vec![Event::PointerMoved(pos)]);
1204 let events = chart.drain_events().collect::<Vec<_>>();
1205 assert!(events.iter().any(|event| matches!(event, ChartEvent::CrosshairMoved(Some(point)) if point.series == id && (point.price - 75.0).abs() < 0.001)), "{events:?}");
1206 let pane = &chart.last_rendered_indicator_panes[0];
1207 let pos = Pos2::new(pane.mapping.idx_to_x(70), pane.mapping.price_to_y(75.0));
1208 frame(&ctx, &mut chart, 1.95, vec![Event::PointerMoved(pos)]);
1209 let events = chart.drain_events().collect::<Vec<_>>();
1210 assert!(events.iter().any(|event| matches!(event, ChartEvent::CrosshairMoved(Some(point)) if point.target == Some(PointerTarget::Element(ChartElementId::Indicator(other))))), "{events:?}");
1211 let series = chart.add_series(crate::widget::SeriesOptions::default());
1214 chart.set_data(series, bars.clone());
1215 let series_pane = chart.add_pane();
1216 assert!(chart.move_series_to_pane(series, series_pane));
1217 let below = chart.add_indicator(Box::new(crate::studies::RSI::default()));
1218 frame(&ctx, &mut chart, 1.96, vec![Event::PointerGone]);
1219 chart.drain_events().for_each(drop);
1220 let map = chart.series_price_map(series).unwrap();
1221 let pos = Pos2::new(
1222 chart.get_chart_mapping().idx_to_x(70),
1223 map.price_to_y(bars[70].close) + 2.0,
1224 );
1225 assert!(chart.pane_layout.panes[2].rect.contains(pos));
1226 frame(&ctx, &mut chart, 1.965, vec![Event::PointerMoved(pos)]);
1227 let events = chart.drain_events().collect::<Vec<_>>();
1228 assert!(events.iter().any(|event| matches!(event, ChartEvent::CrosshairMoved(Some(point)) if point.series == series && point.pane == 2 && point.price == bars[70].close)), "{events:?}");
1229 let pane = chart.last_rendered_indicator_panes.last().unwrap();
1230 let pos = Pos2::new(pane.mapping.idx_to_x(70), pane.chart_rect.center().y);
1231 frame(&ctx, &mut chart, 1.97, vec![Event::PointerMoved(pos)]);
1232 let events = chart.drain_events().collect::<Vec<_>>();
1233 assert!(events.iter().any(|event| matches!(event, ChartEvent::CrosshairMoved(Some(point)) if point.series == below && point.pane == 3)), "{events:?}");
1234 assert!(chart.move_pane(super::super::PaneId::MAIN, usize::MAX));
1236 frame(&ctx, &mut chart, 1.98, vec![Event::PointerGone]);
1237 chart.drain_events().for_each(drop);
1238 let pos = chart.last_rendered_price_rect.center();
1239 frame(&ctx, &mut chart, 1.99, vec![Event::PointerMoved(pos)]);
1240 let events = chart.drain_events().collect::<Vec<_>>();
1241 assert!(events.iter().any(|event| matches!(event, ChartEvent::CrosshairMoved(Some(point)) if point.series == SeriesId::MAIN && point.pane == 3)), "{events:?}");
1242 chart.remove_series(series);
1243 chart.remove_series(below);
1244 chart.remove_series(other);
1245 chart.remove_series(id);
1246 chart.set_crosshair_mode(crate::config::CrosshairMode::Normal);
1247 frame(&ctx, &mut chart, 2.0, vec![Event::PointerGone]);
1248 chart.drain_events().for_each(drop);
1249 }
1250 chart.config.show_volume = true;
1251 frame(&ctx, &mut chart, 2.1, vec![]);
1252 chart.drain_events().for_each(drop);
1253 let volume = chart.last_rendered_volume_rect;
1254 let pos = Pos2::new(chart.get_chart_mapping().idx_to_x(70), volume.center().y);
1255 for (step, (mode, price)) in [
1256 (crate::config::CrosshairMode::Normal, 0.5),
1257 (crate::config::CrosshairMode::Magnet, 1.0),
1258 ]
1259 .into_iter()
1260 .enumerate()
1261 {
1262 chart.set_crosshair_mode(mode);
1263 frame(
1264 &ctx,
1265 &mut chart,
1266 2.2 + step as f64,
1267 vec![Event::PointerMoved(pos)],
1268 );
1269 chart.drain_events().for_each(drop);
1270 for pressed in [true, false] {
1271 frame(
1272 &ctx,
1273 &mut chart,
1274 2.3 + step as f64,
1275 vec![Event::PointerButton {
1276 pos,
1277 button: PointerButton::Secondary,
1278 pressed,
1279 modifiers: Default::default(),
1280 }],
1281 );
1282 }
1283 assert!(chart.drain_events().any(|event| matches!(event, ChartEvent::Click { position, button: PointerButton::Secondary } if position.series == SeriesId::VOLUME && (position.price - price).abs() < 0.001)));
1284 }
1285 frame(&ctx, &mut chart, 3.5, vec![Event::PointerGone]);
1286 chart.drain_events().for_each(drop);
1287 let series = chart.add_series(Default::default());
1289 chart.set_data(series, bars.clone());
1290 assert!(chart.set_series_visible(series, false));
1291 #[cfg(feature = "drawings")]
1292 {
1293 let mut line = crate::drawings::Drawing::with_color(
1294 1,
1295 crate::drawings::DrawingToolType::HorizontalLine,
1296 [255, 0, 0, 255],
1297 );
1298 line.chart_points = vec![crate::drawings::ChartPoint::new(50.0, 150.0)];
1299 chart.drawings_mut().drawings.push(line);
1300 }
1301 frame(&ctx, &mut chart, 3.6, vec![]);
1302 chart.drain_events().for_each(drop);
1303 let map = chart.get_chart_mapping();
1304 let pos = Pos2::new(map.idx_to_x(70), map.price_to_y(150.0));
1305 frame(
1306 &ctx,
1307 &mut chart,
1308 3.7,
1309 vec![
1310 Event::ModifiersChanged(Modifiers::SHIFT),
1311 Event::Touch {
1312 device_id: egui::TouchDeviceId(0),
1313 id: egui::TouchId(0),
1314 phase: egui::TouchPhase::Start,
1315 pos,
1316 force: None,
1317 },
1318 Event::PointerMoved(pos),
1319 ],
1320 );
1321 let events = chart.drain_events().collect::<Vec<_>>();
1322 let Some(ChartEvent::CrosshairMoved(Some(pointer))) = events.last() else {
1323 panic!("{events:?}")
1324 };
1325 assert_eq!(pointer.series_bars, vec![(series, bars[70].clone())]);
1326 #[cfg(feature = "drawings")]
1327 assert_eq!(pointer.target, Some(PointerTarget::Drawing(1)));
1328 assert_eq!(
1329 (pointer.modifiers, pointer.kind),
1330 (Modifiers::SHIFT, PointerKind::Touch)
1331 );
1332 for kind in [PointerKind::Pen, PointerKind::Mouse] {
1333 chart.set_pointer_kind(Some(kind));
1334 frame(&ctx, &mut chart, 3.75, vec![Event::PointerMoved(pos)]);
1335 assert!(chart.drain_events().any(|event| matches!(event,
1336 ChartEvent::CrosshairMoved(Some(pointer)) if pointer.kind == kind
1337 && pointer.series_bars == vec![(series, bars[70].clone())]
1338 )));
1339 }
1340 chart.set_pointer_kind(None);
1341 let mut empty = bars.clone();
1342 empty[70] = Bar::whitespace(bars[70].time);
1343 empty[70].payload = Some(std::sync::Arc::new(serde_json::json!({ "id": "empty" })));
1344 chart.set_data(SeriesId::MAIN, empty.clone());
1345 chart.set_data(series, empty.clone());
1346 frame(&ctx, &mut chart, 3.76, vec![Event::PointerMoved(pos)]);
1347 let pointer = chart
1348 .drain_events()
1349 .find_map(|event| match event {
1350 ChartEvent::CrosshairMoved(Some(pointer)) => Some(pointer),
1351 _ => None,
1352 })
1353 .unwrap();
1354 assert!(pointer.bar.as_ref().unwrap().is_whitespace());
1355 assert!(pointer.series_bars[0].1.is_whitespace());
1356 assert_eq!(pointer.series_bars[0].1.payload, empty[70].payload);
1357 assert_eq!(pointer, pointer.clone());
1358 frame(&ctx, &mut chart, 3.77, vec![]);
1359 assert!(
1360 !chart
1361 .drain_events()
1362 .any(|event| matches!(event, ChartEvent::CrosshairMoved(_)))
1363 );
1364 empty[70].payload = Some(std::sync::Arc::new(serde_json::json!({ "id": "changed" })));
1365 chart.set_data(series, empty);
1366 frame(&ctx, &mut chart, 3.78, vec![]);
1367 assert!(chart.drain_events().any(|event| matches!(event,
1368 ChartEvent::CrosshairMoved(Some(updated)) if updated != pointer
1369 )));
1370 chart.set_data(SeriesId::MAIN, bars.clone());
1371 chart.remove_series(series);
1372 #[cfg(feature = "drawings")]
1373 chart.drawings_mut().drawings.clear();
1374 frame(
1375 &ctx,
1376 &mut chart,
1377 3.8,
1378 vec![Event::ModifiersChanged(Modifiers::NONE), Event::PointerGone],
1379 );
1380 chart.drain_events().for_each(drop);
1381 for (step, pressed) in [true, false, true, false].into_iter().enumerate() {
1383 frame(
1384 &ctx,
1385 &mut chart,
1386 3.95 + step as f64 * 0.01,
1387 vec![
1388 Event::PointerMoved(pos),
1389 Event::PointerButton {
1390 pos,
1391 button: PointerButton::Primary,
1392 pressed,
1393 modifiers: Modifiers::NONE,
1394 },
1395 ],
1396 );
1397 }
1398 let events = chart.drain_events().collect::<Vec<_>>();
1399 let [
1400 ..,
1401 ChartEvent::Click { position, .. },
1402 ChartEvent::DoubleClick { position: second },
1403 ] = events.as_slice()
1404 else {
1405 panic!("{events:?}")
1406 };
1407 assert_eq!((position.index, second), (70, position));
1408 let doubles = events
1409 .iter()
1410 .filter(|event| matches!(event, ChartEvent::DoubleClick { .. }));
1411 assert_eq!(doubles.count(), 1);
1412 frame(&ctx, &mut chart, 3.99, vec![Event::PointerGone]);
1413 chart.drain_events().for_each(drop);
1414 let range = chart.visible_time_range().unwrap();
1415 chart.apply_synced_time_range(VisibleTimeRange {
1416 from: range.from + chrono::Duration::days(1),
1417 to: range.to + chrono::Duration::days(1),
1418 });
1419 frame(&ctx, &mut chart, 4.0, vec![]);
1420 assert!(
1421 chart
1422 .drain_events()
1423 .any(|event| matches!(event, ChartEvent::VisibleRangeChanged(Some(_))))
1424 );
1425 chart.set_data(SeriesId::MAIN, vec![]);
1426 frame(&ctx, &mut chart, 4.1, vec![]);
1427 assert_eq!(
1428 chart.drain_events().collect::<Vec<_>>(),
1429 vec![
1430 ChartEvent::DataLoaded {
1431 series: SeriesId::MAIN,
1432 bars: 0
1433 },
1434 ChartEvent::VisibleRangeChanged(None),
1435 ChartEvent::VisibleLogicalRangeChanged(None),
1436 ]
1437 );
1438 }
1439}