1mod background;
4mod sessions;
5
6#[cfg(feature = "studies")]
7pub(super) use sessions::session_ranges;
8
9use super::selection::hit_test_main_chart;
10use super::{Chart, PrimitivePass, RightClickTarget, primitives};
11use crate::chart::overlays::FootprintConfig;
12use crate::chart::rendering::{self, ChartMapping, RenderContext, StyleColors};
13use crate::data::Footprint;
14use crate::localization as l10n;
15use crate::model::{ChartType, MarkerZOrder, PriceLine};
16use crate::theme::ThemeExt;
17use egui::{Pos2, Rect, Response, Sense, Ui, Vec2};
18
19impl Chart {
20 pub fn prepare_conflation(&mut self, bar_spacing: f32, pixels_per_point: f32) -> usize {
32 let factor = if self.chart_type.is_time_independent() {
33 1
34 } else {
35 super::merge::factor(
36 bar_spacing,
37 pixels_per_point,
38 self.config.conflation_threshold,
39 )
40 };
41 self.merge_groups.update(
42 &self.state.data().bars,
43 self.state.revision(),
44 factor,
45 self.config.price_source,
46 );
47 factor
48 }
49
50 pub fn show(&mut self, ui: &mut Ui) -> Response {
71 self.show_content(ui, None)
72 }
73
74 fn paint_profile_context(
76 &self,
77 ui: &Ui,
78 mapping: &ChartMapping,
79 span: std::ops::Range<f32>,
80 session: chrono::DateTime<chrono::Utc>,
81 levels: &[(&str, f64)],
82 top: f32,
83 ) {
84 let left = span.start.max(mapping.rect.left());
85 let right = span.end.min(mapping.rect.right());
86 if right <= left {
87 return;
88 }
89 let theme = ui.active_theme();
90 let mut text = self.format_time(session);
91 for &(name, price) in levels {
92 let y = mapping.price_to_y(price);
93 let direction = if y < mapping.rect.top() {
94 "above"
95 } else if y > mapping.rect.bottom() {
96 "below"
97 } else {
98 ""
99 };
100 let formatter =
101 self.series_label_formatter(super::SeriesId::MAIN, mapping.price.transform().mode);
102 let value = mapping.price.label(
103 mapping.price.transform().to_scale(price),
104 rendering::axes::tag_decimals(&mapping.price, y),
105 formatter.as_deref(),
106 );
107 text.push_str(&format!("\n{name} {value}"));
108 if !direction.is_empty() {
109 text.push_str(&format!(" ({})", l10n::text(ui.ctx(), direction)));
110 }
111 }
112 let target = Rect::from_x_y_ranges(left..=right, top..=mapping.rect.bottom())
113 .intersect(mapping.rect);
114 if ui.ctx().pointer_hover_pos().is_some_and(|pos| {
116 target.contains(pos) && ui.ctx().layer_id_at(pos) == Some(ui.layer_id())
117 }) {
118 egui::Tooltip::always_open(
119 ui.ctx().clone(),
120 ui.layer_id(),
121 ui.scope_id().with(("profile_context", session)),
122 target,
123 )
124 .show(|ui| {
125 ui.label(&text);
126 });
127 }
128 let painter = ui.painter().with_clip_rect(mapping.rect);
129 if right - left < 40.0 {
130 painter.with_clip_rect(target).text(
131 Pos2::new((left + right) * 0.5, top),
132 egui::Align2::CENTER_TOP,
133 "i",
134 egui::FontId::proportional(theme.text.label),
135 theme.chart.axis_text,
136 );
137 return;
138 }
139 let galley = painter.layout(
140 text,
141 egui::FontId::proportional(theme.text.label),
142 theme.chart.axis_text,
143 (right - left - 12.0).max(1.0),
144 );
145 let pos = Pos2::new(right - galley.size().x - 6.0, top);
146 painter.rect_filled(
147 Rect::from_min_size(pos, galley.size()).expand(3.0),
148 2.0,
149 theme.chart.bg,
150 );
151 painter.galley(pos, galley, theme.chart.axis_text);
152 }
153
154 pub fn show_footprint(
162 &mut self,
163 ui: &mut Ui,
164 flow: &Footprint,
165 config: &FootprintConfig,
166 ) -> Response {
167 self.show_content(ui, Some((flow, config)))
168 }
169
170 fn show_content(
171 &mut self,
172 ui: &mut Ui,
173 footprint: Option<(&Footprint, &FootprintConfig)>,
174 ) -> Response {
175 let footprint = footprint.filter(|_| self.replay.is_none());
176 let theme = self.theme_override.clone();
177 let scaled = self.config.axis_font_size.map(|size| {
179 let mut scaled = theme.clone().unwrap_or_else(|| ui.ctx().active_theme());
180 scaled.text.label = crate::styles::typography::font_size(size);
181 scaled
182 });
183 let _theme_scope = scaled
184 .as_ref()
185 .or(theme.as_ref())
186 .map(|theme| crate::theme::ThemeScope::new(ui.ctx(), theme));
187 let size = self.size;
188 let show = |ui: &mut Ui| {
189 if let Some(theme) = &theme {
190 crate::theme::apply_to_style(ui.style_mut(), theme);
191 }
192 self.show_themed_content(ui, footprint)
193 };
194 match size {
196 Some(size) => ui.allocate_ui(size, show).inner,
197 None => ui.scope(show).inner,
198 }
199 }
200
201 fn show_themed_content(
202 &mut self,
203 ui: &mut Ui,
204 footprint: Option<(&Footprint, &FootprintConfig)>,
205 ) -> Response {
206 self.follow_chart_theme(&ui.active_theme());
207 self.host_events.volume_map = None;
208 self.host_events.target = None;
209 self.apply_config_scale_mode();
210 #[cfg(feature = "studies")]
211 self.sync_volume_study(ui.ctx());
212 #[cfg(feature = "drawings")]
213 self.sync_stored_drawings(ui.ctx());
214 self.pump(ui.ctx());
215 if let Some(wait) = self.refresh_studies(ui.input(|i| i.time)) {
216 ui.ctx()
217 .request_repaint_after(std::time::Duration::from_secs_f64(wait));
218 }
219 self.show_elements();
220 if let Some(period) = self.synced_crosshair_period {
222 self.set_synced_crosshair(Some(period), self.synced_crosshair_price);
223 if self.synced_crosshair_bar_idx.is_none() {
224 self.last_crosshair = None;
225 }
226 }
227 if let Some(wait) = self.refresh_element_studies(ui.input(|i| i.time)) {
228 ui.ctx()
229 .request_repaint_after(std::time::Duration::from_secs_f64(wait));
230 }
231 self.sync_primitives(ui.ctx());
232 let frame = ui.ctx().cumulative_frame_nr();
233 let width = ui.available_width();
234 let resized = self
235 .last_layout
236 .is_none_or(|(previous, changed)| previous != width || changed == frame);
237 let previous_axis_width = self.measured_axis_width;
238 self.last_rendered_indicator_panes.clear();
239 self.sync_panes();
240 for pane in &mut self.pane_layout.panes {
241 pane.rect = Rect::NOTHING;
242 }
243 let has_panes = self.has_panes();
245 let response = if has_panes {
246 self.show_with_panes(ui, footprint)
247 } else {
248 let response = self.show_internal(ui, None, None, footprint);
249 if let Some(main) = self
250 .pane_layout
251 .panes
252 .iter_mut()
253 .find(|pane| pane.id == super::PaneId::MAIN)
254 {
255 main.rect = response.rect;
256 }
257 response
258 };
259 self.menu_open = false;
260 if !self.chart_type.is_time_independent() {
261 let source_marks = self.mark_source.as_ref().map(|source| &source.page);
262 let mut coords = self.get_chart_mapping();
263 coords.start_idx = 0;
264 let baseline = if has_panes {
265 response.rect.bottom() - self.time_axis_height().min(response.rect.height())
266 } else {
267 coords.rect.bottom()
268 };
269 coords.rect.max.y = baseline;
270 let (from, to) = super::marks::window(self.state.shown(), &coords, coords.rect);
273 let mut events: Vec<_> = self
274 .events
275 .within(from, to)
276 .into_iter()
277 .map(|index| (index, self.events[index].clone()))
278 .collect();
279 let mut key = self.events.len();
280 if let Some(source) = source_marks {
281 events.extend(
282 source
283 .time_scale_marks
284 .iter()
285 .cloned()
286 .enumerate()
287 .map(|(index, event)| (key + index, event)),
288 );
289 key += source.time_scale_marks.len();
290 }
291 let typed_count = self.time_scale_events.len();
292 let typed = self
293 .time_scale_events
294 .within(from, to)
295 .into_iter()
296 .map(|index| (key + index, &self.time_scale_events[index]))
297 .chain(source_marks.into_iter().flat_map(|page| {
298 page.events
299 .iter()
300 .enumerate()
301 .map(move |(index, event)| (key + typed_count + index, event))
302 }));
303 let mut breaks = Vec::new();
304 for (key, event) in typed {
305 if event.kind == crate::model::EventKind::Earnings
306 && self.event_visibility.show_earnings_breaks
307 {
308 breaks.push(event.time);
309 }
310 if self.event_visibility.shows(event.kind) {
311 events.push((
312 key,
313 crate::model::Marker::event(event.time, event.kind.label())
314 .with_id(&event.id)
315 .with_tooltip(format!("{}\n{}", event.title, event.details)),
316 ));
317 }
318 }
319 rendering::render_event_breaks(ui, &breaks, self.state.shown(), &coords);
320 if let Some(id) = rendering::render_event_badges(
321 ui,
322 events.iter().map(|(key, event)| (*key, event)),
323 self.state.shown(),
324 &coords,
325 baseline,
326 ) {
327 self.event_click = Some(id);
328 }
329 }
330 if self.last_rendered_price_rect.width() > 1.0
331 && self
332 .last_layout
333 .is_none_or(|(previous, _)| previous != width)
334 {
335 self.last_layout = Some((width, frame));
336 }
337 if (self.measured_axis_width - previous_axis_width).abs() > 0.5 {
338 let old_width = self.last_rendered_price_rect.width();
339 let new_width = self
340 .calculate_layout_rects(response.rect)
341 .price_rect
342 .width();
343 if (!resized
345 || self.desired_visible_bars.is_some_and(|count| count > 0)
346 || self
347 .chart_options
348 .time_scale
349 .lock_visible_time_range_on_resize)
350 && previous_axis_width > 0.0
351 && old_width > 0.0
352 && new_width > 0.0
353 {
354 let scale = self.state.time_scale_mut();
355 scale.set_bar_spacing(scale.bar_spacing() * new_width / old_width);
356 }
357 ui.ctx().request_discard("Chart measured its axis labels");
358 }
359 self.record_host_events(ui);
360 let needs_info = response.clicked()
361 || response.double_clicked()
362 || response.triple_clicked()
363 || response.gained_focus()
364 || response.changed()
365 || ui
366 .ctx()
367 .accesskit_node_builder(response.id, |_| ())
368 .is_some();
369 #[cfg(debug_assertions)]
370 let needs_info = needs_info || ui.ctx().global_style().debug.show_interactive_widgets;
371 if needs_info {
372 let info = self.accessibility_info(ui.ctx(), response.enabled());
374 response.widget_info(|| info.clone());
375 }
376 #[cfg(feature = "drawings")]
378 self.save_stored_drawings();
379 response
380 }
381
382 fn paint_last_price_pulse(
385 &self,
386 ui: &Ui,
387 painter: &egui::Painter,
388 price_rect: Rect,
389 coords: &ChartMapping,
390 ) {
391 use crate::model::LastPriceAnimation;
392 let styles = &self.chart_type_params.styles;
393 let animation = styles.last_price_animation;
394 let Some(index) = self
395 .state
396 .shown()
397 .iter()
398 .rposition(|bar| !bar.is_whitespace())
399 else {
400 return;
401 };
402 let last = &self.state.shown()[index];
403 let price = self
404 .config
405 .price_source
406 .compute(last.open, last.high, last.low, last.close);
407 let Some(color) = rendering::candles::line_color_at(
408 self.chart_type,
409 styles,
410 self.chart_type_params.baseline,
411 &coords.price,
412 price_rect,
413 price,
414 last,
415 )
416 .filter(|_| animation != LastPriceAnimation::Disabled) else {
417 return;
418 };
419 let now = ui.input(|input| input.time);
420 let started = match self.last_price_pulse.get() {
421 Some((time, seen, started)) if time == last.time && seen.total_cmp(&price).is_eq() => {
422 started
423 }
424 previous => {
425 let started = previous.map_or(f64::NEG_INFINITY, |_| now);
427 self.last_price_pulse.set(Some((last.time, price, started)));
428 started
429 }
430 };
431 let period = rendering::candles::LAST_PRICE_PULSE_SECONDS;
432 let phase = match animation {
433 LastPriceAnimation::Continuous => (now / period).fract(),
434 _ => (now - started) / period,
435 };
436 let point = Pos2::new(coords.idx_to_x(index), coords.price.price_to_y(price));
437 if (0.0..1.0).contains(&phase) && price_rect.contains(point) {
438 rendering::candles::render_last_price_pulse(painter, point, color, phase as f32);
439 ui.ctx().request_repaint();
440 }
441 }
442
443 pub(crate) fn show_internal(
445 &mut self,
446 ui: &mut Ui,
447 shared_time_axis: Option<Rect>,
448 shared_plot: Option<&Response>,
449 footprint: Option<(&Footprint, &FootprintConfig)>,
450 ) -> Response {
451 #[cfg(feature = "icons")]
453 {
454 let ctx = ui.ctx().clone();
455 let loaders_installed = egui::Id::unique("egui_charts::image_loaders_installed");
456 if !ctx.data(|d| d.get_temp::<bool>(loaders_installed).unwrap_or(false)) {
457 #[cfg(feature = "ui")]
458 egui_extras::install_image_loaders(&ctx);
459 crate::icons::install_loader(&ctx);
460 ctx.data_mut(|d| d.insert_temp(loaders_installed, true));
461 }
462 }
463
464 self.zoom_just_applied = false;
466
467 self.right_click = None;
470
471 let available_size = ui.available_size();
472 let (mut response, mut painter) =
473 ui.allocate_painter(available_size, Sense::click_and_drag());
474 let rect = response.rect;
475
476 if let Some(axis) = shared_time_axis {
477 painter.set_clip_rect(ui.clip_rect().intersect(if rect.height() == 0.0 {
478 axis
479 } else {
480 rect.union(axis)
481 }));
482 response = response.union(ui.interact(
483 axis,
484 response.id.with("shared_time_axis"),
485 Sense::click_and_drag(),
486 ));
487 }
488 let mut layout = self.pane_layout_rects(
489 rect,
490 self.config.show_time_labels && shared_time_axis.is_none(),
491 );
492 let time_axis_top = shared_time_axis.map_or(layout.price_rect.bottom(), |axis| axis.top());
493 let statistics_rect = footprint
494 .filter(|(_, config)| {
495 config.statistics
496 && !self.chart_type.is_time_independent()
497 && self.main_series_visible
498 })
499 .map(|_| {
500 let bottom = layout.price_rect.bottom();
501 let height = 112.0_f32.min(layout.price_rect.height().max(0.0) * 0.4);
502 layout.price_rect.max.y -= height;
503 if self.shows_volume() {
504 layout.volume_rect =
505 super::volume::overlay_rect(layout.price_rect, &self.config);
506 }
507 Rect::from_min_max(
508 Pos2::new(layout.price_rect.left(), layout.price_rect.bottom()),
509 Pos2::new(layout.price_rect.right(), bottom),
510 )
511 });
512 let chart_rect = layout.price_rect;
513
514 self.handle_secondary_axes(ui, rect, chart_rect);
515 let chart_width = chart_rect.width();
516
517 self.apply_timescale_config(chart_width);
522 self.advance_scroll_animation(ui);
523
524 let navigation_rect = self.navigation_rect(ui, chart_rect);
525 let navigation = navigation_rect
526 .filter(|_| self.navigation_visible(ui, chart_rect))
527 .map(|nav| {
528 (
529 nav,
530 self.interact_navigation(ui, nav, chart_rect, response.id),
531 )
532 });
533 let scale = self.state.time_scale();
534 let mut live_scale = scale.clone();
535 live_scale.scroll_to_realtime();
536 let realtime_rect = (self.config.show_realtime_btn
537 && !self.state.data().is_empty()
538 && (scale.right_offset_precise() - live_scale.right_offset_precise()).abs() > 1e-6)
539 .then(|| {
540 rendering::realtime_button_rect(
541 chart_rect,
542 self.config.realtime_button_size,
543 self.config.realtime_button_pos,
544 navigation_rect.filter(|_| {
545 self.config.navigation_buttons != crate::config::NavigationButtons::Never
546 }),
547 )
548 })
549 .flatten();
550 let realtime_interaction = realtime_rect.is_some_and(|button| {
551 ui.input(|input| {
552 (input.pointer.any_down() || input.pointer.any_released())
553 && input
554 .pointer
555 .press_origin()
556 .or_else(|| input.pointer.hover_pos())
557 .is_some_and(|pos| button.contains(pos))
558 })
559 });
560 let navigation_interaction = navigation_rect.is_some_and(|nav| {
561 self.config.navigation_buttons != crate::config::NavigationButtons::Never
562 && !self.state.data().is_empty()
563 && ui.input(|i| {
564 (i.pointer.any_down() || i.pointer.any_released())
565 && i.pointer
566 .press_origin()
567 .or_else(|| i.pointer.hover_pos())
568 .is_some_and(|p| nav.contains(p))
569 })
570 });
571 let shown =
574 shared_plot.map_or_else(|| response.clone(), |plot| response.union(plot.clone()));
575 let axis_corner = Rect::from_min_max(
576 Pos2::new(
577 if self.config.show_right_axis {
578 chart_rect.right()
579 } else {
580 rect.left()
581 },
582 time_axis_top,
583 ),
584 Pos2::new(
585 if self.config.show_right_axis {
586 rect.right()
587 } else {
588 chart_rect.left()
589 },
590 time_axis_top + crate::styles::sizing::chart::BOTTOM_PADDING_WITH_TIME,
591 ),
592 );
593 #[cfg(feature = "drawings")]
594 let eraser_mode = self.drawings.eraser_mode;
595 #[cfg(not(feature = "drawings"))]
596 let eraser_mode = false;
597 #[cfg(feature = "drawings")]
598 let drawing_tool_active = self.drawings.active_tool.is_some();
599 #[cfg(not(feature = "drawings"))]
600 let drawing_tool_active = false;
601 #[cfg(not(feature = "drawings"))]
602 let is_drawing_interaction = false;
603 #[cfg(feature = "drawings")]
605 let is_drawing_interaction = self.drawings.active_tool.is_some()
606 || self.chart_options.crosshair.style == crate::config::CrosshairStyle::Magic
607 || self.drawings.dragging_handle.is_some()
608 || self.drawings.text_editor.is_some()
609 || self.drawings.curr_drawing.is_some()
610 || ui
611 .input(|i| i.pointer.press_origin().or_else(|| i.pointer.hover_pos()))
612 .is_some_and(|origin| {
613 self.drawings
614 .selected_ids()
615 .any(|id| self.drawings.hit_test_handle_by_id(origin, id).is_some())
616 || self.drawings.hit_test(origin).is_some_and(|id| {
617 self.drawings
618 .drawings
619 .iter()
620 .any(|d| d.id == id && !d.locked)
621 })
622 });
623 let show_price_button =
624 self.config.show_plus_button && !eraser_mode && !is_drawing_interaction;
625 let show_axis_settings = !self.layout_inactive || ui.rect_contains_pointer(rect);
626 let axis_interaction = cfg!(feature = "icons")
627 && (self.config.show_right_axis || self.config.show_left_axis)
628 && ui.input(|input| {
629 (input.pointer.any_down() || input.pointer.any_released())
630 && input
631 .pointer
632 .press_origin()
633 .or(input.pointer.hover_pos())
634 .is_some_and(|pos| {
635 (show_axis_settings && axis_corner.contains(pos))
636 || (show_price_button
637 && self.axis_hover.is_some_and(|(_, tag)| {
638 rendering::axes::crosshair_price_button_rect(tag)
639 .contains(pos)
640 }))
641 })
642 });
643 if navigation_interaction || realtime_interaction || axis_interaction {
644 response = ui.interact(
645 Rect::NOTHING,
646 response.id.with("navigation_input"),
647 Sense::hover(),
648 );
649 response.rect = rect;
650 }
651
652 let mut time_response =
654 shared_plot.map_or_else(|| response.clone(), |plot| response.union(plot.clone()));
655 self.handle_tracking_mode(ui, &time_response);
657
658 self.request_focus_if_needed(&mut time_response);
660
661 self.handle_keyboard_shortcuts(ui, &time_response, chart_width, chart_rect.min.x);
663
664 let logical_range = self.state.time_scale().visible_logical_range();
666 let visible_bars = logical_range.length().ceil() as usize;
667 self.last_visible_bars = visible_bars;
668
669 self.set_panning_cursor(ui, &time_response);
671
672 let price_axis_rect = Rect::from_min_max(
674 Pos2::new(axis_corner.left(), chart_rect.top()),
675 Pos2::new(axis_corner.right(), chart_rect.bottom()),
676 );
677 let time_axis_rect = shared_time_axis.unwrap_or_else(|| {
678 Rect::from_min_max(
679 Pos2::new(chart_rect.min.x, chart_rect.max.y),
680 Pos2::new(chart_rect.max.x, rect.max.y),
681 )
682 });
683 self.time_axis_rect = Rect::from_min_size(
684 time_axis_rect.min,
685 Vec2::new(
686 time_axis_rect.width(),
687 self.time_axis_height().min(time_axis_rect.height()),
688 ),
689 );
690
691 self.handle_double_click(&time_response, price_axis_rect, time_axis_rect);
693
694 let time_based = !self.chart_type.is_time_independent();
697 let show_volume = self.shows_volume();
698 let pending_price_zoom = self.handle_mouse_wheel(
699 ui,
700 &time_response,
701 chart_width,
702 chart_rect.min.x,
703 price_axis_rect,
704 );
705
706 self.handle_pinch_zoom(ui, &time_response, chart_width, chart_rect.min.x);
708
709 self.handle_drag_pan(
711 ui,
712 &time_response,
713 price_axis_rect,
714 time_axis_rect,
715 chart_rect.min.x,
716 is_drawing_interaction,
717 );
718
719 self.apply_kinetic_scroll(ui);
721
722 self.zoom_just_applied = self.handle_box_zoom(
725 ui,
726 &response,
727 chart_rect,
728 chart_width,
729 self.zoom_mode_active,
730 );
731 if self.zoom_just_applied {
732 log::info!("Zoom applied - chart will auto-deactivate zoom mode");
733 }
734
735 if self.zoom_mode_active && response.hovered() {
737 ui.ctx().set_cursor_icon(egui::CursorIcon::ZoomIn);
738 }
739 else if response.hovered() {
742 use crate::config::CrosshairStyle;
743 match self.chart_options.crosshair.style {
744 CrosshairStyle::Full => {
745 ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair);
747 }
748 CrosshairStyle::Dot => {
749 ui.ctx().set_cursor_icon(egui::CursorIcon::Default);
751 }
752 CrosshairStyle::Arrow
753 | CrosshairStyle::Demonstration
754 | CrosshairStyle::Magic
755 | CrosshairStyle::Hidden => {
756 ui.ctx().set_cursor_icon(egui::CursorIcon::Default);
758 }
759 }
760 }
761
762 self.draw_background(&painter, rect);
764 if let Some(axis) = shared_time_axis {
765 self.draw_background(
766 &painter,
767 Rect::from_min_max(
768 Pos2::new(rect.left(), axis.top()),
769 Pos2::new(rect.right(), axis.bottom()),
770 ),
771 );
772 }
773
774 self.pump_signals(ui.ctx());
775 self.pump_targets(ui.ctx());
776 #[cfg(feature = "drawings")]
777 self.pump_posts(ui.ctx());
778
779 self.draw_watermark(ui, &painter, chart_rect);
781 if let Some(watermark) = &self.watermark {
782 rendering::render_watermark(ui, &painter, chart_rect, watermark);
783 }
784
785 if self.state.data().is_empty() {
786 self.last_hover_bar_idx = None;
787 self.last_crosshair = None;
788 self.pump_marks(ui.ctx());
789 painter.text(
790 rect.center(),
791 egui::Align2::CENTER_CENTER,
792 l10n::text(ui.ctx(), "No data available"),
793 egui::FontId::proportional(ui.active_theme().text.title),
794 self.config.text_color,
795 );
796 return response;
797 }
798
799 let price_rect = layout.price_rect;
800 let volume_rect = layout.volume_rect;
801
802 let (start_idx, end_idx) = self.state.visible_range();
804 let cutoff = self.replay_time();
805 let (registry, bars) = if self.chart_type.is_time_independent() {
806 (
807 &mut self.element_studies.studies,
808 &self.element_studies.bars[..],
809 )
810 } else {
811 (&mut self.studies, self.state.data().bars.as_slice())
812 };
813 let mut changed: Vec<_> = registry
814 .indicators_mut()
815 .iter_mut()
816 .enumerate()
817 .filter_map(|(at, study)| study.set_visible_range(start_idx..end_idx).then_some(at))
818 .collect();
819 if !changed.is_empty() {
820 let requests = registry
821 .indicators()
822 .iter()
823 .flat_map(|study| study.data_requests())
824 .collect();
825 let sources = self
826 .study_data
827 .data(requests, &self.symbol, self.timeframe, cutoff);
828 for at in super::studies::order(registry.indicators(), &self.study_ids) {
829 if let Some(crate::studies::StudySource::Study { id, .. }) =
830 registry.indicators()[at].source()
831 && let Some(parent) = self
832 .study_ids
833 .iter()
834 .position(|candidate| candidate.0 == id)
835 && changed.contains(&parent)
836 && !changed.contains(&at)
837 {
838 changed.push(at);
839 }
840 if changed.contains(&at) {
841 super::studies::update(
842 registry.indicators_mut(),
843 &self.study_ids,
844 at,
845 bars,
846 0,
847 &sources,
848 );
849 }
850 }
851 }
852 self.start_idx = start_idx;
853
854 let factor = self.prepare_conflation(
855 self.state.time_scale().bar_spacing(),
856 ui.ctx().pixels_per_point(),
857 );
858
859 #[cfg(feature = "studies")]
860 let execution_profiles = self
861 .feed_link
862 .as_ref()
863 .and_then(|link| link.footprint.as_ref())
864 .filter(|_| {
865 self.main_series_visible
866 && footprint.is_none()
867 && self.replay.is_none()
868 && self.chart_type == ChartType::SessionVolumeProfile
869 })
870 .and_then(|flow| {
871 let first = flow.complete_from()?;
872 let candles =
873 self.state.shown()[start_idx..end_idx].partition_point(|bar| bar.time < first);
874 let ranges = self.execution_profile_ranges(start_idx + candles..end_idx)?;
875 let mapping = self.get_chart_mapping().with_rect(price_rect);
876 let bars = &self.state.data().bars;
877 let mut profiles = crate::studies::trade_profiles(flow, &ranges);
878 profiles.retain(|profile| {
879 let first = bars.partition_point(|b| b.time < profile.range.start);
880 let end = bars.partition_point(|b| b.time < profile.range.end);
881 first < end
882 && mapping.idx_to_x(end - 1) >= price_rect.left()
883 && mapping.idx_to_x(first) - mapping.bar_spacing * 0.45
884 <= price_rect.right()
885 });
886 Some((flow.price_step(), flow.tick_size(), profiles))
887 });
888 #[cfg(feature = "studies")]
889 let profile_prices = execution_profiles
890 .iter()
891 .flat_map(|(step, tick, profiles)| {
892 profiles.iter().flat_map(move |profile| {
893 profile
894 .volume
895 .levels
896 .first_key_value()
897 .zip(profile.volume.levels.last_key_value())
898 .into_iter()
899 .flat_map(move |((&low, _), (&high, _))| {
900 [
901 low as f64 * step - tick * 0.5,
902 (high as f64 + 1.0) * step - tick * 0.5,
903 ]
904 })
905 })
906 });
907 #[cfg(not(feature = "studies"))]
908 let profile_prices = std::iter::empty();
909 let series_values = self.fit_secondary_series();
911 let primitive_prices = self.main_primitive_prices();
912 let target_prices = self.target_prices();
913 let source_marks = self
914 .mark_source
915 .as_ref()
916 .map(|source| &source.page.bar_marks);
917 let marker_prices = self
918 .marker_prices(
919 &self.marks,
920 source_marks.map_or(&[], |marks| &marks[..]),
921 self.marker_options,
922 )
923 .filter(|_| self.main_series_visible && time_based)
924 .collect::<Vec<_>>();
925 let heikin_ashi = (self.chart_type == crate::model::ChartType::Heikin).then(|| {
926 self.heikin_ashi
927 .bars(&self.state.data().bars, self.state.revision())
928 });
929 let (transform, data_range) = self.state.fit_price_scale_with_series(
930 (if self.chart_type.is_time_independent() {
931 &self.element_studies.studies
932 } else {
933 &self.studies
934 })
935 .indicators()
936 .iter()
937 .zip(&self.study_autoscale)
938 .filter(|(study, autoscale)| {
939 !self.config.scale_price_chart_only && **autoscale && !study.is_volume()
940 })
941 .map(|(study, _)| study.as_ref()),
942 heikin_ashi.as_ref().map(|bars| &bars[start_idx..end_idx]),
943 self.main_series_visible,
944 self.main_series_style.autoscale.as_ref(),
945 (factor > 1
946 && heikin_ashi.is_none()
947 && self.state.price_scale().options().mode
948 != crate::scales::PriceScaleMode::Logarithmic)
949 .then(|| {
950 self.merge_groups
951 .price_range(self.state.shown(), start_idx..end_idx)
952 })
953 .flatten(),
954 marker_prices
955 .into_iter()
956 .chain(primitive_prices)
957 .chain(target_prices)
958 .chain(profile_prices),
959 series_values,
960 );
961 drop(heikin_ashi);
962 if self.main_series_visible
963 && footprint.is_none()
964 && self.chart_type == ChartType::TimePriceOpportunity
965 && !self.state.price_scale().is_manual()
966 {
967 let mut clock = self.bar_clock();
968 if let Some(zone) = self
969 .instrument()
970 .filter(|i| i.session.is_none())
971 .and_then(|i| i.time_zone.as_ref())
972 {
973 clock = clock.with_timezone(zone.clone());
974 }
975 let tick = self
976 .instrument()
977 .and_then(|instrument| instrument.tick_size);
978 let config = self.chart_type_params.tpo_config(tick);
979 let initial = self.state.price_scale().range();
983 let mut fitted = data_range;
984 loop {
985 let map =
986 self.state
987 .price_scale()
988 .map(transform, price_rect.top(), price_rect.bottom());
989 let range = map.range();
990 let (mut low, mut high) = fitted;
991 let (mut traded_low, mut traded_high) = data_range;
992 let mapping = self.get_chart_mapping().with_rect(price_rect);
993 rendering::candles::tpo::visit_profiles(
994 &self.state.data().bars,
995 start_idx..end_idx,
996 self.state.revision(),
997 (&config, tick),
998 &clock,
999 (&map, price_rect, |i| mapping.idx_to_x(i)),
1000 |profile, _, anchor, _, _| {
1001 if anchor >= price_rect.right() {
1002 return;
1003 }
1004 for value in [profile.profile_low, profile.profile_high]
1005 .map(|p| transform.to_scale(p))
1006 {
1007 if value.is_finite() {
1008 traded_low = traded_low.min(value);
1009 traded_high = traded_high.max(value);
1010 }
1011 }
1012 for price in [
1013 profile.row_span(profile.profile_low).0,
1014 profile.row_span(profile.profile_high).1,
1015 ] {
1016 let value = transform.to_scale(price);
1017 if value.is_finite() {
1018 low = low.min(value);
1019 high = high.max(value);
1020 }
1021 }
1022 },
1023 );
1024 if (low, high) == fitted {
1025 break;
1026 }
1027 self.state.price_scale_mut().autoscale(low, high);
1028 fitted = (low, high);
1029 let next = self.state.price_scale().range();
1030 if traded_high - traded_low <= 1e-9
1033 && next.max - next.min < initial.max - initial.min
1034 && initial.contains(low)
1035 && initial.contains(high)
1036 {
1037 self.state
1038 .price_scale_mut()
1039 .autoscale(data_range.0, data_range.1);
1040 break;
1041 }
1042 if next == range {
1043 break;
1044 }
1045 }
1046 }
1047 self.fit_marker_scales(&painter, price_rect);
1048 self.apply_price_zoom(pending_price_zoom, &response, price_rect, transform);
1049 let bar_spacing = self.state.time_scale().bar_spacing();
1050 if self.config.lock_price_to_bar_ratio != self.last_ratio_lock {
1051 self.state.price_scale_mut().set_bar_ratio_locked(
1052 self.config.lock_price_to_bar_ratio,
1053 bar_spacing,
1054 price_rect.height(),
1055 );
1056 }
1057 self.config.lock_price_to_bar_ratio = self.state.price_scale().bar_ratio_locked();
1058 self.last_ratio_lock = self.config.lock_price_to_bar_ratio;
1059 self.state
1060 .price_scale_mut()
1061 .update_bar_ratio(bar_spacing, price_rect.height());
1062 let price_map = self
1063 .series_display_map(
1064 super::SeriesId::MAIN,
1065 self.state
1066 .price_scale()
1067 .map(transform, price_rect.min.y, price_rect.max.y),
1068 )
1069 .with_thousands_sep(self.config.thousands_sep);
1070
1071 self.update_shared_study_maps(price_rect);
1072 let price_formatter = self.series_label_formatter(
1073 self.series_axis_owner(super::SeriesId::MAIN),
1074 price_map.transform().mode,
1075 );
1076
1077 self.last_price_map = price_map;
1079 self.last_rendered_price_rect = price_rect;
1080 self.last_rendered_volume_rect = volume_rect;
1081
1082 self.begin_mark_hits();
1083 self.pump_marks(ui.ctx());
1084
1085 let visible_data = self.state.visible_data();
1087
1088 let groups = (factor > 1).then(|| {
1089 let covering = self
1090 .merge_groups
1091 .covering(start_idx..start_idx + visible_data.len());
1092 (covering.start, &self.merge_groups.groups[covering])
1093 });
1094 let time_bars: Option<Vec<_>> =
1096 groups.map(|(_, groups)| groups.iter().map(|group| group.bar.clone()).collect());
1097
1098 let max_volume = time_bars
1100 .as_deref()
1101 .unwrap_or(visible_data)
1102 .iter()
1103 .map(|c| c.volume)
1104 .filter(|v| v.is_finite())
1105 .max_by(f64::total_cmp)
1106 .unwrap_or(1.0);
1107
1108 let horizontal_grid = self.config.show_grid && self.config.show_horizontal_grid;
1110 let time_grid = time_based && self.config.show_grid && self.config.show_vertical_grid;
1111 if horizontal_grid && !time_grid {
1112 rendering::render_grid(
1113 &painter,
1114 price_rect,
1115 &price_map,
1116 self.config.grid_color,
1117 self.config.grid_line_style,
1118 );
1119 }
1120
1121 let bar_spacing = self.state.time_scale().bar_spacing();
1123 let bar_width = bar_spacing * self.config.candle_width;
1124 let price_ctx = RenderContext::new(&painter, price_rect);
1125 let coords = ChartMapping::new(
1126 price_rect,
1127 bar_spacing,
1128 start_idx,
1129 self.state.time_scale().base_idx(),
1130 self.state.time_scale().right_offset_precise(),
1131 price_map,
1132 );
1133 let (time_data, time_coords) = match (&time_bars, groups) {
1134 (Some(bars), Some((start, _))) => (
1135 bars.as_slice(),
1136 super::merge::group_mapping(&coords, factor, start),
1137 ),
1138 _ => (visible_data, coords),
1139 };
1140 let colors = StyleColors {
1141 bullish: self.config.bullish_color,
1142 bearish: self.config.bearish_color,
1143 text: self.config.text_color,
1144 bullish_border: self.config.bullish_border_color,
1145 bearish_border: self.config.bearish_border_color,
1146 bullish_wick: self.config.bullish_wick_color,
1147 bearish_wick: self.config.bearish_wick_color,
1148 candle_border_width: self.config.candle_border_width,
1149 };
1150
1151 let timezone = self.display_timezone();
1152 let time_scale = self.build_time_formatter();
1153 let host_scale = self.horizontal_scale.clone();
1154 let scale: &dyn crate::scales::HorizontalScale =
1155 host_scale.as_deref().unwrap_or(&time_scale);
1156 let (time_marks, time_points, axis_coords) = if time_grid || self.config.show_time_labels {
1158 let calendar = self.calendar_timezone();
1160 rendering::time_axis_marks(
1161 &RenderContext::new(&painter, chart_rect),
1162 time_data,
1163 &time_coords,
1164 scale,
1165 &self.chart_options.time_scale,
1166 calendar.as_ref(),
1167 |index| {
1168 crate::model::time_at_with_timeframe(
1169 self.state.shown(),
1170 index * factor as f64,
1171 self.timeframe(),
1172 calendar.as_ref(),
1173 )
1174 },
1175 )
1176 } else {
1177 (Vec::new(), Vec::new(), time_coords)
1178 };
1179
1180 if self.config.show_grid && self.config.show_vertical_grid {
1181 let style = self
1182 .config
1183 .vertical_grid_line_style
1184 .unwrap_or(self.config.grid_line_style);
1185 if time_based {
1186 rendering::render_time_grid(
1187 &RenderContext::new(&painter, chart_rect),
1188 &time_marks,
1189 &time_points,
1190 &axis_coords,
1191 self.config
1192 .vertical_grid_color
1193 .unwrap_or(self.config.grid_color),
1194 style,
1195 horizontal_grid
1196 .then_some((self.config.grid_color, self.config.grid_line_style)),
1197 );
1198 } else {
1199 rendering::render_vertical_grid(
1200 &painter,
1201 chart_rect,
1202 &coords,
1203 self.config
1204 .vertical_grid_color
1205 .unwrap_or(self.config.grid_color),
1206 style,
1207 );
1208 }
1209 }
1210
1211 let session_tf = self.resolve_session_timeframe(visible_data);
1212 let exchange_session = rendering::exchange_session(
1213 self.instrument(),
1214 self.config.session_config.as_ref(),
1215 &timezone,
1216 session_tf,
1217 );
1218 let session_painter = painter.with_clip_rect(price_rect);
1219 let session_ctx = RenderContext::new(&session_painter, price_rect);
1220 if self.config.show_session_breaks && host_scale.is_none() {
1222 let provider = self
1223 .session_provider
1224 .as_deref()
1225 .unwrap_or(&exchange_session);
1226 let background =
1227 rendering::SessionBackgroundRenderer::from_background(self.config.background_color);
1228 background.render(
1229 &session_ctx,
1230 time_data,
1231 provider,
1232 &time_coords,
1233 time_coords.start_idx,
1234 );
1235 rendering::SessionBreakRenderer::new(rendering::SessionBreakRenderConfig {
1236 line_color: self.config.session_break_color,
1237 line_width: 1.0,
1238 style: self.config.session_break_style,
1239 })
1240 .render(
1241 &session_ctx,
1242 time_data,
1243 provider,
1244 &time_coords,
1245 time_coords.start_idx,
1246 );
1247 }
1248 if time_based
1249 && host_scale.is_none()
1250 && session_tf.duration_ms() < crate::model::Timeframe::Day1.duration_ms()
1251 && self.config.show_extended_hours
1252 {
1253 let color = if self.config.extended_hours_color == egui::Color32::PLACEHOLDER {
1254 ui.active_theme().ui.accent.linear_multiply(0.08)
1255 } else {
1256 self.config.extended_hours_color
1257 };
1258 rendering::render_extended_hours(
1259 &session_ctx,
1260 time_data,
1261 &exchange_session,
1262 &time_coords,
1263 time_coords.start_idx,
1264 color,
1265 );
1266 }
1267
1268 let chart_rect_width = chart_rect.width();
1271 let idx_to_coord = |idx: usize, min_x: f32| -> f32 {
1272 self.state
1273 .time_scale()
1274 .idx_to_coord(idx, min_x, chart_rect_width)
1275 };
1276
1277 let clipped_painter = painter.with_clip_rect(chart_rect);
1279 let clipped_price_ctx = RenderContext::new(&clipped_painter, price_rect);
1280 let clipped_volume_ctx = RenderContext::new(&clipped_painter, volume_rect);
1281
1282 let mut bar_clock = self.bar_clock();
1283 if let Some(zone) = self
1285 .instrument()
1286 .filter(|instrument| instrument.session.is_none())
1287 .and_then(|instrument| instrument.time_zone.as_ref())
1288 {
1289 bar_clock = bar_clock.with_timezone(zone.clone());
1290 }
1291 let render_ctx = rendering::CandleDataContext {
1292 price_ctx: &clipped_price_ctx,
1293 volume_ctx: &clipped_volume_ctx,
1294 price_scale: &price_map,
1295 colors: &colors,
1296 visible_data,
1297 full_data: &self.state.data().bars,
1300 shows_elements: self.state.shows_elements(),
1301 revision: self.state.revision(),
1302 heikin_ashi: &self.heikin_ashi,
1303 start_idx,
1304 clock: &bar_clock,
1305 tick_size: self
1306 .instrument()
1307 .and_then(|instrument| instrument.tick_size),
1308 };
1309
1310 let render_params = rendering::ChartTypeParams::new(
1311 rendering::BarDimensions::new(bar_width, self.config.wick_width),
1312 rendering::VolumeSettings::new(
1313 self.config.show_volume && !cfg!(feature = "studies"),
1314 max_volume,
1315 ),
1316 &self.chart_type_params,
1317 rendering::TradingColors::new(self.config.bullish_color, self.config.bearish_color),
1318 rendering::CoordMapping::new(chart_rect.min.x),
1319 self.config.price_source,
1320 );
1321 let merged = footprint
1323 .map(|_| false)
1324 .or_else(|| super::merge::line_style(self.chart_type))
1325 .filter(|_| factor > 1)
1326 .map(|line| {
1327 let merge = |line| {
1328 super::merge::Merged::new(
1329 self.state.shown(),
1330 &self.merge_groups,
1331 start_idx..start_idx + visible_data.len(),
1332 line,
1333 self.config.price_source,
1334 )
1335 };
1336 let volume = (line && render_params.show_volume()).then(|| merge(false));
1338 (merge(line), volume)
1339 });
1340 let merged_x = |merged: &super::merge::Merged, index: usize, min_x: f32| {
1341 let past = |last: f32| last + (index + 1 - merged.index.len()) as f32 * factor as f32;
1343 let index = merged
1344 .index
1345 .get(index)
1346 .copied()
1347 .unwrap_or_else(|| merged.index.last().map_or(0.0, |&last| past(last)));
1348 self.state
1349 .time_scale()
1350 .idx_to_coord_precise(index, min_x, chart_rect_width)
1351 };
1352 let merged_params = rendering::ChartTypeParams::new(
1353 rendering::BarDimensions::new(bar_width * factor as f32, self.config.wick_width),
1354 rendering::VolumeSettings::new(
1355 render_params.show_volume() && merged.as_ref().is_some_and(|m| m.1.is_none()),
1356 max_volume,
1357 ),
1358 &self.chart_type_params,
1359 rendering::TradingColors::new(self.config.bullish_color, self.config.bearish_color),
1360 rendering::CoordMapping::new(chart_rect.min.x),
1361 self.config.price_source,
1362 );
1363
1364 let host = footprint.filter(|_| time_based);
1365 let feed = self
1366 .feed_link
1367 .as_ref()
1368 .and_then(|link| link.footprint.as_ref())
1369 .filter(|_| {
1370 time_based
1371 && self.replay.is_none()
1372 && (self.chart_type == ChartType::VolumeFootprint
1373 || (self.chart_type == ChartType::SessionVolumeProfile
1374 && cfg!(feature = "studies")))
1375 });
1376 let feed_config = feed.map(|_| FootprintConfig {
1377 text_color: colors.text,
1378 neutral_color: colors.text,
1379 ask_color: colors.bullish,
1380 bid_color: colors.bearish,
1381 ..Default::default()
1382 });
1383 let session_profiles = host.is_none() && self.chart_type == ChartType::SessionVolumeProfile;
1384 let executions = host
1387 .or_else(|| feed.zip(feed_config.as_ref()))
1388 .filter(|(_, config)| {
1389 session_profiles
1390 || crate::chart::overlays::footprint_column_width(bar_spacing, config).is_some()
1391 })
1392 .map(|(flow, config)| {
1393 let first = flow.complete_from();
1394 let candles = first.map_or(visible_data.len(), |first| {
1395 visible_data.partition_point(|bar| bar.time < first)
1396 });
1397 (flow, config, candles)
1398 });
1399
1400 let style = &self.main_series_style;
1401 let last = match style.price_line_source {
1404 crate::model::PriceLineSource::LastBar => {
1405 crate::model::last_with_value(&self.state.data().bars)
1406 }
1407 crate::model::PriceLineSource::LastVisible => {
1408 crate::model::last_with_value(visible_data)
1409 }
1410 };
1411 let last_price_line = last.filter(|_| self.main_series_visible).map(|last| {
1412 let color = style
1413 .price_line_color
1414 .or_else(|| last.colors.as_ref().and_then(|own| own.color))
1415 .unwrap_or(if last.close >= last.open {
1416 self.config.bullish_color
1417 } else {
1418 self.config.bearish_color
1419 });
1420 PriceLine::new(last.close, color)
1421 .with_width(if self.config.show_price_line && style.price_line_visible {
1422 style.price_line_width
1423 } else {
1424 0.0
1425 })
1426 .with_style(style.price_line_style)
1427 .with_axis_label(
1428 (self.config.show_right_axis || self.config.show_left_axis)
1429 && self.config.show_symbol_last_val
1430 && style.last_value_visible,
1431 )
1432 });
1433 let price_line_below = !session_profiles
1435 && executions.is_some_and(|(flow, _, candles)| {
1436 visible_data[candles..]
1437 .iter()
1438 .any(|bar| flow.bar_at(bar.time).is_some())
1439 });
1440
1441 self.paint_primitives(&painter, &coords, PrimitivePass::Below);
1442 let studies = if self.chart_type.is_time_independent() {
1443 &self.element_studies.studies
1444 } else {
1445 &self.studies
1446 };
1447 let volume_map = volume_rect.is_positive().then(|| {
1448 crate::scales::PriceMap::new(
1449 crate::scales::PriceTransform::default(),
1450 crate::scales::PriceRange::new(
1451 0.0,
1452 studies
1453 .indicators()
1454 .iter()
1455 .find(|study| {
1456 study.is_volume()
1457 && study.is_overlay()
1458 && study.is_visible()
1459 && !matches!(study.scale(), crate::studies::StudyScale::Shared(_))
1460 })
1461 .map_or(max_volume.max(1.0), |study| {
1462 super::volume::range(
1463 study.as_ref(),
1464 &(start_idx..end_idx),
1465 Some(&self.merge_groups),
1466 )
1467 .1
1468 }),
1469 ),
1470 volume_rect.top(),
1471 volume_rect.bottom(),
1472 false,
1473 )
1474 .with_price_precision(Some(
1475 self.instrument()
1476 .and_then(|instrument| instrument.volume_decimals)
1477 .unwrap_or(0),
1478 ))
1479 });
1480 self.host_events.volume_map = volume_map;
1481 let series_at = self
1483 .pane_layout
1484 .main
1485 .iter()
1486 .position(|id| *id == super::SeriesId::MAIN)
1487 .unwrap_or(0);
1488 let (behind, front) = self.pane_layout.main.split_at(series_at);
1489 let main_visible_range = start_idx..(start_idx + visible_data.len());
1490 let volume_coords = show_volume.then(|| coords.with_rect(volume_rect));
1491 let pointer = ui
1493 .input(|input| input.pointer.interact_pos())
1494 .filter(|pos| chart_rect.contains(*pos));
1495 let primitive_hit = pointer.and_then(|pos| {
1496 primitives::primitive_at(
1497 self.main_primitives(&coords)
1498 .filter(|(item, _)| !item.replaces),
1499 pos,
1500 )
1501 });
1502 let renderer_hit = pointer.and_then(|pos| {
1503 self.pane_layout.main.iter().rev().find_map(|id| {
1504 primitives::primitive_at(
1505 self.main_primitives(&coords)
1506 .filter(|(item, _)| item.replaces && item.series == *id),
1507 pos,
1508 )
1509 })
1510 });
1511 let rendered: Vec<_> = self
1512 .study_ids
1513 .iter()
1514 .copied()
1515 .filter(|id| self.has_renderer(*id))
1516 .collect();
1517 let hit_order: Vec<_> = self
1518 .pane_layout
1519 .main
1520 .iter()
1521 .copied()
1522 .filter(|id| {
1523 !rendered.contains(id) && (*id != super::SeriesId::MAIN || self.main_series_visible)
1524 })
1525 .collect();
1526 let last_bar = self.state.shown().len().saturating_sub(1);
1527 let secondary_hits = [pointer, response.interact_pointer_pos()].map(|pos| {
1528 pos.map(|pos| {
1529 (
1530 pos,
1531 hit_order.iter().rev().find_map(|id| {
1532 let item = self.main_pane_series().find(|item| item.id == *id)?;
1533 self.hit_price_series(
1534 item,
1535 pos,
1536 price_rect,
1537 self.series_map(item, price_rect),
1538 )
1539 .map(|bar| (super::ChartElementId::Series(item.id), bar))
1540 }),
1541 )
1542 })
1543 });
1544 let element_at = |pos: Pos2| {
1545 let renderer = renderer_hit
1546 .filter(|_| Some(pos) == pointer)
1547 .and_then(|hit| {
1548 if let super::PointerTarget::Element(id) = hit.target {
1549 Some((id, coords.x_to_idx(pos.x).min(last_bar)))
1550 } else {
1551 None
1552 }
1553 });
1554 let hit = hit_test_main_chart(
1555 pos,
1556 studies,
1557 &self.study_ids,
1558 &hit_order,
1559 &coords,
1560 volume_coords.as_ref(),
1561 self.state.shown(),
1562 max_volume,
1563 main_visible_range.clone(),
1564 self.main_series_style.hit_tolerance,
1565 Some(&self.merge_groups),
1566 )
1567 .filter(|(id, _)| match id {
1568 crate::widget::ChartElementId::Indicator(study) => !rendered.contains(study),
1570 _ => self.main_series_visible,
1571 });
1572 let secondary = secondary_hits
1573 .iter()
1574 .flatten()
1575 .find_map(|(at, hit)| (*at == pos).then_some(*hit).flatten());
1576 hit.into_iter()
1577 .chain(secondary)
1578 .chain(renderer)
1579 .max_by_key(|(element, _)| {
1580 let id = match element {
1581 super::ChartElementId::Indicator(id)
1582 | super::ChartElementId::Series(id) => id,
1583 };
1584 self.pane_layout.main.iter().position(|member| member == id)
1585 })
1586 };
1587 let on_top = ui
1589 .input(|input| input.pointer.hover_pos())
1590 .filter(|pos| self.chart_options.hovered_series_on_top && chart_rect.contains(*pos))
1591 .and_then(element_at)
1592 .map(|(id, _)| id);
1593 let main_on_top = on_top == Some(super::ChartElementId::Series(super::SeriesId::MAIN));
1594 let paint_overlay = |id: &super::SeriesId| {
1595 if let Some(item) = self.main_pane_series().find(|item| item.id == *id) {
1596 self.paint_series(
1597 &clipped_painter,
1598 price_rect,
1599 item,
1600 self.series_map(item, price_rect),
1601 );
1602 return;
1603 }
1604 let Some(study) = self
1605 .study_ids
1606 .iter()
1607 .position(|study| study == id)
1608 .map(|idx| &studies.indicators()[idx])
1609 .filter(|study| study.is_visible() && study.is_overlay())
1610 else {
1611 return;
1612 };
1613 if study.is_volume()
1614 && !matches!(study.scale(), crate::studies::StudyScale::Shared(_))
1615 && self.main_series_visible
1616 {
1617 let rect = super::volume::overlay_rect(price_rect, &self.config);
1618 let (min, max) = super::volume::range(
1619 study.as_ref(),
1620 &(start_idx..end_idx),
1621 Some(&self.merge_groups),
1622 );
1623 super::volume::paint(
1624 &painter,
1625 rect,
1626 study.as_ref(),
1627 &(start_idx..end_idx),
1628 &coords,
1629 min,
1630 max,
1631 Some(&self.merge_groups),
1632 );
1633 }
1634 if let Some(config) = study
1636 .tpo()
1637 .filter(|_| !self.chart_type.is_time_independent())
1638 {
1639 rendering::candles::tpo::render_live_tpo(
1640 &clipped_painter,
1641 price_rect,
1642 &self.state.data().bars,
1643 start_idx..start_idx + visible_data.len(),
1644 self.state.revision(),
1645 &price_map,
1646 config,
1647 render_ctx.tick_size,
1648 &bar_clock,
1649 idx_to_coord,
1650 chart_rect.min.x,
1651 self.config.bullish_color,
1652 self.config.bearish_color,
1653 );
1654 }
1655 if self.has_renderer(*id) {
1656 let view = self.primitive_view(
1657 *id,
1658 crate::chart::rendering::study_mapping(study.as_ref(), &coords),
1659 );
1660 for item in self.primitives_of(*id).filter(|item| item.replaces) {
1661 item.draw(&clipped_painter, &view, PrimitivePass::Series, factor);
1662 }
1663 } else {
1664 rendering::IndicatorRenderer::render(
1665 &price_ctx,
1666 std::slice::from_ref(study),
1667 visible_data,
1668 &price_map,
1669 &coords,
1670 );
1671 }
1672 };
1673 let paint_main_series = || {
1674 if self.main_series_visible {
1675 if time_based {
1676 self.paint_liquidity(&clipped_painter, &self.get_chart_mapping());
1677 }
1678 let (mut data, params) = match &merged {
1680 Some((merged, _)) => (
1681 rendering::CandleDataContext {
1682 visible_data: &merged.bars[merged.start..],
1683 full_data: &merged.bars,
1684 start_idx: merged.start,
1685 ..render_ctx
1686 },
1687 &merged_params,
1688 ),
1689 None => (
1690 rendering::CandleDataContext { ..render_ctx },
1691 &render_params,
1692 ),
1693 };
1694 if let Some((_, _, candles)) = executions {
1695 data.visible_data = match &merged {
1696 Some((merged, _)) if candles < visible_data.len() => {
1698 let end = merged.count_before(start_idx + candles, factor);
1699 &merged.bars[merged.start..end.max(merged.start)]
1700 }
1701 Some(_) => data.visible_data,
1702 None => &visible_data[..candles],
1703 };
1704 }
1705 let series_type = if host.is_some() {
1707 ChartType::Candles
1708 } else {
1709 self.chart_type
1710 };
1711 let paint_series =
1712 |data: &rendering::CandleDataContext,
1713 params: &rendering::ChartTypeParams,
1714 merged: Option<&super::merge::Merged>| {
1715 rendering::render_chart_type(series_type, data, params, |index, min_x| {
1716 match merged {
1717 Some(merged) => merged_x(merged, index, min_x),
1718 None => idx_to_coord(index, min_x),
1719 }
1720 });
1721 };
1722 paint_series(&data, params, merged.as_ref().map(|(merged, _)| merged));
1723 if let Some((flow, config, candles)) = executions {
1724 let has_cells = |bar: &crate::model::Bar| {
1725 session_profiles || flow.bar_at(bar.time).is_some()
1726 };
1727 let mut first = start_idx + candles;
1728 for run in visible_data[candles..]
1730 .chunk_by(|bar, next| has_cells(bar) == has_cells(next))
1731 {
1732 if !has_cells(&run[0]) {
1733 let data = rendering::CandleDataContext {
1734 visible_data: run,
1735 start_idx: first,
1736 ..render_ctx
1737 };
1738 paint_series(&data, &render_params, None);
1739 } else if params.show_volume() {
1740 rendering::candles::volume::render_volume_bars(
1741 &clipped_volume_ctx,
1742 run,
1743 first,
1744 max_volume,
1745 &colors,
1746 chart_rect.min.x,
1747 idx_to_coord,
1748 );
1749 }
1750 first += run.len();
1751 }
1752 let mapping = self.get_chart_mapping();
1753 let bars = self.bars(super::SeriesId::MAIN);
1754 if !session_profiles {
1755 if price_line_below && let Some(line) = &last_price_line {
1756 rendering::render_price_line(
1757 &painter,
1758 price_rect,
1759 &price_map,
1760 line,
1761 price_rect.left(),
1762 false,
1763 price_rect.right(),
1764 );
1765 }
1766 crate::chart::overlays::render_footprint(
1767 &clipped_painter,
1768 &mapping,
1769 bars,
1770 flow,
1771 config,
1772 );
1773 #[cfg(feature = "studies")]
1774 crate::chart::overlays::render_footprint_annotations(
1775 &clipped_painter,
1776 &mapping,
1777 bars,
1778 flow,
1779 config,
1780 response.id,
1781 );
1782 }
1783 #[cfg(feature = "studies")]
1784 if session_profiles && let Some((step, tick, profiles)) = &execution_profiles {
1785 crate::chart::overlays::render_session_volume_profiles(
1786 &clipped_painter,
1787 &mapping,
1788 bars,
1789 profiles,
1790 *step,
1791 *tick,
1792 self.trade_profile_settings(),
1793 );
1794 }
1795 }
1796 if let Some(table) = statistics_rect
1797 && let Some((flow, config)) = host
1798 {
1799 crate::chart::overlays::render_footprint_statistics(
1800 &painter,
1801 &self.get_chart_mapping().with_rect(table),
1802 self.bars(super::SeriesId::MAIN),
1803 flow,
1804 config.text_color,
1805 );
1806 }
1807 if let Some((_, Some(volume))) = &merged {
1808 rendering::candles::volume::render_volume_bars(
1809 &clipped_volume_ctx,
1810 &volume.bars[volume.start..],
1811 volume.start,
1812 max_volume,
1813 &colors,
1814 chart_rect.min.x,
1815 |index, min_x| merged_x(volume, index, min_x),
1816 );
1817 }
1818 self.paint_last_price_pulse(ui, &clipped_painter, price_rect, &coords);
1819 }
1820 };
1821 let paint_unhovered = |id: &super::SeriesId| {
1822 let element = if self.price_series.iter().any(|item| item.id == *id) {
1823 super::ChartElementId::Series(*id)
1824 } else {
1825 super::ChartElementId::Indicator(*id)
1826 };
1827 if on_top != Some(element) {
1828 paint_overlay(id);
1829 }
1830 };
1831 behind.iter().for_each(paint_unhovered);
1832 if !main_on_top {
1833 paint_main_series();
1834 }
1835
1836 self.paint_marks(&clipped_price_ctx, &coords, MarkerZOrder::Normal);
1837
1838 if let Some(bubbles) = self
1839 .feed_link
1840 .as_ref()
1841 .and_then(|link| link.bubbles.as_ref())
1842 .filter(|_| time_based && self.main_series_visible && self.replay.is_none())
1843 {
1844 let x = |i| idx_to_coord(start_idx + i, chart_rect.min.x);
1845 bubbles.render(&clipped_painter, visible_data, coords.bar_spacing, x, |p| {
1846 price_map.price_to_y(p)
1847 });
1848 }
1849
1850 front.iter().for_each(paint_unhovered);
1851 match on_top {
1852 Some(super::ChartElementId::Indicator(id)) => paint_overlay(&id),
1853 _ if main_on_top => paint_main_series(),
1854 Some(super::ChartElementId::Series(id)) => {
1855 if let Some(item) = self.main_pane_series().find(|item| item.id == id) {
1856 self.paint_series(
1857 &clipped_painter,
1858 price_rect,
1859 item,
1860 self.series_map(item, price_rect),
1861 );
1862 }
1863 }
1864 _ => {}
1865 }
1866 for study in studies
1867 .indicators()
1868 .iter()
1869 .filter(|study| study.is_visible() && study.labels_on_price())
1870 {
1871 crate::chart::indicators::plots::paint_labels(
1872 &clipped_painter,
1873 study.as_ref(),
1874 &price_map,
1875 &coords,
1876 );
1877 }
1878 self.paint_signals(ui, &coords);
1879 let target_lines = self
1880 .show_price_targets
1881 .then(|| painter.add(egui::Shape::Noop));
1882
1883 self.paint_primitives(&painter, &coords, PrimitivePass::Series);
1884
1885 if response.clicked()
1890 && let Some(click_pos) = response.interact_pointer_pos()
1891 && self.mark_at(click_pos).is_none()
1892 {
1893 match element_at(click_pos) {
1894 Some((id, bar_idx)) => self.selection.select(id, Some(bar_idx)),
1895 None => self.selection.deselect(),
1896 }
1897 }
1898
1899 if response.secondary_clicked()
1906 && let Some(click_pos) = response.interact_pointer_pos()
1907 && chart_rect.contains(click_pos)
1908 {
1909 let price = coords.y_to_price(click_pos.y);
1910
1911 #[cfg(feature = "drawings")]
1913 let drawing_hit = self.drawings.hit_test(click_pos);
1914 #[cfg(not(feature = "drawings"))]
1915 let drawing_hit: Option<usize> = None;
1916
1917 self.right_click = Some(if let Some(drawing_id) = drawing_hit {
1918 self.selection.deselect();
1922 #[cfg(feature = "drawings")]
1923 {
1924 if self.drawings.is_selected(drawing_id) {
1925 let ids: Vec<_> = std::iter::once(drawing_id)
1926 .chain(self.drawings.selected_ids())
1927 .collect();
1928 self.drawings.select_drawings(ids);
1929 } else {
1930 self.drawings.select(drawing_id);
1931 }
1932 }
1933 RightClickTarget::Drawing {
1934 drawing_id,
1935 pos: click_pos,
1936 }
1937 } else {
1938 match element_at(click_pos) {
1939 Some((id, bar_idx)) => {
1940 self.selection.select(id, Some(bar_idx));
1941 RightClickTarget::Element {
1942 id,
1943 bar_idx,
1944 pos: click_pos,
1945 price,
1946 }
1947 }
1948 None => {
1949 self.selection.deselect();
1950 RightClickTarget::Background {
1951 pos: click_pos,
1952 price,
1953 }
1954 }
1955 }
1956 });
1957 }
1958
1959 if let Some(pos) = pointer {
1961 #[cfg(feature = "drawings")]
1962 let drawing = self.drawings.hit_test(pos);
1963 #[cfg(not(feature = "drawings"))]
1964 let drawing: Option<usize> = None;
1965 if let Some(hit) = primitive_hit
1966 .or(renderer_hit.filter(|hit| {
1967 element_at(pos).map(|(id, _)| super::PointerTarget::Element(id))
1968 == Some(hit.target)
1969 }))
1970 .filter(|_| {
1971 drawing.is_none()
1972 && response.hovered()
1973 && !response.dragged()
1974 && !self.zoom_mode_active
1975 && !eraser_mode
1976 && !drawing_tool_active
1977 })
1978 {
1979 ui.ctx().set_cursor_icon(hit.cursor);
1980 }
1981 self.host_events.target = drawing
1982 .map(super::PointerTarget::Drawing)
1983 .or(primitive_hit.map(|hit| hit.target))
1984 .or_else(|| element_at(pos).map(|(id, _)| super::PointerTarget::Element(id)));
1985 }
1986
1987 self.render_main_chart_selection(&painter, &coords, visible_data, main_visible_range);
1990
1991 self.paint_marks(&clipped_price_ctx, &coords, MarkerZOrder::AboveSeries);
1992
1993 if self.config.show_right_axis || self.config.show_left_axis {
1994 let axis_painter =
1995 painter.with_clip_rect(painter.clip_rect().intersect(price_axis_rect));
1996 for (item, view) in self.main_primitives(&coords) {
1997 item.get()
1998 .draw_price_axis(&axis_painter, price_axis_rect, &view);
1999 }
2000 }
2001
2002 let price_labels = painter.add(egui::Shape::Noop);
2004
2005 let axis_edge = if self.config.show_right_axis {
2006 rect.max.x
2007 } else {
2008 price_rect.left()
2009 };
2010 let show_price_axis = self.config.show_right_axis || self.config.show_left_axis;
2011 let study_axis = crate::config::PriceAxisOptions {
2012 show_study_values: self.config.show_indicator_last_val
2013 && self.config.price_axis.show_study_values,
2014 ..self.config.price_axis
2015 };
2016 let currency = self
2017 .instrument()
2018 .and_then(|instrument| instrument.currency.as_deref())
2019 .filter(|code| self.config.show_currency && !code.is_empty());
2020 let unit = self
2021 .instrument()
2022 .and_then(|instrument| instrument.unit.as_deref())
2023 .filter(|unit| self.config.show_unit && !unit.is_empty());
2024 let has_currency = currency.is_some() || unit.is_some();
2025 let mut tags = Vec::new();
2026 if has_currency {
2027 tags.push(Some(Rect::from_min_size(
2028 price_rect.min,
2029 egui::vec2(price_rect.width(), 24.0),
2030 )));
2031 }
2032 let mut pending_values = Vec::new();
2033 for (_, line) in self.price_lines.iter().filter(|_| self.main_series_visible) {
2034 rendering::render_price_line(
2035 &painter,
2036 price_rect,
2037 &price_map,
2038 line,
2039 price_rect.left(),
2040 false,
2041 axis_edge,
2042 );
2043 if line.axis_label {
2044 pending_values.push((line.price, rendering::axes::axis_label_colors(line), false));
2045 }
2046 }
2047 #[cfg(feature = "ui")]
2048 let trading_outlined = std::mem::take(&mut self.trading_outlined);
2049 #[cfg(feature = "ui")]
2050 pending_values.extend(
2051 std::mem::take(&mut self.trading_tags)
2052 .into_iter()
2053 .enumerate()
2054 .map(|(index, (value, fill))| {
2055 let outlined = trading_outlined.contains(&index);
2056 let colors = if outlined {
2057 (fill, ui.active_theme().chart.bg)
2058 } else {
2059 (rendering::axes::tag_text_color(fill), fill)
2060 };
2061 (value, colors, outlined)
2062 }),
2063 );
2064
2065 let session_closes = (self.config.show_symbol_prev_close
2068 || self.config.show_prev_close_line
2069 || self.config.show_last_day_change)
2070 .then(|| {
2071 let bars = &self.state.data().bars;
2072 let last = crate::model::last_with_value(bars)?;
2073 let index = bars
2074 .partition_point(|bar| {
2075 exchange_session.trading_date(bar.time)
2076 < exchange_session.trading_date(last.time)
2077 })
2078 .checked_sub(1)?;
2079 crate::model::last_with_value(&bars[..=index])
2080 .map(|previous| (previous.close, last.close))
2081 })
2082 .flatten();
2083 if self.main_series_visible
2084 && (self.config.show_symbol_prev_close || self.config.show_prev_close_line)
2085 && let Some((previous_close, _)) = session_closes
2086 {
2087 let line = PriceLine::new(previous_close, ui.active_theme().ui.text_secondary)
2088 .with_style(crate::model::LineStyle::Dotted)
2089 .with_axis_label(self.config.show_symbol_prev_close)
2090 .with_width(if self.config.show_prev_close_line {
2091 1.0
2092 } else {
2093 0.0
2094 });
2095 rendering::render_price_line(
2096 &painter,
2097 price_rect,
2098 &price_map,
2099 &line,
2100 price_rect.left(),
2101 false,
2102 axis_edge,
2103 );
2104 if self.config.show_symbol_prev_close {
2105 pending_values.push((
2106 previous_close,
2107 rendering::axes::axis_label_colors(&line),
2108 false,
2109 ));
2110 }
2111 }
2112
2113 let style = &self.main_series_style;
2114 if let Some(line) = super::series::base_line(&price_map)
2115 .filter(|_| self.main_series_visible && style.base_line_visible)
2116 {
2117 rendering::render_price_line(
2118 &painter,
2119 price_rect,
2120 &price_map,
2121 &line,
2122 price_rect.left(),
2123 false,
2124 axis_edge,
2125 );
2126 }
2127
2128 let last_price_tag = if let Some((last, line)) = last.zip(last_price_line) {
2129 let line = if price_line_below {
2130 line.with_width(0.0)
2131 } else {
2132 line
2133 };
2134 let countdown = self.current_bar_countdown().filter(|_| {
2136 line.axis_label
2137 && price_map
2138 .range()
2139 .contains(price_map.transform().to_scale(last.close))
2140 });
2141 if countdown.is_some()
2142 && (self.server_time_offset.is_some() || self.feed_link.is_some())
2143 {
2144 ui.ctx()
2145 .request_repaint_after(std::time::Duration::from_secs(1));
2146 }
2147 let quote_tags = rendering::axes::render_quote_tags_with_countdown(
2148 &painter,
2149 price_rect,
2150 &price_map,
2151 &line,
2152 self.session_quote
2153 .as_ref()
2154 .filter(|_| show_price_axis && self.config.show_extended_session_price)
2155 .map(|quote| (quote, self.config.extended_session_price_color)),
2156 (show_volume && show_price_axis && !cfg!(feature = "studies")).then_some((
2157 last.volume,
2158 if last.is_bullish() {
2161 ui.active_theme().chart.candle_up
2162 } else {
2163 ui.active_theme().chart.candle_down
2164 },
2165 )),
2166 axis_edge,
2167 countdown.as_deref(),
2168 self.instrument()
2169 .and_then(|instrument| instrument.volume_decimals),
2170 if has_currency { 24.0 } else { 0.0 },
2171 price_formatter.as_deref(),
2172 self.volume_formatter.as_deref(),
2173 self.config.price_axis.allow_label_overlap,
2174 if self.config.price_axis.show_symbol_name {
2175 &self.symbol
2176 } else {
2177 ""
2178 },
2179 );
2180 tags.extend([quote_tags[1], quote_tags[2]]);
2181 quote_tags[0]
2182 } else {
2183 None
2184 };
2185
2186 tags.push(last_price_tag);
2187 if show_price_axis {
2188 let mut shown: Vec<_> = pending_values
2190 .iter()
2191 .filter(|(value, ..)| {
2192 price_map
2193 .range()
2194 .contains(price_map.transform().to_scale(*value))
2195 })
2196 .map(|(value, colors, outlined)| {
2197 (price_map.price_to_y(*value), *value, *colors, *outlined)
2198 })
2199 .collect();
2200 shown.sort_by(|a, b| a.0.total_cmp(&b.0));
2201 let mut ys: Vec<_> = shown.iter().map(|shown| shown.0).collect();
2202 if !self.config.price_axis.allow_label_overlap {
2203 ys = rendering::axes::spread_axis_ys(&painter, &ys);
2204 }
2205 for ((_, value, colors, outlined), y) in shown.into_iter().zip(ys) {
2206 let label = price_map.label(
2207 price_map.transform().to_scale(value),
2208 rendering::axes::tag_decimals(&price_map, price_map.price_to_y(value)),
2209 price_formatter.as_deref(),
2210 );
2211 let tag = rendering::axes::render_axis_value(
2212 &painter,
2213 price_rect,
2214 axis_edge,
2215 y,
2216 label,
2217 colors,
2218 &tags,
2219 self.config.price_axis.allow_label_overlap,
2220 );
2221 if outlined && let Some(rect) = tag {
2222 painter.rect_stroke(
2223 rect,
2224 2.0,
2225 egui::Stroke::new(1.0, colors.0),
2226 egui::StrokeKind::Inside,
2227 );
2228 }
2229 tags.push(tag);
2230 }
2231 }
2232 if show_price_axis {
2233 for (value, colors, map, label) in
2234 self.secondary_axis_values(self.main_series_scale, price_rect)
2235 {
2236 tags.push(rendering::axes::render_axis_value(
2237 &painter,
2238 price_rect,
2239 axis_edge,
2240 map.price_to_y(value),
2241 label,
2242 colors,
2243 &tags,
2244 self.config.price_axis.allow_label_overlap,
2245 ));
2246 }
2247 }
2248 if show_price_axis {
2249 let labels: Vec<_> = self
2250 .main_primitives(&coords)
2251 .flat_map(|(item, view)| {
2252 let map = view.mapping.price;
2253 let labels = item.get().price_axis_labels(&view);
2254 labels.into_iter().map(move |label| (label, map))
2255 })
2256 .collect();
2257 for (label, map) in labels {
2258 if map.range().contains(map.transform().to_scale(label.price)) {
2259 tags.push(rendering::axes::render_axis_value(
2260 &painter,
2261 price_rect,
2262 axis_edge,
2263 map.price_to_y(label.price),
2264 label.text,
2265 (rendering::axes::tag_text_color(label.color), label.color),
2266 &tags,
2267 self.config.price_axis.allow_label_overlap,
2268 ));
2269 }
2270 }
2271 }
2272 let mut independent_axis_width =
2273 self.paint_series_axes(ui, &painter, rect, price_rect, &colors);
2274 if show_price_axis {
2275 let axes = studies.indicators().iter().filter(|study| {
2276 study.is_visible()
2277 && study.is_overlay()
2278 && !study.is_volume()
2279 && study.scale() == crate::studies::StudyScale::Independent
2280 });
2281 let mut edge = price_rect.left()
2282 - if self.config.show_left_axis {
2283 self.config.left_axis_width.max(self.measured_axis_width)
2284 } else {
2285 0.0
2286 };
2287 let padding = self
2288 .config
2289 .padding
2290 .max(0.0)
2291 .min(rect.width().min(rect.height()).max(0.0) / 2.0);
2292 let series_left = self
2293 .series_axis_keys()
2294 .iter()
2295 .filter(|key| **key != crate::model::SeriesScale::Right)
2296 .count();
2297 let slot_width = (edge - rect.left() - padding).max(0.0)
2298 / (axes.clone().count() + series_left).max(1) as f32;
2299 edge -= series_left as f32 * slot_width;
2300 for study in axes {
2301 let mapping = crate::chart::rendering::study_mapping(study.as_ref(), &coords);
2302 let axis = Rect::from_min_max(
2303 Pos2::new(edge - slot_width, price_rect.top()),
2304 Pos2::new(edge, price_rect.bottom()),
2305 );
2306 edge -= slot_width;
2307 independent_axis_width =
2308 independent_axis_width.max(crate::chart::rendering::render_study_axis(
2309 &painter,
2310 &[study.as_ref()],
2311 &mapping,
2312 axis,
2313 &study_axis,
2314 None,
2315 ));
2316 }
2317 }
2318 if show_price_axis && study_axis.show_study_values {
2319 for study in studies
2320 .indicators()
2321 .iter()
2322 .filter(|study| study.is_overlay())
2323 {
2324 if study.is_volume()
2325 && !matches!(study.scale(), crate::studies::StudyScale::Shared(_))
2326 {
2327 if self.main_series_visible {
2328 let rect = super::volume::overlay_rect(price_rect, &self.config);
2329 let mapping = super::volume::mapping(
2330 &coords,
2331 rect,
2332 study.as_ref(),
2333 &(start_idx..end_idx),
2334 Some(&self.merge_groups),
2335 );
2336 super::volume::tags(
2337 &painter,
2338 price_rect,
2339 &mapping.price,
2340 study.as_ref(),
2341 axis_edge,
2342 &mut tags,
2343 &study_axis,
2344 );
2345 }
2346 continue;
2347 }
2348 if study.scale() != crate::studies::StudyScale::Main
2349 && study.scale() != crate::studies::StudyScale::Shared(self.main_series_scale)
2350 {
2351 continue;
2352 }
2353 rendering::axes::render_study_tags(
2354 &painter,
2355 price_rect,
2356 &price_map,
2357 study.as_ref(),
2358 axis_edge,
2359 &mut tags,
2360 price_formatter.as_deref(),
2361 &study_axis,
2362 );
2363 }
2364 }
2365
2366 if show_price_axis && self.main_series_visible {
2367 let mut values = Vec::new();
2368 if self.config.show_high_low {
2369 let high = visible_data
2370 .iter()
2371 .map(|bar| bar.high)
2372 .fold(f64::NEG_INFINITY, f64::max);
2373 let low = visible_data
2374 .iter()
2375 .map(|bar| bar.low)
2376 .fold(f64::INFINITY, f64::min);
2377 values.extend([
2378 (high, "High", self.config.bullish_color),
2379 (low, "Low", self.config.bearish_color),
2380 ]);
2381 }
2382 let trading_bid_ask = {
2383 #[cfg(feature = "ui")]
2384 {
2385 std::mem::take(&mut self.trading_bid_ask)
2386 }
2387 #[cfg(not(feature = "ui"))]
2388 {
2389 false
2390 }
2391 };
2392 if (self.config.show_bid_ask || trading_bid_ask)
2393 && let Some(quote) = self.quote.as_ref().filter(|quote| {
2394 quote.bid.is_finite()
2395 && quote.ask.is_finite()
2396 && quote.bid <= quote.ask
2397 && quote.spread().is_finite()
2398 })
2399 {
2400 values.extend([
2401 (quote.bid, "Bid", ui.active_theme().ui.accent),
2402 (quote.ask, "Ask", ui.active_theme().chart.candle_down),
2403 ]);
2404 }
2405 for (value, name, color) in values {
2406 let line = PriceLine::new(value, color)
2407 .with_width(
2408 if (name == "Bid" || name == "Ask") && !self.config.show_bid_ask {
2409 0.0
2410 } else {
2411 1.0
2412 },
2413 )
2414 .with_style(crate::model::LineStyle::Dotted)
2415 .with_axis_label(false);
2416 rendering::render_price_line(
2417 &painter,
2418 price_rect,
2419 &price_map,
2420 &line,
2421 price_rect.left(),
2422 false,
2423 axis_edge,
2424 );
2425 if price_map
2426 .range()
2427 .contains(price_map.transform().to_scale(value))
2428 {
2429 let price = price_map.label(
2430 price_map.transform().to_scale(value),
2431 rendering::axes::tag_decimals(&price_map, price_map.price_to_y(value)),
2432 price_formatter.as_deref(),
2433 );
2434 let quote = name == "Bid" || name == "Ask";
2435 let label = if quote {
2436 price
2437 } else {
2438 format!("{name} {price}")
2439 };
2440 let text_color = rendering::axes::tag_text_color(color);
2441 let tag = rendering::axes::render_axis_value(
2442 &painter,
2443 price_rect,
2444 axis_edge,
2445 price_map.price_to_y(value),
2446 label,
2447 (text_color, color),
2448 &tags,
2449 self.config.price_axis.allow_label_overlap,
2450 );
2451 if quote && let Some(rect) = tag {
2452 let badge = painter.layout_no_wrap(
2453 l10n::text(ui.ctx(), name),
2454 rendering::axes::axis_font(ui.ctx()),
2455 text_color,
2456 );
2457 let box_rect = Rect::from_min_size(
2458 egui::pos2(
2459 if axis_edge <= price_rect.left() {
2460 rect.right() + 1.0
2461 } else {
2462 rect.left() - badge.size().x - 8.0
2463 },
2464 rect.top(),
2465 ),
2466 egui::vec2(badge.size().x + 7.0, rect.height()),
2467 );
2468 painter.rect_filled(box_rect, 2.0, color);
2469 painter.galley(box_rect.center() - badge.size() / 2.0, badge, text_color);
2470 }
2471 tags.push(tag);
2472 }
2473 }
2474 }
2475
2476 if self.config.show_time_labels {
2477 let axis = self.time_axis_rect;
2478 let axis_painter = painter.with_clip_rect(painter.clip_rect().intersect(axis));
2479 for (item, view) in self.time_axis_primitives(&coords) {
2480 item.get().draw_time_axis(&axis_painter, axis, &view);
2481 }
2482 }
2483 let time_labels = painter.add(egui::Shape::Noop);
2484 let mut time_tags = Vec::new();
2486 if self.config.show_time_labels {
2487 let labels: Vec<_> = self
2488 .time_axis_primitives(&coords)
2489 .flat_map(|(item, view)| {
2490 let labels = item.get().time_axis_labels(&view);
2491 labels
2492 .into_iter()
2493 .filter_map(|label| Some((view.time_to_x(label.time)?, label)))
2494 .collect::<Vec<_>>()
2495 })
2496 .collect();
2497 for (x, label) in labels {
2498 if chart_rect.x_range().contains(x) {
2499 time_tags.push(Some(rendering::axes::paint_crosshair_tag(
2500 &painter,
2501 Pos2::new(x, time_axis_top + 1.5),
2502 egui::Align2::CENTER_TOP,
2503 chart_rect.left()..=chart_rect.right(),
2504 label.text,
2505 (rendering::axes::tag_text_color(label.color), label.color),
2506 rendering::axes::axis_font(ui.ctx()),
2507 25.0,
2508 None,
2509 )));
2510 }
2511 }
2512 }
2513
2514 let bar_seconds = self.time_label_seconds();
2515
2516 #[cfg(feature = "drawings")]
2518 {
2519 let timescale = self.state.time_scale().clone();
2521
2522 let drawing_history = self.history.start_drawing_action(&mut self.drawings);
2523 let mut cursor_modes = std::mem::take(&mut self.cursor_modes);
2525 let mut drawings = std::mem::take(&mut self.drawings);
2526
2527 self.handle_drawings(
2528 ui,
2529 &mut drawings,
2530 &mut cursor_modes,
2531 &response,
2532 price_rect,
2533 &price_map,
2534 &painter,
2535 ×cale,
2536 (time_scale.as_ref(), bar_seconds),
2537 );
2538
2539 self.render_eraser_highlight(&painter, &drawings, &cursor_modes);
2541
2542 self.drawings = drawings;
2544 self.cursor_modes = cursor_modes;
2545 self.history
2546 .finish_drawing_action(&mut self.drawings, drawing_history);
2547 }
2548 self.paint_marks(&clipped_price_ctx, &coords, MarkerZOrder::Top);
2549 self.paint_primitives(&painter, &coords, PrimitivePass::Above);
2550
2551 #[cfg(feature = "drawings")]
2554 {
2555 let mut prices = Vec::new();
2556 for tag in self
2557 .drawings
2558 .axis_tags(price_rect, ui.active_theme().chart.axis_text_secondary)
2559 {
2560 match tag {
2561 crate::drawings::manager::DrawingAxisTag::Price { price, color } => {
2562 if show_price_axis
2563 && price.is_finite()
2564 && price_map
2565 .range()
2566 .contains(price_map.transform().to_scale(price))
2567 {
2568 prices.push((price_map.price_to_y(price), price, color));
2569 }
2570 }
2571 crate::drawings::manager::DrawingAxisTag::Time { x, anchor, color } => {
2572 if self.config.show_time_labels
2573 && let Some(time) = self.drawing_point_time(&anchor)
2574 {
2575 time_tags.push(Some(rendering::axes::paint_crosshair_tag(
2576 &painter,
2577 Pos2::new(x, time_axis_top + 1.5),
2578 egui::Align2::CENTER_TOP,
2579 chart_rect.left()..=chart_rect.right(),
2580 scale.format_crosshair(time, bar_seconds),
2581 (rendering::axes::tag_text_color(color), color),
2582 rendering::axes::axis_font(ui.ctx()),
2583 25.0,
2584 None,
2585 )));
2586 }
2587 }
2588 }
2589 }
2590 if !prices.is_empty() {
2591 prices.sort_by(|a, b| a.0.total_cmp(&b.0));
2593 let mut ys: Vec<_> = prices.iter().map(|price| price.0).collect();
2594 if !self.config.price_axis.allow_label_overlap {
2595 ys = rendering::axes::spread_axis_ys(&painter, &ys);
2596 }
2597 for ((_, price, color), y) in prices.into_iter().zip(ys) {
2598 let label = price_map.label(
2599 price_map.transform().to_scale(price),
2600 rendering::axes::tag_decimals(&price_map, price_map.price_to_y(price)),
2601 price_formatter.as_deref(),
2602 );
2603 tags.push(rendering::axes::render_axis_value(
2604 &painter,
2605 price_rect,
2606 axis_edge,
2607 y,
2608 label,
2609 (rendering::axes::tag_text_color(color), color),
2610 &tags,
2611 self.config.price_axis.allow_label_overlap,
2612 ));
2613 }
2614 }
2615 }
2616
2617 #[cfg(feature = "drawings")]
2621 let eraser_mode = self.drawings.eraser_mode;
2622 #[cfg(feature = "drawings")]
2623 let drawing_tool_active = self.drawings.active_tool.is_some();
2624 let menu_open = self.menu_open || response.secondary_clicked();
2625 self.menu_open = menu_open;
2626 let mut crosshair_tags = [None; 2];
2627 #[cfg(feature = "drawings")]
2628 {
2629 self.cursor_modes.update(ui.input(|input| input.stable_dt));
2630 let trail = Some(self.chart_options.crosshair.style)
2631 .filter(|style| {
2632 matches!(
2633 style,
2634 crate::config::CrosshairStyle::Demonstration
2635 | crate::config::CrosshairStyle::Magic
2636 )
2637 })
2638 .filter(|_| !eraser_mode);
2639 self.cursor_modes.follow(
2640 trail,
2641 response.hover_pos().filter(|pos| price_rect.contains(*pos)),
2642 ui.input(|input| input.pointer.primary_down()),
2643 );
2644 if trail.is_some() {
2645 self.cursor_modes.render(ui, price_rect);
2646 }
2647 }
2648 let mut table_rect = price_rect;
2649 for (id, study) in self
2650 .study_ids
2651 .iter()
2652 .zip(self.rendered_studies().indicators())
2653 .filter(|(_, study)| study.is_visible() && study.is_overlay())
2654 {
2655 if let Some(table) = study.table() {
2656 table_rect.max.y =
2657 crate::studies::table::paint(ui, table_rect, table, ui.scope_id().with(id))
2658 .top()
2659 - 8.0;
2660 }
2661 }
2662 let hover_rect = shared_plot.map_or_else(
2663 || Rect::from_min_max(price_rect.min, Pos2::new(price_rect.right(), time_axis_top)),
2664 |plot| plot.rect,
2665 );
2666 let hover_pos = self
2667 .axis_hover
2668 .filter(|(_, tag)| {
2669 ui.input(|input| {
2670 input.pointer.hover_pos().is_some_and(|pos| {
2671 cfg!(feature = "icons")
2672 && show_price_button
2673 && rendering::axes::crosshair_price_button_rect(*tag).contains(pos)
2674 })
2675 })
2676 })
2677 .map(|(pos, _)| pos)
2678 .or_else(|| {
2679 ui.input(|input| input.pointer.hover_pos())
2680 .filter(|pos| hover_rect.contains(*pos) && ui.rect_contains_pointer(hover_rect))
2681 })
2682 .and_then(|pos| self.crosshair_position(pos, &coords));
2683 let mut study_action = None;
2684 let mut series_action = None;
2685 let mut legend_bottom = price_rect.top() + 8.0;
2686 {
2687 let visible_data = self.state.visible_data();
2688 let selected = hover_pos
2689 .and_then(|pos| coords.local_idx_at_x(pos.x, visible_data.len()))
2690 .map(|index| start_idx + index)
2691 .unwrap_or_else(|| {
2692 let shown = self.state.shown();
2694 shown
2695 .iter()
2696 .rposition(|bar| !bar.is_whitespace())
2697 .unwrap_or(shown.len().saturating_sub(1))
2698 });
2699 if let Some(bar) = self.state.shown().get(selected) {
2700 let timeframe = self.timeframe.map(|tf| tf.as_str()).unwrap_or_default();
2701 #[cfg(feature = "ui")]
2702 let order_quantity = self.order_quantity.take();
2703 let mut legend = super::Legend::new(&self.symbol, &timeframe, bar);
2704 let statuses: Vec<_> = self
2705 .legend_statuses
2706 .iter()
2707 .map(|(text, color, background)| super::LegendStatus {
2708 text,
2709 color: *color,
2710 background: *background,
2711 })
2712 .collect();
2713 legend.formatters = self.value_formatters();
2714 legend.thousands_sep = price_map.thousands_sep();
2715 legend.instrument = self.instrument();
2716 legend.badge = self.legend_badge.clone();
2717 legend.show_logo = self.config.show_legend_badge;
2718 legend.statuses = &statuses;
2719 legend.quote = self
2720 .quote
2721 .as_ref()
2722 .filter(|_| self.main_series_visible && self.config.show_quote_boxes);
2723 legend.show_title = self.main_series_present && self.config.show_symbol_labels;
2724 legend.show_market_status = self.config.show_market_status;
2725 if legend.show_title && legend.show_market_status {
2726 let now = self.chart_time().unwrap_or_else(chrono::Utc::now);
2728 legend.session = rendering::exchange_session(
2729 self.instrument(),
2730 self.config.session_config.as_ref(),
2731 &self.display_timezone(),
2732 crate::model::Timeframe::Min1,
2733 )
2734 .market_session(now);
2735 if legend.session.is_some() && self.replay_time().is_none() {
2736 let seconds = now.timestamp().rem_euclid(60) as u64;
2738 ui.ctx()
2739 .request_repaint_after(std::time::Duration::from_secs(60 - seconds));
2740 }
2741 }
2742 legend.series_visible = self.main_series_visible;
2743 legend.title_as_description = self.config.title_as_description;
2744 let values = self.main_series_visible && !bar.is_whitespace();
2746 legend.show_ohlc = values && self.config.show_ohlc_info;
2747 legend.show_change = values && self.config.show_ohlc_change;
2748 legend.show_change_percent = values && self.config.show_ohlc_change_percent;
2749 legend.show_indicator_titles = self.config.show_indicator_labels;
2750 legend.show_indicator_inputs = self.config.show_indicator_inputs;
2751 legend.show_indicator_values = self.config.show_indicator_last_val;
2752 legend.day_change =
2753 session_closes.filter(|_| values && self.config.show_last_day_change);
2754 legend.study_backdrop = self.config.indicator_backdrop_opacity;
2755 let inputs: Vec<_> = self.study_inputs.iter().map(String::as_str).collect();
2756 legend.study_inputs = &inputs;
2757 legend.previous_close =
2758 crate::model::last_with_value(&self.state.shown()[..selected])
2759 .map(|bar| bar.close);
2760 legend.bar_index = selected;
2761 legend.studies = self.rendered_studies().indicators();
2762 legend.show_volume =
2763 values && self.chart_type.supports_volume() && self.config.show_legend_volume;
2764 #[cfg(feature = "ui")]
2765 {
2766 legend.quantity = order_quantity;
2767 }
2768 let (quote_action, study_settings, bottom, quantity, main_action) = ui
2769 .push_id(shown.id, |ui| {
2770 legend.show_with_colors(
2771 ui,
2772 Rect::from_min_max(
2773 price_rect.min,
2774 egui::pos2(
2775 (price_rect.right() - 48.0).min(rect.right() - 100.0),
2776 price_rect.bottom(),
2777 ),
2778 ),
2779 &colors,
2780 )
2781 })
2782 .inner;
2783 if let Some(action) = quote_action {
2784 self.quote_action = Some(action);
2785 }
2786 #[cfg(feature = "ui")]
2787 if quantity.is_some() {
2788 self.quantity_clicked = quantity;
2789 }
2790 #[cfg(not(feature = "ui"))]
2791 let _ = quantity;
2792 study_action = study_settings;
2793 series_action = main_action;
2794 legend_bottom = bottom;
2795 }
2796 }
2797 let legend_bottom = self.paint_secondary_legend(
2798 ui,
2799 &painter,
2800 price_rect,
2801 legend_bottom + 4.0,
2802 self.main_pane_series(),
2803 );
2804 #[cfg(feature = "studies")]
2805 if let Some((step, tick, profiles)) = &execution_profiles {
2806 for profile in profiles {
2807 let levels = &profile.volume.levels;
2808 let Some(((&low, _), (&high, _))) =
2809 levels.first_key_value().zip(levels.last_key_value())
2810 else {
2811 continue;
2812 };
2813 if [
2814 low as f64 * step - tick * 0.5,
2815 (high as f64 + 1.0) * step - tick * 0.5,
2816 ]
2817 .into_iter()
2818 .all(|p| price_map.range().contains(transform.to_scale(p)))
2819 {
2820 continue;
2821 }
2822 let mut labels = Vec::new();
2823 if let Some(poc) = profile.volume.poc() {
2824 labels.push(("POC", poc as f64 * step));
2825 }
2826 if let Some((low, high)) = self
2827 .trade_profile_settings()
2828 .value_area
2829 .and_then(|s| profile.volume.value_area(s))
2830 {
2831 labels.extend([("VAH", high as f64 * step), ("VAL", low as f64 * step)]);
2832 }
2833 let bars = &self.state.data().bars;
2834 let first = bars.partition_point(|b| b.time < profile.range.start);
2835 let end = bars.partition_point(|b| b.time < profile.range.end);
2836 self.paint_profile_context(
2837 ui,
2838 &coords,
2839 (coords.idx_to_x(first) - coords.bar_spacing * 0.45)
2840 ..(coords.idx_to_x(end.saturating_sub(1)) + coords.bar_spacing * 0.5),
2841 profile.range.start,
2842 &labels,
2843 legend_bottom + 6.0,
2844 );
2845 }
2846 }
2847 if self.main_series_visible
2848 && footprint.is_none()
2849 && self.chart_type == ChartType::TimePriceOpportunity
2850 && self.state.price_scale().is_manual()
2851 {
2852 rendering::candles::tpo::visit_profiles(
2853 &self.state.data().bars,
2854 start_idx..end_idx,
2855 self.state.revision(),
2856 (
2857 &self.chart_type_params.tpo_config(render_ctx.tick_size),
2858 render_ctx.tick_size,
2859 ),
2860 &bar_clock,
2861 (&price_map, price_rect, |i| coords.idx_to_x(i)),
2862 |profile, config, anchor, column, _| {
2863 let cropped = [
2864 profile.row_span(profile.profile_low).0,
2865 profile.row_span(profile.profile_high).1,
2866 ]
2867 .into_iter()
2868 .any(|p| !price_map.range().contains(transform.to_scale(p)));
2869 if !cropped {
2870 return;
2871 }
2872 let mut labels = Vec::new();
2873 if config.show_poc {
2874 labels.push(("POC", profile.poc_price));
2875 }
2876 if config.show_value_area {
2877 labels.extend([
2878 ("VAH", profile.value_area_high),
2879 ("VAL", profile.value_area_low),
2880 ]);
2881 }
2882 let widest = profile.price_tpo_count.values().max().copied().unwrap_or(0);
2883 self.paint_profile_context(
2884 ui,
2885 &coords,
2886 anchor..anchor + column * widest as f32,
2887 profile.session_date,
2888 &labels,
2889 legend_bottom + 6.0,
2890 );
2891 },
2892 );
2893 }
2894 if let Some(lines) = target_lines {
2895 self.paint_targets(ui, &coords, legend_bottom + 4.0, lines);
2896 }
2897
2898 if self.chart_options.time_scale.ignore_whitespace_indices
2899 || self.chart_options.time_scale.ignore_hidden_indices
2900 {
2901 self.last_crosshair = None;
2902 }
2903 if let Some(hover_pos) = hover_pos {
2904 self.last_hover_bar_idx = coords
2906 .local_idx_at_x(hover_pos.x, visible_data.len())
2907 .map(|local| (start_idx + local) as f64);
2908
2909 if !menu_open {
2910 let mut tooltip_options = self.chart_options.tooltip.clone();
2911 tooltip_options.enabled &= self.main_series_visible;
2912 tooltip_options.show_volume &= self.chart_type.supports_volume();
2913 let held = ui.input(|input| {
2914 input.pointer.primary_down()
2915 && (!input.any_touches()
2916 || input
2917 .pointer
2918 .press_start_time()
2919 .is_some_and(|start| input.time - start >= 0.5))
2920 });
2921 let held_values = tooltip_options.enabled
2922 && tooltip_options.on_long_press
2923 && held
2924 && !eraser_mode
2925 && !drawing_tool_active;
2926 let mut crosshair_options = self.chart_options.crosshair;
2927 if (held_values || eraser_mode)
2929 && crosshair_options.style != crate::config::CrosshairStyle::Hidden
2930 {
2931 crosshair_options.style = crate::config::CrosshairStyle::Arrow;
2932 }
2933 let index = coords
2934 .local_idx_at_x(hover_pos.x, visible_data.len())
2935 .map(|local| start_idx + local);
2936 let snapped = if price_rect.contains(hover_pos) {
2937 index.map_or(hover_pos, |index| {
2938 self.snap_crosshair(hover_pos, &price_map, index)
2939 })
2940 } else {
2941 hover_pos
2942 };
2943 self.last_crosshair = index.map(|index| {
2944 let main = price_rect.contains(hover_pos) && !volume_rect.contains(hover_pos);
2945 (
2946 self.state.shown()[index].time,
2947 main.then(|| price_map.y_to_price(snapped.y)),
2948 )
2949 });
2950 crosshair_options.mode = crate::config::CrosshairMode::Normal;
2951 let mut main_options = crosshair_options;
2952 if volume_rect.contains(hover_pos) {
2953 main_options.horz_line_visible = false;
2954 main_options.horz_label_visible = false;
2955 if main_options.style == crate::config::CrosshairStyle::Dot {
2956 main_options.style = crate::config::CrosshairStyle::Arrow;
2957 }
2958 }
2959 crosshair_tags = rendering::render_crosshair_with_options(
2960 &price_ctx,
2961 snapped,
2962 visible_data,
2963 &price_map,
2964 &coords,
2965 &main_options,
2966 time_axis_top,
2967 axis_edge,
2968 last_price_tag,
2969 scale,
2970 bar_seconds,
2971 price_formatter.as_deref(),
2972 );
2973 if let Some(index) = index
2974 .filter(|_| crosshair_options.style != crate::config::CrosshairStyle::Hidden)
2975 .filter(|_| self.main_series_visible)
2976 {
2977 let bar = &self.state.shown()[index];
2978 let value = self
2979 .config
2980 .price_source
2981 .compute(bar.open, bar.high, bar.low, bar.close);
2982 if let Some(color) = rendering::candles::line_color_at(
2983 self.chart_type,
2984 &self.chart_type_params.styles,
2985 self.chart_type_params.baseline,
2986 &price_map,
2987 price_rect,
2988 value,
2989 bar,
2990 ) {
2991 rendering::candles::render_crosshair_marker(
2992 &painter.with_clip_rect(price_rect),
2993 Pos2::new(coords.idx_to_x(index), price_map.price_to_y(value)),
2994 self.chart_type_params.styles.crosshair_marker_radius,
2995 color,
2996 self.config.background_color,
2997 );
2998 }
2999 }
3000
3001 if volume_rect.contains(hover_pos)
3002 && let Some(map) = volume_map
3003 {
3004 let mut pos = hover_pos;
3005 if let Some(index) = index {
3006 pos.x = coords.idx_to_x(index);
3007 if self.chart_options.crosshair.mode.snaps() {
3008 pos.y = map.price_to_y(self.state.shown()[index].volume);
3009 }
3010 }
3011 let mut options = crosshair_options.resolved(ui.ctx());
3012 options.vert_line_visible = false;
3013 let context = RenderContext::new(&painter, volume_rect);
3014 rendering::render_crosshair_lines(
3015 &context,
3016 pos,
3017 &options,
3018 volume_rect.bottom(),
3019 );
3020 crosshair_tags[0] = rendering::render_crosshair_price_tag(
3021 &context,
3022 pos,
3023 &map,
3024 &options,
3025 axis_edge,
3026 None,
3027 Some(
3028 crate::scales::StudyFormat::Volume
3029 .format(map.y_to_price(pos.y), map.price_precision()),
3030 ),
3031 );
3032 }
3033
3034 if tooltip_options.on_long_press
3035 && ui.input(|input| input.any_touches() && input.pointer.primary_down())
3036 {
3037 ui.ctx().request_repaint();
3038 }
3039 if tooltip_options.enabled
3040 && (!tooltip_options.on_long_press || held)
3041 && !eraser_mode
3042 && !drawing_tool_active
3043 && let Some(local_idx) = coords.local_idx_at_x(hover_pos.x, visible_data.len())
3044 && !visible_data[local_idx].is_whitespace()
3045 {
3046 let candle = &visible_data[local_idx];
3047 if tooltip_options.on_long_press {
3048 let x = coords.idx_to_x(start_idx + local_idx);
3049 rendering::draw_dashed_line(
3050 &clipped_painter,
3051 Pos2::new(x, price_rect.top()),
3052 Pos2::new(x, price_rect.bottom()),
3053 egui::Stroke::new(
3054 1.0,
3055 self.chart_options
3056 .crosshair
3057 .resolved(ui.ctx())
3058 .vert_line_color,
3059 ),
3060 5.0,
3061 5.0,
3062 );
3063 for (index, bar) in visible_data
3064 .iter()
3065 .enumerate()
3066 .step_by(25)
3067 .filter(|(_, bar)| !bar.is_whitespace())
3068 {
3069 let pos = Pos2::new(
3070 coords.idx_to_x(start_idx + index),
3071 price_map.price_to_y(bar.close),
3072 );
3073 clipped_painter.circle(
3074 pos,
3075 3.5,
3076 ui.active_theme().chart.bg,
3077 egui::Stroke::new(1.5, ui.active_theme().ui.accent),
3078 );
3079 }
3080 }
3081 let heading = (self.instrument().is_none() && !self.symbol.is_empty())
3083 .then(|| crate::model::Instrument::new(self.symbol.as_str()));
3084 rendering::render_tooltip_with_options(
3085 &price_ctx,
3086 hover_pos,
3087 candle,
3088 crate::model::last_with_value(&self.state.shown()[..start_idx + local_idx])
3089 .map(|bar| bar.close),
3090 &tooltip_options,
3091 &colors,
3092 &price_map,
3093 &coords,
3094 visible_data,
3095 scale,
3096 bar_seconds,
3097 &self
3098 .study_rows_at(start_idx + local_idx)
3099 .into_iter()
3100 .filter(|(id, _)| {
3101 !tooltip_options.on_long_press
3102 || tooltip_options.show_volume_details
3103 || self
3104 .study_ids
3105 .iter()
3106 .zip(self.rendered_studies().indicators())
3107 .any(|(study_id, study)| {
3108 study_id == id && !study.is_volume()
3109 })
3110 })
3111 .map(|(_, row)| row)
3112 .collect::<Vec<_>>(),
3113 self.instrument().or(heading.as_ref()),
3114 self.value_formatters(),
3115 );
3116 }
3117 }
3118 } else {
3119 self.last_hover_bar_idx = None;
3121
3122 {
3124 if !menu_open
3125 && let Some(bar_idx) = self.synced_crosshair_bar_idx.and_then(|index| {
3126 let options = &self.chart_options.time_scale;
3127 if !options.ignore_whitespace_indices && !options.ignore_hidden_indices {
3128 return Some(index);
3129 }
3130 self.crosshair_position(
3131 Pos2::new(
3132 coords.idx_to_x(index.floor() as usize)
3133 + index.fract() as f32 * coords.bar_spacing,
3134 price_rect.center().y,
3135 ),
3136 &coords,
3137 )
3138 .map(|pos| coords.x_to_idx_f64(pos.x))
3139 })
3140 {
3141 let x = coords.idx_to_x(bar_idx.round() as usize);
3143 if coords.is_x_visible(x) {
3144 let mut options = self.chart_options.crosshair;
3147 let y = match self.synced_crosshair_price {
3148 Some(price) => {
3149 options.mode = crate::config::CrosshairMode::Normal;
3150 price_map.price_to_y(price)
3151 }
3152 None => price_rect.center().y,
3153 };
3154 if let Some(bar) = self.state.shown().get(bar_idx.round() as usize) {
3155 self.last_crosshair = Some((bar.time, self.synced_crosshair_price));
3156 }
3157 crosshair_tags = rendering::render_crosshair_with_options(
3158 &price_ctx,
3159 Pos2::new(x, y),
3160 visible_data,
3161 &price_map,
3162 &coords,
3163 &options,
3164 time_axis_top,
3165 axis_edge,
3166 last_price_tag,
3167 scale,
3168 bar_seconds,
3169 price_formatter.as_deref(),
3170 );
3171 }
3172 }
3173 }
3174 }
3175
3176 self.axis_hover = hover_pos.zip(crosshair_tags[0]);
3177 #[cfg(feature = "icons")]
3178 if show_price_axis {
3179 if let Some((pos, tag)) = self.axis_hover.filter(|(pos, _)| {
3180 show_price_button && price_rect.contains(*pos) && !volume_rect.contains(*pos)
3181 }) {
3182 let price = if self.chart_options.crosshair.mode.snaps()
3183 && let Some(index) = coords.local_idx_at_x(pos.x, visible_data.len())
3184 {
3185 price_map.y_to_price(self.snap_crosshair(pos, &price_map, start_idx + index).y)
3186 } else {
3187 price_map.y_to_price(pos.y)
3188 };
3189 let options = self.chart_options.crosshair.resolved(ui.ctx());
3190 if let Some(price) = rendering::axes::crosshair_price_action_with_colors(
3191 ui,
3192 tag,
3193 price,
3194 (
3195 options.label_text_color,
3196 options.horz_label_background_color,
3197 ),
3198 ) {
3199 self.right_click = Some(RightClickTarget::Background {
3200 pos: Pos2::new(tag.left(), tag.bottom().min(price_rect.bottom())),
3201 price,
3202 });
3203 }
3204 }
3205 if show_axis_settings
3206 && rendering::axes::axis_settings_button(ui, axis_corner).clicked()
3207 {
3208 self.scale_settings_requested = true;
3209 }
3210 }
3211
3212 let current_price = crate::model::last_with_value(&self.state.data().bars)
3213 .filter(|_| self.main_series_visible && self.config.show_symbol_last_val)
3214 .map(|bar| {
3215 price_map.label(
3216 price_map.transform().to_scale(bar.close),
3217 rendering::axes::tag_decimals(&price_map, price_map.price_to_y(bar.close)),
3218 price_formatter.as_deref(),
3219 )
3220 });
3221 self.measured_axis_width = rendering::axes::price_axis_width(
3222 &painter,
3223 &price_map,
3224 ¤t_price.as_deref().into_iter().collect::<Vec<_>>(),
3225 price_formatter.as_deref(),
3226 );
3227 if cfg!(feature = "icons")
3228 && self.config.show_left_axis
3229 && !self.config.show_right_axis
3230 && self.config.show_plus_button
3231 {
3232 self.measured_axis_width += 26.0;
3234 }
3235 self.measured_axis_width = self.measured_axis_width.max(independent_axis_width);
3236 for tag in tags.iter().skip(usize::from(has_currency)).flatten() {
3237 self.measured_axis_width = self.measured_axis_width.max(tag.width() + 5.0 + 6.0);
3238 }
3239
3240 if show_price_axis && has_currency {
3241 let currency = self
3242 .instrument()
3243 .and_then(|instrument| instrument.currency.as_deref())
3244 .filter(|_| self.config.show_currency)
3245 .unwrap_or("");
3246 let unit = self
3247 .instrument()
3248 .and_then(|instrument| instrument.unit.as_deref())
3249 .filter(|_| self.config.show_unit);
3250
3251 let header = match (currency.is_empty(), unit.filter(|unit| !unit.is_empty())) {
3252 (false, Some(unit)) => format!("{currency} ยท {unit}"),
3253 (true, Some(unit)) => unit.to_owned(),
3254 _ => currency.to_owned(),
3255 };
3256 let header_width = painter
3257 .layout_no_wrap(header, rendering::axes::axis_font(ui.ctx()), colors.text)
3258 .size()
3259 .x
3260 + if currency.is_empty() { 16.0 } else { 26.0 };
3261 let axis = Rect::from_min_max(
3262 Pos2::new(
3263 if self.config.show_right_axis {
3264 price_rect.right()
3265 } else {
3266 rect.left()
3267 },
3268 price_rect.top(),
3269 ),
3270 Pos2::new(axis_edge, price_rect.bottom()),
3271 );
3272 let (button, choice) =
3273 rendering::render_axis_currency(ui, axis, currency, unit, &self.currency_choices);
3274 self.measured_axis_width = self.measured_axis_width.max(header_width);
3275 if choice.is_some() {
3276 self.currency_choice = choice;
3277 }
3278 tags.push(Some(button.rect));
3279 }
3280
3281 tags.extend(crosshair_tags);
3283 let time_tags = if time_tags.is_empty() {
3284 &tags
3285 } else {
3286 time_tags.extend_from_slice(&tags);
3287 &time_tags
3288 };
3289 if self.config.show_right_axis {
3290 rendering::render_price_labels(
3291 &price_ctx,
3292 &price_map,
3293 &colors,
3294 &self.config.price_axis,
3295 &tags,
3296 price_labels,
3297 price_formatter.as_deref(),
3298 );
3299 } else if self.config.show_left_axis {
3300 rendering::render_price_labels_at_pos(
3301 &price_ctx,
3302 &price_map,
3303 &colors,
3304 &self.config.price_axis,
3305 crate::scales::PriceScaleId::Left,
3306 &tags,
3307 price_labels,
3308 price_formatter.as_deref(),
3309 );
3310 }
3311 if self.config.show_time_labels {
3312 let axis_context_rect =
3313 Rect::from_min_max(chart_rect.min, Pos2::new(chart_rect.right(), time_axis_top));
3314 let chart_ctx = RenderContext::new(&painter, axis_context_rect);
3315 rendering::render_time_labels(
3316 &chart_ctx,
3317 time_marks,
3318 &time_points,
3319 &axis_coords,
3320 &colors,
3321 time_tags,
3322 &self.chart_options.time_scale,
3323 time_labels,
3324 );
3325 }
3326
3327 crate::widget::pan_zoom::render_box_zoom(&painter, &self.box_zoom);
3329
3330 if let Some((nav, buttons)) = navigation {
3331 self.paint_navigation(ui, nav, buttons);
3332 }
3333
3334 self.show_realtime_button(ui, realtime_rect, shown.id);
3335
3336 crate::styles::focus::draw_focus_ring(ui, &shown, 0.0);
3338
3339 self.show_signal_menu(ui, price_rect);
3340 if let Some(action) = study_action {
3341 self.apply_study_action(action);
3342 }
3343 if let Some(action) = series_action {
3344 self.apply_main_series_action(action);
3345 }
3346 self.main_pane_actions(ui, rect, chart_rect);
3347 self.record_host_click(ui, &response);
3348 shown
3349 }
3350}
3351
3352#[cfg(test)]
3353mod tests;