egui_charts/widget/
crosshair.rs1use super::{Chart, ChartElementId, PaneId, RightClickTarget, SeriesId};
4use crate::chart::rendering::{ChartMapping, SeriesAction, StudyAction, study_mapping};
5use crate::config::CrosshairMode;
6use crate::scales::PriceMap;
7use crate::studies::Indicator;
8use egui::Pos2;
9
10impl Chart {
11 pub fn set_crosshair_mode(&mut self, mode: CrosshairMode) {
13 self.chart_options.crosshair.mode = mode;
14 }
15
16 pub(super) fn crosshair_position(&self, mut pos: Pos2, coords: &ChartMapping) -> Option<Pos2> {
18 let options = &self.chart_options.time_scale;
19 if !options.ignore_whitespace_indices && !options.ignore_hidden_indices {
20 return Some(pos);
21 }
22 let bars = self.state.shown();
23 let index = coords.x_to_idx_f64(pos.x);
24 if bars.is_empty() || !index.is_finite() {
25 return None;
26 }
27 let first = coords.x_to_idx_f64(coords.rect.left()).ceil().max(0.0) as isize;
28 let last = coords
29 .x_to_idx_f64(coords.rect.right())
30 .floor()
31 .min(bars.len() as f64 - 1.0) as isize;
32 if first > last {
33 return None;
34 }
35 let max_coordinate = coords.rect.left().abs().max(coords.rect.right().abs());
37 let precision = f32::EPSILON * max_coordinate.max(1.0);
38 let mut left = index.floor().clamp(first as f64 - 1.0, last as f64) as isize;
39 let mut right = left + 1;
40 while left >= first || right <= last {
41 let candidate = if right > last
42 || (left >= first
43 && (pos.x - coords.idx_to_x(left as usize)).abs() + precision
44 < (coords.idx_to_x(right as usize) - pos.x).abs())
45 {
46 let candidate = left as usize;
47 left -= 1;
48 candidate
49 } else {
50 let candidate = right as usize;
51 right += 1;
52 candidate
53 };
54 let x = coords.idx_to_x(candidate);
55 if self.crosshair_index_eligible(candidate) {
56 pos.x = x;
57 return Some(pos);
58 }
59 }
60 None
61 }
62
63 fn crosshair_index_eligible(&self, index: usize) -> bool {
64 let options = &self.chart_options.time_scale;
65 let bar = &self.state.shown()[index];
66 let series = self.price_series.iter().filter_map(|item| {
67 let at = item
68 .bars
69 .binary_search_by_key(&bar.time, |bar| bar.time)
70 .ok()?;
71 Some((item.visible, !item.bars[at].is_whitespace()))
72 });
73 let studies = self
74 .rendered_studies()
75 .indicators()
76 .iter()
77 .flat_map(|study| {
78 (0..study.line_count()).map(move |line| {
79 let plot = study.plot_style(line);
80 let valued = !matches!(
81 plot.plot_type,
82 crate::studies::PlotType::BarColor
83 | crate::studies::PlotType::BackgroundColor
84 ) && study.plot_value(line, index).is_some_and(f64::is_finite);
85 (study.is_visible() && plot.visible, valued)
86 })
87 });
88 let mut valued = false;
89 for (visible, has_value) in
90 std::iter::once((self.main_series_visible, !bar.is_whitespace()))
91 .chain(series)
92 .chain(studies)
93 {
94 if has_value {
95 if visible || !options.ignore_hidden_indices {
96 return true;
97 }
98 valued = true;
99 }
100 }
101 !valued && !options.ignore_whitespace_indices
102 }
103
104 pub(super) fn snap_crosshair(&self, mut pos: Pos2, map: &PriceMap, index: usize) -> Pos2 {
105 let mode = self.chart_options.crosshair.mode;
106 if !mode.snaps() {
107 return pos;
108 }
109 let close_only = mode == CrosshairMode::MagnetClose;
110 let ohlc = self
112 .state
113 .shown()
114 .get(index)
115 .filter(|bar| self.main_series_visible && !bar.is_whitespace())
116 .into_iter()
117 .flat_map(|bar| {
118 [bar.open, bar.high, bar.low, bar.close]
119 .into_iter()
120 .skip(if close_only { 3 } else { 0 })
121 })
122 .map(|value| map.price_to_y(value));
123 let time_based = !self.chart_type.is_time_independent();
124 let coords = ChartMapping {
125 price: *map,
126 ..self.get_chart_mapping()
127 };
128 let studies = self
129 .study_ids
130 .iter()
131 .zip(self.studies.indicators())
132 .filter(|(id, study)| {
133 !close_only
134 && time_based
135 && study.is_visible()
136 && study.is_overlay()
137 && !study.is_volume()
138 && self.series_pane(**id) == Some(PaneId::MAIN)
139 })
140 .flat_map(|(_, study)| {
141 let scale = study_mapping(study.as_ref(), &coords).price;
142 (0..study.line_count())
143 .filter(|line| {
144 let plot = study.plot_style(*line);
145 plot.visible
146 && !matches!(
147 plot.plot_type,
148 crate::studies::PlotType::BarColor
149 | crate::studies::PlotType::BackgroundColor
150 )
151 })
152 .filter_map(move |line| study.plot_value(line, index))
153 .map(move |value| scale.price_to_y(value))
154 });
155 let rect = self.last_rendered_price_rect;
156 let series = self
157 .main_pane_series()
158 .filter(|_| time_based || close_only)
159 .filter_map(|item| {
160 let scale = self.series_map(item, rect);
161 self.series_close_at(item, index)
162 .map(|value| scale.price_to_y(value))
163 });
164 if let Some(y) = ohlc
166 .chain(studies)
167 .chain(series)
168 .filter(|y| y.is_finite())
169 .min_by(|a, b| (a - pos.y).abs().total_cmp(&(b - pos.y).abs()))
170 {
171 pos.y = y;
172 }
173 pos
174 }
175
176 pub(super) fn apply_main_series_action(&mut self, action: SeriesAction) {
179 self.user_action(|chart| match action {
180 SeriesAction::Settings => chart.indicator_settings_request = Some(SeriesId::MAIN),
181 SeriesAction::ToggleVisibility => {
182 chart.set_main_series_visible(!chart.main_series_visible);
183 }
184 SeriesAction::Remove => {
185 chart.remove_series(SeriesId::MAIN);
186 }
187 });
188 }
189
190 pub(super) fn apply_study_action(&mut self, action: StudyAction) {
191 let index = match action {
192 StudyAction::Settings(index)
193 | StudyAction::ToggleVisibility(index)
194 | StudyAction::Remove(index)
195 | StudyAction::Menu { index, .. } => index,
196 };
197 let Some(id) = self.study_ids.get(index).copied() else {
198 return;
199 };
200 if let StudyAction::Menu { pos, .. } = action {
201 let bars = self.state.shown().len();
203 let bar_idx = self
204 .last_hover_bar_idx
205 .map(|index| index.round() as usize)
206 .filter(|index| *index < bars)
207 .unwrap_or(bars.saturating_sub(1));
208 let element = ChartElementId::Indicator(id);
209 self.selection.select(element, Some(bar_idx));
210 self.right_click = Some(RightClickTarget::Element {
211 id: element,
212 bar_idx,
213 pos,
214 price: self
215 .indicator_value_at(id, bar_idx)
216 .map_or(f64::NAN, |(_, value)| value),
217 });
218 return;
219 }
220 self.user_action(|chart| match action {
221 StudyAction::Settings(_) => chart.indicator_settings_request = Some(id),
222 StudyAction::ToggleVisibility(_) => {
223 chart.set_indicator_visible(id, !chart.studies.indicators()[index].is_visible());
224 }
225 StudyAction::Remove(_) => {
226 chart.remove_series(id);
227 }
228 StudyAction::Menu { .. } => {}
229 });
230 }
231}
232
233pub(crate) fn menu_value(study: &dyn Indicator, bar: usize) -> Option<(usize, f64)> {
236 (0..study.line_count())
237 .filter(|line| {
238 let plot = study.plot_style(*line);
239 plot.visible
240 && !matches!(
241 plot.plot_type,
242 crate::studies::PlotType::BarColor | crate::studies::PlotType::BackgroundColor
243 )
244 })
245 .find_map(|line| Some((line, study.plot_value(line, bar)?)))
246}