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 ×cale,
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}