Skip to main content

egui_charts/widget/
mod.rs

1//! The primary Chart widget for embedding interactive financial charts in egui.
2//!
3//! This module provides [`Chart`], the main entry point for rendering OHLCV
4//! (Open-High-Low-Close-Volume) financial data as an interactive egui widget.
5//! It supports multiple chart types (candlestick, line, area, bar, Renko, Kagi),
6//! real-time data streaming, drawing tools, technical indicators, and
7//! multi-chart synchronization.
8//!
9//! # Quick Start
10//!
11//! ```rust,ignore
12//! use egui_charts::widget::Chart;
13//! use egui_charts::model::BarData;
14//!
15//! // Create chart with OHLCV data
16//! let mut chart = Chart::new(bar_data);
17//!
18//! // Show the chart in your egui update loop
19//! egui::CentralPanel::default().show(ctx, |ui| {
20//!     chart.show(ui);
21//! });
22//! ```
23//!
24//! # Builder Pattern
25//!
26//! Configure the chart before showing it:
27//!
28//! ```rust,ignore
29//! let mut chart = Chart::new(data)
30//!     .visible_bars(100)          // Show 100 bars at a time
31//!     .config(my_chart_config)    // Custom visual config
32//!     .with_chart_options(opts);  // Scroll/zoom behavior
33//! ```
34//!
35//! # Real-Time Updates
36//!
37//! For live data feeds, reuse the chart instance across frames:
38//!
39//! ```rust,ignore
40//! // In your app struct:
41//! struct MyApp {
42//!     chart: Chart,
43//! }
44//!
45//! // On new data:
46//! let auto_scrolled = self.chart.update_data(new_bar_data);
47//! self.chart.show(ui);
48//! ```
49//!
50//! # Drawing Tools and Indicators
51//!
52//! ```rust,ignore
53//! // With drawing tools and indicators:
54//! chart.show_with_indicators(
55//!     ui,
56//!     Some(&mut drawing_manager),
57//!     Some(&indicator_registry),
58//! );
59//! ```
60//!
61//! # Multi-Chart Sync
62//!
63//! Synchronize crosshairs and time scales between multiple charts:
64//!
65//! ```rust,ignore
66//! // Emit crosshair position from chart A
67//! let hover_idx = chart_a.get_hover_bar_idx();
68//!
69//! // Apply to chart B
70//! chart_b.set_synced_crosshair_bar_idx(hover_idx);
71//!
72//! // Sync time scales
73//! let (spacing, offset) = chart_a.get_time_scale_state();
74//! chart_b.apply_synced_time_scale(spacing, offset);
75//! ```
76//!
77//! # Sub-modules
78//!
79//! - [`builder`] -- Constructor and configuration methods for [`Chart`]
80//! - [`indicator_pane`] -- Separate indicator panels (RSI, MACD, Stochastic)
81
82use crate::chart::cursor_modes::CursorModeState;
83use crate::chart::renderers::{self, ChartMapping, PriceScale, RenderContext, StyleColors};
84use crate::chart::series::SeriesSettings;
85use crate::config::{BackgroundStyle, ChartConfig, ChartOptions, WatermarkPos};
86use crate::drawings::DrawingManager;
87use crate::model::ChartState;
88use crate::model::ChartType;
89use crate::scales::TimeFormatterBuilder;
90use crate::studies::IndicatorRegistry;
91use crate::validation::DataValidator;
92pub mod indicator_pane;
93use crate::styles::{sizing, typography};
94use crate::tokens::DESIGN_TOKENS;
95use egui::{Pos2, Rect, Response, Sense, Ui, Vec2};
96pub use indicator_pane::{IndicatorCoordParams, IndicatorPane, IndicatorPaneConfig};
97
98// Re-export from logic layer
99use crate::chart::{helpers, rendering, state};
100
101pub mod builder;
102
103pub use helpers::{apply_price_zoom, y_to_price};
104pub use state::{BoxZoomMode, BoxZoomState, ElasticBounceState, KineticScrollState};
105
106/// Interactive financial chart widget for egui.
107///
108/// `Chart` is the core rendering and interaction engine for displaying OHLCV
109/// financial data. It handles all aspects of chart visualization including
110/// candlestick/bar/line rendering, pan/zoom interactions, crosshair display,
111/// drawing tool integration, and indicator overlays.
112///
113/// # Creating a Chart
114///
115/// Use [`Chart::new`] or [`Chart::with_config`] to construct, then call
116/// [`Chart::show`] each frame to render:
117///
118/// ```rust,ignore
119/// use egui_charts::widget::Chart;
120/// use egui_charts::model::BarData;
121///
122/// let mut chart = Chart::new(bar_data);
123///
124/// egui::CentralPanel::default().show(ctx, |ui| {
125///     let response = chart.show(ui);
126///     // response can be used for additional interaction handling
127/// });
128/// ```
129///
130/// # Supported Chart Types
131///
132/// - **Candles** -- Standard Japanese candlestick chart
133/// - **Bars** -- OHLC bar chart
134/// - **Line** -- Close-price line chart
135/// - **Area** -- Filled area under the close-price line
136/// - **Renko** -- Fixed-size brick chart (set brick size with [`Chart::set_renko_brick_size`])
137/// - **Kagi** -- Reversal-based chart (set reversal amount with [`Chart::set_kagi_reversal_amount`])
138///
139/// # Interaction Features
140///
141/// - **Pan**: Click and drag to scroll through history (with kinetic scrolling)
142/// - **Zoom**: Mouse wheel to zoom in/out, pinch-to-zoom on trackpads
143/// - **Box zoom**: Drag-select a region to zoom into (when zoom mode is active)
144/// - **Price scale drag**: Drag the price axis to scale vertically
145/// - **Crosshair**: Hover to see price/time at cursor position
146/// - **Keyboard shortcuts**: Arrow keys, Home/End, +/- for navigation
147/// - **Double-click**: Reset zoom on price or time axis
148///
149/// # Architecture
150///
151/// `Chart` owns a [`ChartState`] (data + coordinate systems) and a
152/// [`ChartConfig`] (visual styling). Rendering is delegated to specialized
153/// modules in `crate::chart::rendering`, while interaction logic lives in
154/// `crate::chart::state`.
155pub struct Chart {
156    /// Backend state holding OHLCV data and coordinate system (time scale, price range).
157    pub state: ChartState,
158    /// Visual configuration controlling colors, padding, grid visibility, and more.
159    pub config: ChartConfig,
160    /// Chart behavior options (bar spacing, scroll/zoom constraints, time scale settings).
161    pub chart_options: ChartOptions,
162    /// Starting index of visible range (for backward compatibility)
163    pub(crate) start_idx: usize,
164    /// Desired number of visible bars (from app state)
165    pub(crate) desired_visible_bars: Option<usize>,
166    /// Cache of last computed visible bars for external syncing
167    pub(crate) last_visible_bars: usize,
168    /// Whether to apply `desired_visible_bars` on next frame only
169    pub(crate) apply_visible_bars_once: bool,
170    /// Kinetic scroll animation state (UI state only)
171    pub(crate) kinetic_scroll: KineticScrollState,
172    /// Last scroll position for drag tracking (UI state only)
173    pub(crate) scroll_start_pos: Option<Pos2>,
174    /// Initial right offset when starting scroll (for drag) (UI state only)
175    pub(crate) scroll_start_offset: Option<f32>,
176    /// Previous widget width for resize handling (UI state only)
177    pub(crate) prev_width: Option<f32>,
178    /// Drag state for price-axis scaling (UI state only)
179    pub(crate) price_scale_drag_start: Option<Pos2>,
180    /// Apply external start-index once to time scale (UI state only)
181    pub(crate) pending_start_idx: Option<usize>,
182    /// Chart type (candlestick, line, area, bar, Renko, Kagi)
183    pub(crate) chart_type: ChartType,
184    /// Renko brick size (for Renko charts)
185    pub(crate) renko_brick_size: f64,
186    /// Kagi reversal amount (for Kagi charts)
187    pub(crate) kagi_reversal_amount: f64,
188    /// Whether tracking mode is currently active
189    pub(crate) tracking_mode_active: bool,
190    /// Mouse entered chart area (for tracking mode exit detection)
191    pub(crate) mouse_in_chart: bool,
192    /// Data validator for detecting data mismatches
193    pub(crate) validator: Option<DataValidator>,
194    /// Right-click box zoom state
195    pub(crate) box_zoom: BoxZoomState,
196    /// Whether zoom mode is currently active (controlled by zoom toolbar button)
197    pub(crate) zoom_mode_active: bool,
198    /// Whether zoom was just applied in the last frame (for auto-deactivation)
199    pub(crate) zoom_just_applied: bool,
200    /// Current symbol being displayed (for legend)
201    pub(crate) symbol: String,
202    /// Current timeframe (for legend)
203    pub(crate) timeframe: String,
204    /// Cursor mode state (Demonstration, Magic, Eraser effects)
205    #[doc(hidden)]
206    pub cursor_modes: CursorModeState,
207    /// Last rendered price range (includes zoom adjustments) for external use
208    pub(crate) last_rendered_price_range: (f64, f64),
209    /// Last rendered price rect (actual rect used for candle rendering).
210    /// Use [`get_rendered_price_rect`](Chart::get_rendered_price_rect) instead.
211    #[doc(hidden)]
212    pub last_rendered_price_rect: Rect,
213    /// Last rendered volume rect (actual rect used for volume rendering)
214    pub(crate) last_rendered_volume_rect: Rect,
215    /// Last rendered indicator pane info for hit testing
216    /// Each entry: (indicator_index, panel_rect, chart_rect, y_min, y_max, coords)
217    pub(crate) last_rendered_indicator_panes: Vec<RenderedIndicatorPane>,
218    // =========================================================================
219    // Multi-Chart Sync State
220    // =========================================================================
221    /// External crosshair position from synced chart (bar index)
222    pub(crate) synced_crosshair_bar_idx: Option<f64>,
223    /// Last computed hover bar index (for sync emission to other charts)
224    pub(crate) last_hover_bar_idx: Option<f64>,
225
226    // =========================================================================
227    // Marks (Widget API)
228    // =========================================================================
229    /// Bar marks (annotations on chart bars, e.g., trade signals)
230    pub marks: Vec<crate::model::Marker>,
231    /// Timescale marks (annotations on the time axis)
232    pub timescale_marks: Vec<crate::model::Marker>,
233}
234
235/// Information about a rendered indicator pane, used for hit testing and coordinate mapping.
236///
237/// After calling [`Chart::show_with_indicators`], each visible separate-pane indicator
238/// (RSI, MACD, etc.) produces a `RenderedIndicatorPane` entry stored in the chart.
239/// Platform code can use these to implement click-on-indicator-line selection,
240/// tooltip display, or other interactive features.
241///
242/// Retrieve with [`Chart::get_rendered_indicator_panes`].
243#[derive(Clone, Debug)]
244pub struct RenderedIndicatorPane {
245    /// Index of the indicator in the registry
246    pub indicator_idx: usize,
247    /// Full panel rect (including y-axis labels)
248    pub panel_rect: Rect,
249    /// Chart drawing area rect (excluding y-axis labels)
250    pub chart_rect: Rect,
251    /// Y-axis minimum value
252    pub y_min: f64,
253    /// Y-axis maximum value
254    pub y_max: f64,
255    /// Coordinate parameters for x-axis calculation
256    pub coords: IndicatorCoordParams,
257}
258
259/// Pre-computed layout rectangles for the chart's sub-regions.
260///
261/// Use [`Chart::calculate_layout_rects`] to obtain these rects for a given
262/// widget area. They are useful for external hit-testing (e.g., determining
263/// whether a click landed on the price area, volume area, or legend).
264#[derive(Clone, Copy, Debug)]
265pub struct ChartLayoutRects {
266    /// The overall widget rect (entire chart area including axes and padding).
267    pub widget_rect: Rect,
268    /// The main price/candle area where OHLC data is rendered.
269    pub price_rect: Rect,
270    /// The volume sub-area below the price chart (empty if volume is hidden).
271    pub volume_rect: Rect,
272    /// The legend/OHLC info area at the top of the chart.
273    pub legend_rect: Rect,
274}
275
276impl Default for ChartLayoutRects {
277    fn default() -> Self {
278        Self {
279            widget_rect: Rect::NOTHING,
280            price_rect: Rect::NOTHING,
281            volume_rect: Rect::NOTHING,
282            legend_rect: Rect::NOTHING,
283        }
284    }
285}
286
287impl Chart {
288    /// Calculate the layout sub-rects for a given widget rect.
289    ///
290    /// Given the overall widget area, this computes where the price chart,
291    /// volume bars, and legend/OHLC header will be drawn. Useful for
292    /// external hit-testing, overlay placement, or custom drawing on top
293    /// of specific chart regions.
294    ///
295    /// The layout respects current config flags like `show_ohlc_info`,
296    /// `show_time_labels`, and `show_volume`.
297    pub fn calculate_layout_rects(&self, widget_rect: Rect) -> ChartLayoutRects {
298        let bottom_padding = if self.config.show_time_labels {
299            30.0
300        } else {
301            20.0
302        };
303        let top_padding = if self.config.show_ohlc_info {
304            40.0
305        } else {
306            20.0
307        };
308        let right_padding = self.config.padding * 2.0;
309
310        // Legend rect is at the top of the widget
311        let legend_rect = if self.config.show_ohlc_info {
312            Rect::from_min_size(
313                widget_rect.min + Vec2::new(self.config.padding, 4.0),
314                Vec2::new(widget_rect.width() * 0.7, top_padding - 8.0),
315            )
316        } else {
317            Rect::NOTHING
318        };
319
320        let chart_rect = Rect::from_min_size(
321            widget_rect.min + Vec2::new(self.config.padding, top_padding),
322            Vec2::new(
323                widget_rect.width() - self.config.padding - right_padding,
324                widget_rect.height() - top_padding - bottom_padding,
325            ),
326        );
327
328        let (price_rect, volume_rect) = if self.config.show_volume {
329            let split_y =
330                chart_rect.min.y + chart_rect.height() * (1.0 - self.config.volume_height_fraction);
331            (
332                Rect::from_min_max(chart_rect.min, Pos2::new(chart_rect.max.x, split_y)),
333                Rect::from_min_max(Pos2::new(chart_rect.min.x, split_y), chart_rect.max),
334            )
335        } else {
336            (chart_rect, Rect::NOTHING)
337        };
338
339        ChartLayoutRects {
340            widget_rect,
341            price_rect,
342            volume_rect,
343            legend_rect,
344        }
345    }
346
347    /// Activates or deactivates box-zoom mode.
348    ///
349    /// When active, left-click drag draws a selection rectangle and zooms into
350    /// that region. The mode auto-deactivates after a successful zoom operation
351    /// (check with [`Chart::zoom_was_applied`]).
352    pub fn set_zoom_mode(&mut self, active: bool) {
353        self.zoom_mode_active = active;
354    }
355
356    /// Returns `true` if a box-zoom was completed in the most recent frame.
357    ///
358    /// Use this to auto-deactivate zoom mode in your toolbar after the user
359    /// completes a zoom selection.
360    pub fn zoom_was_applied(&self) -> bool {
361        self.zoom_just_applied
362    }
363
364    /// Sets the trading symbol displayed in the chart legend (e.g., "BTCUSD", "AAPL").
365    pub fn set_symbol(&mut self, symbol: &str) {
366        self.symbol = symbol.to_string();
367    }
368
369    /// Sets the timeframe label displayed in the chart legend (e.g., "1H", "1D", "1W").
370    pub fn set_timeframe_label(&mut self, timeframe: &str) {
371        self.timeframe = timeframe.to_string();
372    }
373
374    /// Sets the crosshair rendering style (Full, Dot, or Arrow).
375    ///
376    /// Use this to connect a toolbar cursor-type selector to the chart.
377    /// The style controls how the crosshair lines and labels are drawn
378    /// when the user hovers over the chart area.
379    pub fn set_crosshair_style(&mut self, style: crate::config::CrosshairStyle) {
380        self.chart_options.crosshair.style = style;
381    }
382
383    /// Apply series settings to chart colors and price source.
384    ///
385    /// Copies candlestick colors (bullish/bearish fill, border, wick) and the
386    /// price source field from the given [`SeriesSettings`] into the chart's
387    /// [`ChartConfig`]. Call this when the user changes series appearance in a
388    /// settings dialog.
389    pub fn apply_series_settings(&mut self, settings: &SeriesSettings) {
390        self.config.bullish_color = settings.bullish_color;
391        self.config.bearish_color = settings.bearish_color;
392        self.config.bullish_border_color = settings.bullish_border_color;
393        self.config.bearish_border_color = settings.bearish_border_color;
394        self.config.bullish_wick_color = settings.bullish_wick_color;
395        self.config.bearish_wick_color = settings.bearish_wick_color;
396        self.config.price_source = settings.price_source;
397    }
398
399    /// Draw the chart background (solid or gradient)
400    fn draw_background(&self, painter: &egui::Painter, rect: Rect) {
401        // Skip background when chart is inside a container that handles its own background
402        if self.config.skip_background {
403            return;
404        }
405
406        match self.config.background_style {
407            BackgroundStyle::Solid => {
408                painter.rect_filled(rect, 0.0, self.config.background_color);
409            }
410            BackgroundStyle::VerticalGradient {
411                top_color,
412                bottom_color,
413            } => {
414                // Draw vertical gradient using a mesh
415                let mesh = egui::Mesh {
416                    indices: vec![0, 1, 2, 2, 3, 0],
417                    vertices: vec![
418                        egui::epaint::Vertex {
419                            pos: rect.left_top(),
420                            uv: egui::epaint::WHITE_UV,
421                            color: top_color,
422                        },
423                        egui::epaint::Vertex {
424                            pos: rect.right_top(),
425                            uv: egui::epaint::WHITE_UV,
426                            color: top_color,
427                        },
428                        egui::epaint::Vertex {
429                            pos: rect.right_bottom(),
430                            uv: egui::epaint::WHITE_UV,
431                            color: bottom_color,
432                        },
433                        egui::epaint::Vertex {
434                            pos: rect.left_bottom(),
435                            uv: egui::epaint::WHITE_UV,
436                            color: bottom_color,
437                        },
438                    ],
439                    texture_id: egui::TextureId::default(),
440                };
441                painter.add(egui::Shape::mesh(mesh));
442            }
443            BackgroundStyle::HorizontalGradient {
444                left_color,
445                right_color,
446            } => {
447                // Draw horizontal gradient using a mesh
448                let mesh = egui::Mesh {
449                    indices: vec![0, 1, 2, 2, 3, 0],
450                    vertices: vec![
451                        egui::epaint::Vertex {
452                            pos: rect.left_top(),
453                            uv: egui::epaint::WHITE_UV,
454                            color: left_color,
455                        },
456                        egui::epaint::Vertex {
457                            pos: rect.right_top(),
458                            uv: egui::epaint::WHITE_UV,
459                            color: right_color,
460                        },
461                        egui::epaint::Vertex {
462                            pos: rect.right_bottom(),
463                            uv: egui::epaint::WHITE_UV,
464                            color: right_color,
465                        },
466                        egui::epaint::Vertex {
467                            pos: rect.left_bottom(),
468                            uv: egui::epaint::WHITE_UV,
469                            color: left_color,
470                        },
471                    ],
472                    texture_id: egui::TextureId::default(),
473                };
474                painter.add(egui::Shape::mesh(mesh));
475            }
476        }
477    }
478
479    /// Draw watermark overlay (large symbol name)
480    fn draw_watermark(&self, painter: &egui::Painter, rect: Rect) {
481        if !self.config.show_watermark {
482            return;
483        }
484
485        let text = self.config.watermark_text.as_deref().unwrap_or_else(|| {
486            if self.symbol.is_empty() {
487                "SYMBOL"
488            } else {
489                &self.symbol
490            }
491        });
492
493        let font_id = egui::FontId::proportional(self.config.watermark_font_size);
494
495        // Calculate position based on watermark_pos
496        let pos = match self.config.watermark_pos {
497            WatermarkPos::Center => rect.center(),
498            WatermarkPos::TopLeft => Pos2::new(
499                rect.min.x + 20.0,
500                rect.min.y + self.config.watermark_font_size,
501            ),
502            WatermarkPos::TopRight => Pos2::new(
503                rect.max.x - 20.0,
504                rect.min.y + self.config.watermark_font_size,
505            ),
506            WatermarkPos::BottomLeft => Pos2::new(rect.min.x + 20.0, rect.max.y - 20.0),
507            WatermarkPos::BottomRight => Pos2::new(rect.max.x - 20.0, rect.max.y - 20.0),
508        };
509
510        let anchor = match self.config.watermark_pos {
511            WatermarkPos::Center => egui::Align2::CENTER_CENTER,
512            WatermarkPos::TopLeft => egui::Align2::LEFT_TOP,
513            WatermarkPos::TopRight => egui::Align2::RIGHT_TOP,
514            WatermarkPos::BottomLeft => egui::Align2::LEFT_BOTTOM,
515            WatermarkPos::BottomRight => egui::Align2::RIGHT_BOTTOM,
516        };
517
518        painter.text(pos, anchor, text, font_id, self.config.watermark_color);
519    }
520
521    /// Renders the chart with mouse interactions and optional drawing tools.
522    ///
523    /// This is the mid-level rendering method. Use this when you have drawing
524    /// tools but no separate-pane indicators. For the simplest case, use
525    /// [`Chart::show`]. For full functionality, use [`Chart::show_with_indicators`].
526    pub fn show_with_drawings(
527        &mut self,
528        ui: &mut Ui,
529        drawing_manager: Option<&mut DrawingManager>,
530    ) -> Response {
531        self.show_internal(ui, drawing_manager, None)
532    }
533
534    /// Renders the chart with indicators and drawing tools.
535    ///
536    /// This is the most feature-complete rendering method. Overlay indicators
537    /// (moving averages, Bollinger Bands, etc.) are drawn on the main price chart.
538    /// Separate-pane indicators (RSI, MACD, Stochastic) are rendered in dedicated
539    /// panels below the main chart with aligned x-axes.
540    ///
541    /// After rendering, use [`Chart::get_rendered_indicator_panes`] to access
542    /// indicator pane layout information for hit testing.
543    ///
544    /// # Arguments
545    ///
546    /// * `ui` -- The egui UI to render into
547    /// * `drawing_manager` -- Optional drawing tool manager for trend lines, etc.
548    /// * `indicators` -- Optional indicator registry containing computed indicators
549    pub fn show_with_indicators(
550        &mut self,
551        ui: &mut Ui,
552        drawing_manager: Option<&mut DrawingManager>,
553        indicators: Option<&IndicatorRegistry>,
554    ) -> Response {
555        // Clear previous frame's indicator pane info
556        self.last_rendered_indicator_panes.clear();
557
558        // Calculate total height needed for indicator panes FIRST
559        // This allows us to reserve space before the main chart
560        let indicator_pane_height = if let Some(indicators) = indicators {
561            let mut total_height = 0.0f32;
562            let mut pane_count = 0;
563
564            for indicator in indicators.indicators() {
565                if indicator.is_overlay() || !indicator.is_visible() {
566                    continue;
567                }
568                pane_count += 1;
569                let height = match indicator.name() {
570                    "RSI" => IndicatorPaneConfig::rsi().height,
571                    "MACD" => IndicatorPaneConfig::macd().height,
572                    "Stochastic" => IndicatorPaneConfig::stochastic().height,
573                    _ => IndicatorPaneConfig::default().height,
574                };
575                total_height += height;
576            }
577
578            if pane_count > 0 {
579                // Add minimal gap between panes (seamless panes)
580                total_height + 1.0 * pane_count as f32
581            } else {
582                0.0
583            }
584        } else {
585            0.0
586        };
587
588        // Calculate available height and reserve space for indicators
589        let available = ui.available_size();
590        let main_chart_height = (available.y - indicator_pane_height).max(200.0);
591
592        // Allocate fixed height for main chart (prevents it from taking all space)
593        let response = ui
594            .allocate_ui_with_layout(
595                egui::vec2(available.x, main_chart_height),
596                egui::Layout::top_down(egui::Align::LEFT),
597                |ui| self.show_internal(ui, drawing_manager, indicators),
598            )
599            .inner;
600
601        // Render separate pane indicators below the main chart
602        if let Some(indicators) = indicators {
603            let (start_idx, end_idx) = self.state.visible_range();
604            let visible_range = start_idx..end_idx;
605            let bars = &self.state.data().bars;
606
607            // Get coordinate parameters from time scale for x-axis alignment
608            let time_scale = self.state.time_scale();
609            let coords = IndicatorCoordParams::new(
610                time_scale.bar_spacing(),
611                time_scale.right_offset(),
612                self.state.data().len().saturating_sub(1),
613                start_idx,
614            );
615
616            let mut has_pane_indicators = false;
617            for indicator in indicators.indicators() {
618                if indicator.is_overlay() || !indicator.is_visible() {
619                    continue;
620                }
621                has_pane_indicators = true;
622                break;
623            }
624
625            if has_pane_indicators {
626                for (idx, indicator) in indicators.indicators().iter().enumerate() {
627                    if indicator.is_overlay() || !indicator.is_visible() {
628                        continue;
629                    }
630
631                    // Minimal gap, no visible separator (seamless panes)
632                    ui.add_space(DESIGN_TOKENS.spacing.hairline);
633
634                    let config = match indicator.name() {
635                        "RSI" => IndicatorPaneConfig::rsi(),
636                        "MACD" => IndicatorPaneConfig::macd(),
637                        "Stochastic" => IndicatorPaneConfig::stochastic(),
638                        _ => IndicatorPaneConfig::default(),
639                    };
640
641                    let mut panel =
642                        IndicatorPane::with_config(egui::Id::new("main_chart_x_axis"), config);
643
644                    // Use show_aligned_interactive to get pane info for hit testing
645                    if let Some((panel_rect, chart_rect, y_min, y_max, _response)) = panel
646                        .show_aligned_interactive(
647                            ui,
648                            indicator.as_ref(),
649                            bars,
650                            visible_range.clone(),
651                            coords,
652                        )
653                    {
654                        // Store the pane info for hit testing by platform
655                        self.last_rendered_indicator_panes
656                            .push(RenderedIndicatorPane {
657                                indicator_idx: idx,
658                                panel_rect,
659                                chart_rect,
660                                y_min,
661                                y_max,
662                                coords,
663                            });
664                    }
665                }
666            }
667        }
668
669        response
670    }
671
672    /// Renders the chart with indicators using simple per-bar x-positioning.
673    ///
674    /// Unlike [`Chart::show_with_indicators`], which uses aligned coordinate
675    /// parameters from the main chart's time scale, this method creates
676    /// indicator panes with basic visible-range positioning. It is simpler
677    /// but may not perfectly align indicator data points with the main chart
678    /// when the user scrolls or zooms. Prefer [`Chart::show_with_indicators`]
679    /// for production use.
680    pub fn show_with_indicators_plot(
681        &mut self,
682        ui: &mut Ui,
683        drawing_manager: Option<&mut DrawingManager>,
684        indicators: Option<&IndicatorRegistry>,
685    ) -> Response {
686        let response = self.show_with_drawings(ui, drawing_manager);
687
688        if let Some(indicators) = indicators {
689            ui.separator();
690
691            let (start_idx, end_idx) = self.state.visible_range();
692            let visible_range = start_idx..end_idx;
693            let bars = &self.state.data().bars;
694
695            for indicator in indicators.indicators() {
696                if indicator.is_overlay() {
697                    continue;
698                }
699
700                if !indicator.is_visible() {
701                    continue;
702                }
703
704                let config = match indicator.name() {
705                    "RSI" => IndicatorPaneConfig::rsi(),
706                    "MACD" => IndicatorPaneConfig::macd(),
707                    "Stochastic" => IndicatorPaneConfig::stochastic(),
708                    _ => IndicatorPaneConfig::default(),
709                };
710
711                let mut panel =
712                    IndicatorPane::with_config(egui::Id::new("main_chart_x_axis"), config);
713
714                panel.show(ui, indicator.as_ref(), bars, visible_range.clone());
715            }
716        }
717
718        response
719    }
720
721    /// Renders the chart with standard mouse interactions.
722    ///
723    /// This is the simplest way to display a chart. It handles pan, zoom,
724    /// crosshair, keyboard shortcuts, and all visual elements configured in
725    /// [`ChartConfig`]. No drawing tools or separate-pane indicators are rendered.
726    ///
727    /// Returns an [`egui::Response`] for additional interaction handling.
728    ///
729    /// # Example
730    ///
731    /// ```rust,ignore
732    /// egui::CentralPanel::default().show(ctx, |ui| {
733    ///     let response = chart.show(ui);
734    ///     if response.hovered() {
735    ///         // Chart is being hovered
736    ///     }
737    /// });
738    /// ```
739    pub fn show(&mut self, ui: &mut Ui) -> Response {
740        self.show_internal(ui, None, None)
741    }
742
743    /// Internal rendering method that orchestrates all modules
744    pub(crate) fn show_internal(
745        &mut self,
746        ui: &mut Ui,
747        drawing_manager: Option<&mut DrawingManager>,
748        indicators: Option<&IndicatorRegistry>,
749    ) -> Response {
750        // Reset zoom_just_applied flag at the start of each frame
751        self.zoom_just_applied = false;
752
753        let available_size = ui.available_size();
754        let (mut response, painter) = ui.allocate_painter(available_size, Sense::click_and_drag());
755        let rect = response.rect;
756
757        // Establish chart_rect FIRST before any operations
758        let top_padding = if self.config.show_ohlc_info {
759            sizing::chart::TOP_PADDING_WITH_OHLC
760        } else {
761            sizing::chart::TOP_PADDING_NO_OHLC
762        };
763        let bottom_padding = if self.config.show_time_labels {
764            sizing::chart::BOTTOM_PADDING_WITH_TIME
765        } else {
766            sizing::chart::BOTTOM_PADDING_NO_TIME
767        };
768        let right_axis_width = sizing::chart::RIGHT_AXIS_WIDTH;
769
770        let left_margin = sizing::chart::PADDING;
771        let right_margin = sizing::chart::PADDING + right_axis_width;
772
773        let chart_rect = Rect::from_min_size(
774            rect.min + Vec2::new(left_margin, top_padding),
775            Vec2::new(
776                (rect.width() - left_margin - right_margin).max(sizing::chart::MIN_CHART_WIDTH),
777                (rect.height() - top_padding - bottom_padding).max(sizing::chart::MIN_CHART_HEIGHT),
778            ),
779        );
780
781        let chart_width = chart_rect.width();
782
783        // CRITICAL: Apply TimeScale width configuration BEFORE any zoom operations
784        // This ensures apply_constraints() inside zoom() uses the correct self.width
785        // to calculate constraint bounds. Without this, drawings drift during zoom
786        // because constraints are calculated with stale width values.
787        self.apply_timescale_config(chart_width);
788
789        // Handle tracking mode
790        self.handle_tracking_mode(ui, &response);
791
792        // Request focus on hover for keyboard shortcuts
793        self.request_focus_if_needed(&mut response);
794
795        // Handle keyboard shortcuts
796        self.handle_keyboard_shortcuts(ui, &response, chart_width, chart_rect.min.x);
797
798        // Calculate visible bars
799        let logical_range = self.state.time_scale().visible_logical_range();
800        let visible_bars = logical_range.length().ceil() as usize;
801        self.last_visible_bars = visible_bars;
802
803        // Show grabbing cursor during panning
804        self.set_panning_cursor(ui, &response);
805
806        // Define axis rects
807        let price_axis_rect = Rect::from_min_max(
808            Pos2::new(chart_rect.max.x, chart_rect.min.y),
809            Pos2::new(rect.max.x, chart_rect.max.y),
810        );
811        let time_axis_rect = Rect::from_min_max(
812            Pos2::new(chart_rect.min.x, chart_rect.max.y),
813            Pos2::new(chart_rect.max.x, rect.max.y),
814        );
815
816        // Handle double-click to reset axes
817        self.handle_double_click(&response, price_axis_rect, time_axis_rect);
818
819        // Handle mouse wheel for zoom/scroll
820        // Block pan/zoom when drawing tool is active OR when manipulating a drawing
821        let is_drawing_interaction = drawing_manager.as_ref().is_some_and(|dm| {
822            dm.active_tool.is_some() || dm.dragging_handle.is_some() || dm.curr_drawing.is_some()
823        });
824        let pending_price_zoom = self.handle_mouse_wheel(
825            ui,
826            &response,
827            chart_width,
828            chart_rect.min.x,
829            price_axis_rect,
830        );
831
832        // Handle pinch-to-zoom for touch/trackpad gestures
833        self.handle_pinch_zoom(ui, &response, chart_width, chart_rect.min.x);
834
835        // Handle drag to pan (blocked when interacting with drawings)
836        self.handle_drag_pan(
837            ui,
838            &response,
839            price_axis_rect,
840            time_axis_rect,
841            chart_rect.min.x,
842            is_drawing_interaction,
843        );
844
845        // Apply kinetic scrolling
846        self.apply_kinetic_scroll(ui);
847
848        // Handle box zoom (only when zoom mode is active from toolbar)
849        // Right-click is reserved for context menu, zoom uses left-click when mode is active
850        self.zoom_just_applied = self.handle_box_zoom(
851            ui,
852            &response,
853            chart_rect,
854            chart_width,
855            self.zoom_mode_active,
856        );
857        if self.zoom_just_applied {
858            log::info!("Zoom applied - chart will auto-deactivate zoom mode");
859        }
860
861        // Set zoom-in cursor when zoom mode is active
862        if self.zoom_mode_active && response.hovered() {
863            ui.ctx().set_cursor_icon(egui::CursorIcon::ZoomIn);
864        }
865        // Set cursor icon based on crosshair style (cursor type)
866        // This is only for default case - drawing manager may override for eraser mode
867        else if response.hovered() {
868            use crate::config::CrosshairStyle;
869            match self.chart_options.crosshair.style {
870                CrosshairStyle::Full => {
871                    // Cross cursor mode - show crosshair cursor
872                    ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair);
873                }
874                CrosshairStyle::Dot => {
875                    // Dot mode - default pointer (dot is rendered on chart)
876                    ui.ctx().set_cursor_icon(egui::CursorIcon::Default);
877                }
878                CrosshairStyle::Arrow => {
879                    // Arrow mode - default pointer
880                    ui.ctx().set_cursor_icon(egui::CursorIcon::Default);
881                }
882            }
883        }
884
885        // Draw background (solid or gradient)
886        self.draw_background(&painter, rect);
887
888        // Draw watermark overlay (if enabled)
889        self.draw_watermark(&painter, chart_rect);
890
891        if self.state.data().is_empty() {
892            painter.text(
893                rect.center(),
894                egui::Align2::CENTER_CENTER,
895                "No data available",
896                egui::FontId::proportional(typography::LG),
897                self.config.text_color,
898            );
899            return response;
900        }
901
902        // Split chart area into price and volume sections
903        let (price_rect, volume_rect) = if self.config.show_volume {
904            let split_y =
905                chart_rect.min.y + chart_rect.height() * (1.0 - self.config.volume_height_fraction);
906            (
907                Rect::from_min_max(chart_rect.min, Pos2::new(chart_rect.max.x, split_y)),
908                Rect::from_min_max(Pos2::new(chart_rect.min.x, split_y), chart_rect.max),
909            )
910        } else {
911            (chart_rect, Rect::ZERO)
912        };
913
914        // Get visible range
915        let (start_idx, _end_idx) = self.state.visible_range();
916        self.start_idx = start_idx;
917
918        // Capture near_live status for button
919        let near_live_edge = self.state.time_scale().right_offset() >= -1.5;
920
921        // Handle "Jump to Latest" button interaction
922        if !near_live_edge {
923            let btn_size = Vec2::new(
924                DESIGN_TOKENS.sizing.charts_ext.realtime_button_width,
925                DESIGN_TOKENS.sizing.button_md,
926            );
927            let btn_pos = Pos2::new(
928                price_rect.center().x - btn_size.x / 2.0,
929                price_rect.min.y + DESIGN_TOKENS.spacing.lg + DESIGN_TOKENS.spacing.xs,
930            );
931            let btn_rect = Rect::from_min_size(btn_pos, btn_size);
932            let btn_id = ui.id().with("jump_to_latest");
933            let btn_res = ui.interact(btn_rect, btn_id, egui::Sense::click());
934
935            if btn_res.clicked() {
936                self.state.time_scale_mut().scroll_to_realtime();
937            }
938        }
939
940        // Determine price bounds
941        let (mut adjusted_min, mut adjusted_max) = self.state.price_range();
942
943        // Apply price zoom
944        let (new_min, new_max) = self.apply_price_zoom(
945            pending_price_zoom,
946            &response,
947            chart_rect,
948            adjusted_min,
949            adjusted_max,
950        );
951        adjusted_min = new_min;
952        adjusted_max = new_max;
953
954        // Store the final rendered price range and rects for external use (selection dots, hit testing)
955        self.last_rendered_price_range = (adjusted_min, adjusted_max);
956        self.last_rendered_price_rect = price_rect;
957        self.last_rendered_volume_rect = volume_rect;
958
959        // Get visible data
960        let visible_data = self.state.visible_data();
961
962        if visible_data.is_empty() {
963            return response;
964        }
965
966        // Calculate volume range
967        let max_volume = if self.config.show_volume {
968            visible_data
969                .iter()
970                .map(|c| c.volume)
971                .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
972                .unwrap_or(1.0)
973        } else {
974            1.0
975        };
976
977        // Draw grid
978        if self.config.show_horizontal_grid {
979            rendering::render_grid(
980                &painter,
981                price_rect,
982                adjusted_min,
983                adjusted_max,
984                self.config.grid_color,
985            );
986        }
987
988        // Create rendering contexts
989        let bar_spacing = self.state.time_scale().bar_spacing();
990        let bar_width = bar_spacing * self.config.candle_width;
991        let price_ctx = RenderContext::new(&painter, price_rect);
992        let price_scale = PriceScale::new(adjusted_min, adjusted_max);
993        let coords = ChartMapping::new(
994            price_rect,
995            bar_spacing,
996            start_idx,
997            self.state.time_scale().base_idx(),
998            self.state.time_scale().right_offset(),
999            adjusted_min,
1000            adjusted_max,
1001        );
1002        let colors = StyleColors {
1003            bullish: self.config.bullish_color,
1004            bearish: self.config.bearish_color,
1005            grid: self.config.grid_color,
1006            text: self.config.text_color,
1007            bullish_border: self.config.bullish_border_color,
1008            bearish_border: self.config.bearish_border_color,
1009            bullish_wick: self.config.bullish_wick_color,
1010            bearish_wick: self.config.bearish_wick_color,
1011            candle_border_width: self.config.candle_border_width,
1012        };
1013
1014        let formatter = if self.config.show_time_labels || self.config.show_vertical_grid {
1015            Some(
1016                TimeFormatterBuilder::new()
1017                    .with_24_hour(true)
1018                    .with_seconds(true)
1019                    .with_timezone(self.chart_options.time_scale.timezone.clone())
1020                    .build(),
1021            )
1022        } else {
1023            None
1024        };
1025
1026        if self.config.show_vertical_grid {
1027            // Simple bar-index-based vertical grid - moves 1:1 with chart
1028            rendering::render_vertical_grid(&painter, chart_rect, &coords, self.config.grid_color);
1029        }
1030
1031        // Render chart type with clipping to prevent bars from overlapping axes
1032        // CRITICAL: Use chart_rect.width() for consistency with drawing coordinate system
1033        let chart_rect_width = chart_rect.width();
1034        let idx_to_coord = |idx: usize, min_x: f32| -> f32 {
1035            self.state
1036                .time_scale()
1037                .idx_to_coord(idx, min_x, chart_rect_width)
1038        };
1039
1040        // Create clipped painter and contexts to prevent bars from rendering on axes
1041        let clipped_painter = painter.with_clip_rect(chart_rect);
1042        let clipped_price_ctx = RenderContext::new(&clipped_painter, price_rect);
1043        let clipped_volume_ctx = RenderContext::new(&clipped_painter, volume_rect);
1044
1045        let render_ctx = rendering::CandleDataContext {
1046            price_ctx: &clipped_price_ctx,
1047            volume_ctx: &clipped_volume_ctx,
1048            price_scale: &price_scale,
1049            colors: &colors,
1050            visible_data,
1051            start_idx,
1052        };
1053
1054        let render_params = rendering::ChartTypeParams::new(
1055            rendering::BarDimensions::new(bar_width, self.config.wick_width),
1056            rendering::VolumeSettings::new(self.config.show_volume, max_volume),
1057            rendering::JapaneseChartSettings::new(self.renko_brick_size, self.kagi_reversal_amount),
1058            rendering::TradingColors::new(self.config.bullish_color, self.config.bearish_color),
1059            rendering::CoordMapping::new(chart_rect.min.x),
1060            self.config.price_source,
1061        );
1062
1063        rendering::render_chart_type(self.chart_type, &render_ctx, &render_params, idx_to_coord);
1064
1065        // Draw indicators
1066        if let Some(indicator_registry) = indicators {
1067            renderers::IndicatorRenderer::render(
1068                &price_ctx,
1069                indicator_registry.indicators(),
1070                visible_data,
1071                &price_scale,
1072                &coords,
1073            );
1074        }
1075
1076        // Render bar marks (Widget API annotations)
1077        if !self.marks.is_empty() {
1078            renderers::render_markers(
1079                &clipped_price_ctx,
1080                &self.marks,
1081                visible_data,
1082                &price_scale,
1083                &coords,
1084            );
1085        }
1086
1087        // Draw price labels
1088        if self.config.show_right_axis {
1089            rendering::render_price_labels(
1090                &price_ctx,
1091                &price_scale,
1092                &colors,
1093                crate::scales::PriceScaleMode::Normal,
1094            );
1095        }
1096
1097        // Last price line & label
1098        if self.config.show_symbol_last_val
1099            && let Some(last) = visible_data.last()
1100        {
1101            rendering::render_last_price_line(
1102                &painter,
1103                price_rect,
1104                last.close,
1105                last.open,
1106                adjusted_min,
1107                adjusted_max,
1108                self.config.bullish_color,
1109                self.config.bearish_color,
1110                self.config.show_right_axis,
1111            );
1112        }
1113
1114        // Draw time labels
1115        if self.config.show_time_labels {
1116            let chart_ctx = RenderContext::new(&painter, chart_rect);
1117            rendering::render_time_labels(
1118                &chart_ctx,
1119                visible_data,
1120                &coords,
1121                &colors,
1122                formatter.as_deref(),
1123            );
1124        }
1125
1126        // Draw OHLC info header (legend if symbol is set)
1127        if self.config.show_ohlc_info {
1128            if !self.symbol.is_empty() {
1129                // Calculate prev_close from second-to-last bar for change calculation
1130                let prev_close = if visible_data.len() >= 2 {
1131                    Some(visible_data[visible_data.len() - 2].close)
1132                } else {
1133                    None
1134                };
1135                rendering::render_legend(
1136                    &painter,
1137                    rect,
1138                    &self.symbol,
1139                    &self.timeframe,
1140                    visible_data,
1141                    prev_close,
1142                    &colors,
1143                    sizing::chart::PADDING,
1144                );
1145            } else {
1146                // Fallback to basic OHLC info
1147                rendering::render_ohlc_info(
1148                    &painter,
1149                    rect,
1150                    visible_data,
1151                    sizing::chart::PADDING,
1152                    self.config.text_color,
1153                );
1154            }
1155        }
1156
1157        // Handle drawing tools
1158        if let Some(dm) = drawing_manager {
1159            // Clone/extract values before mutable borrow of self
1160            let timescale = self.state.time_scale().clone();
1161            let last_close = visible_data.last().map(|b| b.close);
1162
1163            // Temporarily take cursor_modes to avoid borrow conflict
1164            let mut cursor_modes = std::mem::take(&mut self.cursor_modes);
1165
1166            self.handle_drawings(
1167                ui,
1168                dm,
1169                &mut cursor_modes,
1170                &response,
1171                price_rect,
1172                adjusted_min,
1173                adjusted_max,
1174                &painter,
1175                last_close,
1176                &timescale,
1177            );
1178
1179            // Render eraser highlight if in eraser mode
1180            self.render_eraser_highlight(&painter, dm, &cursor_modes);
1181
1182            // Put cursor_modes back
1183            self.cursor_modes = cursor_modes;
1184        }
1185
1186        // Render "Jump to Latest" button
1187        if self.config.show_realtime_btn {
1188            let btn_id = ui.id().with("jump_to_latest");
1189            let btn_size = Vec2::new(
1190                DESIGN_TOKENS.sizing.charts_ext.realtime_button_width,
1191                DESIGN_TOKENS.sizing.button_md,
1192            );
1193            let btn_pos = Pos2::new(
1194                price_rect.center().x - btn_size.x / 2.0,
1195                price_rect.min.y + DESIGN_TOKENS.spacing.lg + DESIGN_TOKENS.spacing.xs,
1196            );
1197            let btn_rect = Rect::from_min_size(btn_pos, btn_size);
1198            let btn_res = ui.interact(btn_rect, btn_id, egui::Sense::click());
1199
1200            rendering::render_realtime_btn(
1201                &painter,
1202                price_rect,
1203                near_live_edge,
1204                self.config.show_realtime_btn,
1205                self.config.realtime_button_size,
1206                self.config.realtime_button_pos,
1207                self.config.realtime_button_color,
1208                self.config.realtime_button_hover_color,
1209                self.config.realtime_button_text_color,
1210                self.config.realtime_button_text.as_deref(),
1211                btn_res.hovered(),
1212            );
1213        }
1214
1215        // Draw crosshair with options from chart_options
1216        if let Some(hover_pos) = response.hover_pos()
1217            && price_rect.contains(hover_pos)
1218        {
1219            // Cache the hover bar index for multi-chart sync
1220            self.last_hover_bar_idx = Some(coords.x_to_idx_f32(hover_pos.x) as f64);
1221
1222            rendering::render_crosshair_with_options(
1223                &price_ctx,
1224                hover_pos,
1225                visible_data,
1226                &price_scale,
1227                &coords,
1228                &self.chart_options.crosshair,
1229            );
1230        } else {
1231            // Clear hover bar index when not hovering locally
1232            self.last_hover_bar_idx = None;
1233
1234            // Render synced crosshair from other charts (if available)
1235            {
1236                if let Some(bar_idx) = self.synced_crosshair_bar_idx {
1237                    // Convert bar index to screen x coordinate
1238                    let x = coords.idx_to_x(bar_idx as usize);
1239                    if coords.is_x_visible(x) {
1240                        // Create a synthetic hover position at the center of the price range
1241                        let center_y = price_rect.center().y;
1242                        let synced_pos = Pos2::new(x, center_y);
1243
1244                        rendering::render_crosshair_with_options(
1245                            &price_ctx,
1246                            synced_pos,
1247                            visible_data,
1248                            &price_scale,
1249                            &coords,
1250                            &self.chart_options.crosshair,
1251                        );
1252                    }
1253                }
1254            }
1255        }
1256
1257        // Draw box zoom rect
1258        rendering::render_box_zoom(&painter, &self.box_zoom);
1259
1260        // Focus ring for keyboard accessibility
1261        crate::styles::focus::draw_focus_ring(ui, &response);
1262
1263        response
1264    }
1265
1266    // =========================================================================
1267    // Multi-Chart Sync Methods
1268    // =========================================================================
1269
1270    /// Sets an external crosshair position from a synced chart (in bar-index coordinates).
1271    ///
1272    /// When set to `Some(idx)`, a crosshair is drawn at the given bar index even
1273    /// if the user is not hovering over this chart. Pass `None` to clear it.
1274    /// This is the receiver side of multi-chart crosshair synchronization.
1275    pub fn set_synced_crosshair_bar_idx(&mut self, bar_idx: Option<f64>) {
1276        self.synced_crosshair_bar_idx = bar_idx;
1277    }
1278
1279    /// Returns the bar index that the user was hovering over in the last frame.
1280    ///
1281    /// Returns `None` if the cursor was not over the chart. This is the emitter
1282    /// side of multi-chart crosshair synchronization: read this value and pass
1283    /// it to [`Chart::set_synced_crosshair_bar_idx`] on other charts.
1284    pub fn get_hover_bar_idx(&self) -> Option<f64> {
1285        self.last_hover_bar_idx
1286    }
1287
1288    /// Applies time-scale state from another chart for synchronized scrolling/zooming.
1289    ///
1290    /// Sets both bar spacing and right offset to match the source chart so that
1291    /// both charts display the same time range. Use together with
1292    /// [`Chart::get_time_scale_state`] on the source chart.
1293    pub fn apply_synced_time_scale(&mut self, bar_spacing: f32, right_offset: f32) {
1294        self.state.time_scale_mut().set_bar_spacing(bar_spacing);
1295        self.state.time_scale_mut().set_right_offset(right_offset);
1296    }
1297
1298    /// Returns the current time-scale state as `(bar_spacing, right_offset)`.
1299    ///
1300    /// This is the emitter side of multi-chart time-scale synchronization.
1301    /// Pass the returned values to [`Chart::apply_synced_time_scale`] on other
1302    /// charts to keep them scrolled/zoomed in unison.
1303    pub fn get_time_scale_state(&self) -> (f32, f32) {
1304        (
1305            self.state.time_scale().bar_spacing(),
1306            self.state.time_scale().right_offset(),
1307        )
1308    }
1309
1310    /// Get a [`ChartMapping`] for coordinate conversions.
1311    ///
1312    /// Returns a mapping constructed from the last rendered frame's parameters
1313    /// (price rect, bar spacing, right offset, price range). This is used for
1314    /// converting between screen coordinates and data coordinates, particularly
1315    /// for drawing tool restoration and hit testing.
1316    pub fn get_chart_mapping(&self) -> ChartMapping {
1317        ChartMapping::new(
1318            self.last_rendered_price_rect,
1319            self.state.time_scale().bar_spacing(),
1320            self.start_idx,
1321            self.state.time_scale().base_idx(),
1322            self.state.time_scale().right_offset(),
1323            self.last_rendered_price_range.0,
1324            self.last_rendered_price_range.1,
1325        )
1326    }
1327}