Skip to main content

egui_charts/widget/
mod.rs

1//! Interactive charts, events, panes, layouts and export.
2//!
3//! Keep a [`Chart`] across frames and call [`Chart::show`] from your egui UI callback.
4//! Construct it from [`BarData`](crate::model::BarData), or use [`ChartBuilder`] to
5//! configure an instrument, interval and optional [`Datafeed`](crate::data::Datafeed).
6//!
7//! # Updating data
8//!
9//! Merge each live bar into its series. The chart updates studies when it next draws;
10//! use [`Chart::set_data`] to replace a series' history.
11//!
12//! ```
13//! use egui_charts::{Chart, model::{Bar, BarData}, widget::SeriesId};
14//!
15//! let mut chart = Chart::new(BarData::default());
16//! let bar = Bar::new("2024-01-01T00:00:00Z".parse().unwrap(),
17//!                    100.0, 102.0, 99.0, 101.0, 50.0);
18//! chart.update(SeriesId::MAIN, bar);
19//! # egui::__run_test_ui(|ui| {
20//! chart.show(ui);
21//! # });
22//! ```
23//!
24//! # Host integration
25//!
26//! Read [`ChartEvent`] values with [`Chart::drain_events`] for host legends,
27//! inspection and application actions. [`ChartLayout`] arranges multiple charts
28//! and synchronizes their viewports and crosshairs by timestamp.
29//!
30//! Studies added with [`Chart::add_indicator`] are calculated and drawn by the
31//! chart. Built-in studies require the `studies` feature; drawing tools require
32//! `drawings`. Toolbars and dialogs in `ui` are separate components placed and
33//! connected by the host.
34
35use crate::chart::rendering::ChartMapping;
36use crate::chart::selection::SelectionState;
37use crate::chart::series::SeriesSettings;
38use crate::config::{ChartConfig, ChartOptions};
39use crate::data::FeedState;
40#[cfg(feature = "drawings")]
41use crate::drawings::DrawingManager;
42use crate::model::Timeframe;
43use crate::model::{ChartType, ChartTypeParams};
44use crate::studies::ChartState;
45#[cfg(feature = "drawings")]
46use crate::widget::tool_interaction::cursor_modes::CursorModeState;
47
48pub use crate::chart::rendering::{
49    Legend, LegendStatus, QuoteSide, StudyAction, StudyValue, ValueFormatters, Watermark,
50    WatermarkLine,
51};
52pub use crate::chart::selection::{ChartElementId, SeriesId};
53use crate::scales::{PriceMap, PriceScale};
54use crate::studies::IndicatorRegistry;
55pub use builder::ChartBuilder;
56use chrono::{DateTime, Utc};
57use egui::{Pos2, Rect, Vec2};
58pub use export::{ChartImage, DataTable, ExportError, ImageExportOptions};
59pub use pane::{IndicatorCoordParams, IndicatorPane, IndicatorPaneConfig, PaneInteraction};
60use std::collections::BTreeMap;
61
62// Re-export from logic layer
63use pan_zoom::state;
64
65mod accessibility;
66#[cfg(feature = "drawings")]
67#[cfg_attr(docsrs, doc(cfg(feature = "drawings")))]
68mod alerts;
69#[cfg(feature = "drawings")]
70#[cfg_attr(docsrs, doc(cfg(feature = "drawings")))]
71mod anchors;
72mod builder;
73mod change_marks;
74#[cfg(feature = "drawings")]
75#[cfg_attr(docsrs, doc(cfg(feature = "drawings")))]
76mod clipboard;
77mod crosshair;
78pub(crate) use crosshair::menu_value;
79mod events;
80mod feed;
81mod marks;
82mod merge;
83mod signals;
84mod targets;
85#[cfg(feature = "drawings")]
86#[cfg_attr(docsrs, doc(cfg(feature = "drawings")))]
87pub use events::ImageRequest;
88pub use events::{ChartEvent, ChartPointer, DataOperation, PointerKind, PointerTarget};
89mod export;
90mod format;
91mod frame;
92mod history;
93pub(crate) mod interaction;
94pub(crate) mod keyboard;
95mod layout;
96mod navigation;
97mod palette;
98pub(crate) mod pan_zoom;
99mod pane;
100pub use pane::{ChartLayoutRects, PaneId, PaneOptions, RenderedIndicatorPane};
101#[cfg(feature = "drawings")]
102#[cfg_attr(docsrs, doc(cfg(feature = "drawings")))]
103mod posts;
104mod primitives;
105#[cfg(feature = "png-export")]
106mod raster;
107mod readout;
108mod replay;
109mod selection;
110mod series;
111pub mod settings;
112#[cfg(feature = "ui")]
113#[cfg_attr(docsrs, doc(cfg(feature = "ui")))]
114mod snapshot;
115#[cfg(feature = "drawings")]
116#[cfg_attr(docsrs, doc(cfg(feature = "drawings")))]
117mod stored_drawings;
118mod studies;
119#[cfg(feature = "studies")]
120#[cfg_attr(docsrs, doc(cfg(feature = "studies")))]
121mod study_templates;
122mod svg;
123#[cfg(feature = "studies")]
124mod trade_profile;
125
126#[cfg(feature = "drawings")]
127#[cfg_attr(docsrs, doc(cfg(feature = "drawings")))]
128mod drawing_api;
129#[cfg(feature = "drawings")]
130#[cfg_attr(docsrs, doc(cfg(feature = "drawings")))]
131pub use drawing_api::DrawingError;
132mod studies_data;
133pub use crate::chart::primitives::{
134    PriceAxisLabel, Primitive, PrimitiveId, PrimitivePass, PrimitiveView, TimeAxisLabel,
135};
136pub use pane::VisualOrder;
137pub use series::SeriesOptions;
138pub use series::{BarsInRange, MismatchDirection, PriceLineId};
139#[cfg(feature = "ui")]
140#[cfg_attr(docsrs, doc(cfg(feature = "ui")))]
141pub use snapshot::{
142    ChartSnapshot, RestoreError, SavedAnnotations, SavedLayout, SavedPane, SavedSeries, SavedStudy,
143};
144mod volume;
145pub use layout::{ChartLayout, ChartLayoutMode, ChartSyncOptions};
146pub(crate) mod sync;
147mod time_scale;
148use feed::FeedLink;
149pub use sync::VisibleTimeRange;
150#[cfg(feature = "drawings")]
151pub(crate) mod tool_interaction;
152
153pub use keyboard::{ShortcutMap, ShortcutRequest};
154pub use state::{BoxZoomState, ElasticBounceState, KineticScrollState};
155
156/// Interactive financial chart widget for egui.
157///
158/// `Chart` is the core rendering and interaction engine for displaying OHLCV
159/// financial data. It handles all aspects of chart visualization including
160/// candlestick/bar/line rendering, pan/zoom interactions, crosshair display,
161/// drawing tool integration, and indicator overlays.
162///
163/// # Creating a Chart
164///
165/// Use [`Chart::new`] or [`Chart::with_config`] to construct, then call
166/// [`Chart::show`] each frame to render:
167///
168/// ```rust
169/// # let bars: Vec<egui_charts::model::Bar> = (0..100).map(|i| { let p = 100.0 + (i as f64 * 0.2).sin() * 5.0; egui_charts::model::Bar::new(chrono::DateTime::from_timestamp(1_700_000_000 + i * 60, 0).unwrap(), p, p + 1.0, p - 1.0, p + 0.5, 1_000.0) }).collect();
170/// use egui_charts::model::BarData;
171/// use egui_charts::widget::Chart;
172///
173/// let bar_data = BarData::from_bars(bars);
174///
175/// let mut chart = Chart::new(bar_data);
176///
177/// # egui::__run_test_ui(|ui| {
178/// egui::CentralPanel::default().show(ui, |ui| {
179///     let response = chart.show(ui);
180///     // response can be used for additional interaction handling
181/// });
182/// # });
183/// ```
184///
185/// # Supported Chart Types
186///
187/// - **Candles** -- Standard Japanese candlestick chart
188/// - **Bars** -- OHLC bar chart
189/// - **Line** -- Close-price line chart
190/// - **Area** -- Filled area under the close-price line
191/// - **Renko** -- Fixed-size brick chart (set brick size with [`Chart::set_chart_type_params`])
192/// - **Kagi** -- Reversal-based chart (set reversal amount with [`Chart::set_chart_type_params`])
193///
194/// # Interaction Features
195///
196/// - **Pan**: Click and drag to scroll through history (with kinetic scrolling)
197/// - **Zoom**: Mouse wheel to zoom in/out, pinch-to-zoom on trackpads
198/// - **Box zoom**: Drag-select a region to zoom into (when zoom mode is active)
199/// - **Price scale drag**: Drag the price axis to scale vertically
200/// - **Crosshair**: Hover to see price/time at cursor position
201/// - **Keyboard shortcuts**: Arrow keys, Home/End, +/- for navigation
202/// - **Double-click**: Reset zoom on price or time axis
203///
204/// # Undo and redo
205///
206/// User drawing interactions and shortcuts share [`Chart::undo`] / [`Chart::redo`] with
207/// host toolbar, menu and dialog edits wrapped in [`Chart::user_action`]. Direct programmatic
208/// setters and feed/data updates do not enter this history. Use [`Chart::can_undo`] and
209/// [`Chart::can_redo`] for toolbar availability, and [`Chart::take_history_changed`] to
210/// synchronize host controls and data after restoring an interval or settings.
211pub struct Chart {
212    /// Backend state holding OHLCV data and coordinate system (time scale, price range).
213    pub(crate) state: ChartState,
214    replay: Option<replay::ReplayWindow>,
215    market_replay: Option<replay::MarketReplay>,
216    /// Visual configuration controlling colors, padding, grid visibility, and more.
217    pub(crate) config: ChartConfig,
218    applied_config_scale_mode: crate::scales::PriceScaleMode,
219    /// Whether the main price series and its volume are drawn.
220    pub(crate) main_series_visible: bool,
221    main_series_present: bool,
222    main_series_scale: crate::model::SeriesScale,
223    price_series: Vec<series::PriceSeries>,
224    heikin_ashi: crate::chart::rendering::candles::HeikinAshiCache,
225    series_scales: std::collections::HashMap<crate::model::SeriesScale, PriceScale>,
226    /// Chart behavior options (bar spacing, scroll/zoom constraints, time scale settings).
227    pub(crate) chart_options: ChartOptions,
228    pub(crate) keyboard: keyboard::KeyboardState,
229    history: history::ChartHistory,
230    pane_layout: pane::PaneLayout,
231    pane_options: PaneOptions,
232    /// Starting index of visible range (for backward compatibility)
233    pub(crate) start_idx: usize,
234    /// Desired number of visible bars (from app state)
235    pub(crate) desired_visible_bars: Option<usize>,
236    /// Cache of last computed visible bars for external syncing
237    pub(crate) last_visible_bars: usize,
238    /// Whether to apply `desired_visible_bars` on next frame only
239    pub(crate) apply_visible_bars_once: bool,
240    /// Kinetic scroll animation state (UI state only)
241    pub(crate) kinetic_scroll: KineticScrollState,
242    /// Last scroll position for drag tracking (UI state only)
243    pub(crate) scroll_start_pos: Option<Pos2>,
244    /// Initial right offset when starting scroll (for drag) (UI state only)
245    pub(crate) scroll_start_offset: Option<f64>,
246    /// Previous widget width for resize handling (UI state only)
247    pub(crate) prev_width: Option<f32>,
248    /// Price-axis drag this frame: anchor y where the drag began, and the y movement (UI state only)
249    pub(crate) price_axis_drag: Option<(f32, f32)>,
250    /// Apply external start-index once to time scale (UI state only)
251    pub(crate) pending_start_idx: Option<usize>,
252    /// Timestamp to centre, retained while older history is loading.
253    pub(crate) pending_scroll_time: Option<DateTime<Utc>>,
254    /// Animated scroll requested by the host, advanced each frame (UI state only)
255    scroll_animation: Option<time_scale::ScrollAnimation>,
256    /// Time axis below the plots in the last frame; zero height while its labels are hidden
257    time_axis_rect: Rect,
258    /// Chart type (candlestick, line, area, bar, Renko, Kagi)
259    pub(crate) chart_type: ChartType,
260    /// Parameters of the price-based chart types and the baseline
261    pub(crate) chart_type_params: ChartTypeParams,
262    #[cfg(feature = "studies")]
263    trade_profiles: trade_profile::TradeProfiles,
264    pending_box_sizes: [bool; 4],
265    /// Whether older source bars can contribute to the shown series.
266    pub(crate) older_bars_drawn: bool,
267    /// Whether tracking mode is currently active
268    pub(crate) tracking_mode_active: bool,
269    /// Mouse entered chart area (for tracking mode exit detection)
270    pub(crate) mouse_in_chart: bool,
271    /// Feed state of the main series; see [`Chart::feed_state`].
272    pub(crate) feed: FeedState,
273    /// Origins of main bars built from ticks, distinguishing trades from empty heartbeats.
274    pub(crate) from_ticks: BTreeMap<DateTime<Utc>, crate::data::merge::Origin>,
275    /// Optional oldest-bar eviction limits, unlimited when absent.
276    bar_limits: std::collections::HashMap<SeriesId, std::num::NonZeroUsize>,
277    /// The chart's studies, in the order they were added.
278    pub(crate) studies: IndicatorRegistry,
279    study_data: studies_data::StudyDataCache,
280    pub(crate) element_studies: crate::chart::rendering::candles::price_based::ElementStudies,
281    /// Range bars built from the held trades, while the chart type is Range.
282    execution_ranges: crate::chart::rendering::candles::price_based::ExecutionRanges,
283    #[cfg(feature = "studies")]
284    volume_setting: bool,
285    #[cfg(feature = "studies")]
286    volume_study: Option<SeriesId>,
287    /// Id of each study in `studies`.
288    pub(crate) study_ids: Vec<SeriesId>,
289    /// The most studies the chart takes through the interface, if capped.
290    study_limit: Option<usize>,
291    /// Whether each study in `studies` contributes to the main price auto-scale.
292    pub(crate) study_autoscale: Vec<bool>,
293    pub(crate) study_inputs: Vec<String>,
294    /// For each study in `studies`, the first bar it must be updated from on the next frame.
295    pub(crate) pending_from: Vec<Option<usize>>,
296    /// For each study in `studies`, the input time in seconds from which it may be recomputed
297    /// in full again.
298    pub(crate) next_refresh: Vec<f64>,
299    /// Id of the next study added.
300    pub(crate) next_series: usize,
301    /// Host primitives, in the order they were attached.
302    primitives: Vec<primitives::Attached>,
303    /// Id of the next primitive attached.
304    next_primitive: u64,
305    /// The chart's drawings and the active drawing tool.
306    #[cfg(feature = "drawings")]
307    pub(crate) drawings: DrawingManager,
308    #[cfg(feature = "drawings")]
309    symbol_drawings: std::collections::HashMap<
310        String,
311        (DrawingManager, marks::TimeOrdered<crate::model::Marker>),
312    >,
313    #[cfg(not(feature = "drawings"))]
314    symbol_markers: std::collections::HashMap<String, marks::TimeOrdered<crate::model::Marker>>,
315    /// Where each symbol's drawings are kept; see [`Chart::set_storage`].
316    #[cfg(feature = "drawings")]
317    drawing_store: Option<stored_drawings::DrawingStore>,
318    /// The id each study a snapshot saved has since [`Chart::restore`] added it again.
319    #[cfg(feature = "drawings")]
320    restored_studies: std::collections::HashMap<usize, usize>,
321    /// Right-click box zoom state
322    pub(crate) box_zoom: BoxZoomState,
323    /// Whether zoom mode is currently active (controlled by zoom toolbar button)
324    pub(crate) zoom_mode_active: bool,
325    /// Whether zoom was just applied in the last frame (for auto-deactivation)
326    pub(crate) zoom_just_applied: bool,
327    /// Current symbol being displayed (for legend)
328    pub(crate) symbol: String,
329    /// Metadata supplied directly by a host without a feed.
330    legend_instrument: Option<crate::model::Instrument>,
331    currency_choices: Vec<String>,
332    currency_choice: Option<String>,
333    legend_badge: Option<egui::ImageSource<'static>>,
334    watermark: Option<Watermark>,
335    legend_statuses: Vec<(String, egui::Color32, egui::Color32)>,
336    /// Current quote, independent of the historical readout.
337    quote: Option<crate::model::Level1Quote>,
338    quote_action: Option<QuoteSide>,
339    server_time_offset: Option<chrono::TimeDelta>,
340    bar_close_time: Option<chrono::DateTime<chrono::Utc>>,
341    pub(crate) indicator_settings_request: Option<SeriesId>,
342    session_quote: Option<crate::model::quote::SessionQuote>,
343    session_provider: Option<std::sync::Arc<dyn crate::model::SessionProvider>>,
344    horizontal_scale: Option<std::sync::Arc<dyn crate::scales::HorizontalScale>>,
345    locale: Option<String>,
346    date_format: Option<String>,
347    time_formatter: Option<std::sync::Arc<dyn crate::scales::TimeFormatter>>,
348    price_formatter: Option<std::sync::Arc<dyn crate::scales::PriceFormatter>>,
349    percentage_formatter: Option<std::sync::Arc<dyn crate::scales::PriceFormatter>>,
350    volume_formatter: Option<std::sync::Arc<dyn crate::scales::PriceFormatter>>,
351    series_formatters:
352        std::collections::HashMap<SeriesId, std::sync::Arc<dyn crate::scales::PriceFormatter>>,
353    extended_hours: bool,
354    conversion: Option<String>,
355    pub(crate) measured_axis_width: f32,
356    /// Last usable widget width and its first frame, including discarded sizing passes.
357    last_layout: Option<(f32, u64)>,
358    last_ratio_lock: bool,
359    axis_hover: Option<(Pos2, Rect)>,
360    scale_settings_requested: bool,
361    pub(crate) theme_override: Option<crate::theme::Theme>,
362    inherited_theme: Option<crate::theme::Theme>,
363    /// Palettes the colours of a restored configuration may still follow, until the next frame.
364    restored_palettes: Vec<ChartConfig>,
365    host_events: events::HostEvents,
366    pub(crate) indicator_source_request: Option<crate::studies::StudySource>,
367    events: marks::TimeOrdered<crate::model::Marker>,
368    time_scale_events: marks::TimeOrdered<crate::model::TimeScaleEvent>,
369    /// How many of `time_scale_events`, at their end, an export froze from a mark source.
370    frozen_source_events: usize,
371    event_visibility: crate::model::EventVisibility,
372    mark_source: Option<marks::MarkSource>,
373    #[cfg(feature = "drawings")]
374    post_source: Option<posts::PostSource>,
375    signal_sources: Vec<signals::SignalSource>,
376    targets: targets::TargetState,
377    show_price_targets: bool,
378    hidden_signal_categories: std::collections::BTreeSet<String>,
379    event_click: Option<String>,
380    /// Bar marks with host IDs painted this frame, and those of the previous frame, which
381    /// clicks resolve against.
382    mark_hits: std::cell::RefCell<Vec<marks::MarkHit>>,
383    clickable_marks: Vec<marks::MarkHit>,
384    /// Timeframe of the bars, for the legend, the session breaks and the feed
385    pub(crate) timeframe: Option<Timeframe>,
386    /// The feed the chart loads from, if it was built with one.
387    pub(crate) feed_link: Option<FeedLink>,
388    /// Cursor mode state (Demonstration, Magic, Eraser effects)
389    #[cfg(feature = "drawings")]
390    pub(crate) cursor_modes: CursorModeState,
391    /// The main series' last value, price line, base line, autoscale and click tolerance.
392    pub(crate) main_series_style: crate::model::SeriesStyle,
393    /// The last bar's time and price when the last price pulse saw it, and the input time of
394    /// that pulse's start, negative infinity before any update.
395    pub(crate) last_price_pulse: std::cell::Cell<Option<(chrono::DateTime<chrono::Utc>, f64, f64)>>,
396    /// Price mapping of the last rendered frame (includes zoom adjustments)
397    pub(crate) last_price_map: PriceMap,
398    /// Last rendered price rect (actual rect used for candle rendering); see
399    /// [`get_rendered_price_rect`](Chart::get_rendered_price_rect).
400    pub(crate) last_rendered_price_rect: Rect,
401    /// Last rendered volume rect (actual rect used for volume rendering)
402    pub(crate) last_rendered_volume_rect: Rect,
403    /// Last rendered indicator pane info for hit testing
404    /// Each entry: (indicator_index, panel_rect, chart_rect, y_min, y_max, coords)
405    pub(crate) last_rendered_indicator_panes: Vec<RenderedIndicatorPane>,
406    // =========================================================================
407    // Multi-Chart Sync State
408    // =========================================================================
409    /// This chart is an inactive, attached slot in a multi-chart layout for the current frame.
410    pub(crate) layout_inactive: bool,
411    /// External crosshair position from synced chart (bar index)
412    pub(crate) synced_crosshair_bar_idx: Option<f64>,
413    /// Source period retained by time-based setters and resolved against the current bars.
414    synced_crosshair_period: Option<(DateTime<Utc>, DateTime<Utc>)>,
415    /// Main-pane price of the external crosshair; `None` centres its horizontal line.
416    synced_crosshair_price: Option<f64>,
417    /// Time and main-pane price of the crosshair last drawn, for exported images.
418    last_crosshair: Option<(DateTime<Utc>, Option<f64>)>,
419    /// Last computed hover bar index (for sync emission to other charts)
420    pub(crate) last_hover_bar_idx: Option<f64>,
421
422    // =========================================================================
423    // Marks (Widget API)
424    // =========================================================================
425    /// Bar marks (annotations on chart bars, e.g., trade signals); see [`Chart::set_markers`].
426    pub(crate) marks: marks::TimeOrdered<crate::model::Marker>,
427    /// Fit and layer of the main series' marks; see [`Chart::set_marker_options`].
428    marker_options: crate::model::MarkerOptions,
429    /// Marks of the main series' recent changes; see [`Chart::set_change_markers`].
430    change_marks: Option<change_marks::ChangeMarks>,
431    /// Horizontal lines on the main series; see [`Chart::set_price_lines`].
432    price_lines: Vec<(PriceLineId, crate::model::PriceLine)>,
433    /// The number of price line handles issued.
434    next_price_line: u64,
435    /// Price axis tags of the trading lines drawn over the last frame, shown once on the next.
436    #[cfg(feature = "ui")]
437    pub(crate) trading_tags: Vec<(f64, egui::Color32)>,
438    #[cfg(feature = "ui")]
439    pub(crate) trading_outlined: Vec<usize>,
440    #[cfg(feature = "ui")]
441    pub(crate) trading_bid_ask: bool,
442    /// The order quantity the legend shows under the quote buttons' spread, set by trading for
443    /// the next frame only, so the box goes when trading stops drawing.
444    #[cfg(feature = "ui")]
445    pub(crate) order_quantity: Option<f64>,
446    /// The legend's quantity box, when the user clicked it, for trading to open its calculator.
447    #[cfg(feature = "ui")]
448    pub(crate) quantity_clicked: Option<egui::Rect>,
449
450    // =========================================================================
451    // Selection State
452    // =========================================================================
453    /// Chart-wide click selection, shared by series, overlay indicators, and
454    /// pane indicators. Populated by click hit testing during rendering and
455    /// readable by host apps via [`Chart::selected_element`].
456    pub(crate) selection: SelectionState,
457    /// Right-click hit result captured during the last frame, drained by the
458    /// host via [`Chart::take_right_click`] (or by `ChartMenus` with the `ui`
459    /// feature). Holds no UI types so the core widget builds without the `ui`
460    /// feature.
461    pub(crate) right_click: Option<RightClickTarget>,
462    /// Set by the context menus while one is open, so the next frame draws no crosshair or
463    /// tooltip under it. Cleared by every frame.
464    pub(crate) menu_open: bool,
465    /// Size to allocate instead of the available space; see [`Chart::set_size`].
466    size: Option<Vec2>,
467    /// The main bars in groups that fall within one pixel, while zoomed out that far.
468    merge_groups: merge::Groups,
469}
470
471/// The object a right-click landed on, with the data needed to position and
472/// populate a context menu.
473///
474/// Captured during [`Chart::show`] when the user
475/// right-clicks inside the chart area. Drain it once per frame with
476/// [`Chart::take_right_click`]. Right-clicking also selects the hit object (the
477/// same as a left click) so selection handles and the menu stay consistent.
478///
479/// This type intentionally carries no `ui`-feature types, so the core chart can
480/// surface right-clicks even when built without the `ui` feature; the host
481/// (or `ChartMenus` with the `ui` feature) maps it to a concrete menu.
482#[derive(Clone, Copy, Debug, PartialEq)]
483#[non_exhaustive]
484pub enum RightClickTarget {
485    /// A series or overlay/pane indicator line was hit.
486    Element {
487        /// The selected chart element (series or indicator).
488        id: ChartElementId,
489        /// Data-space index under the cursor, counting elements on price-based chart types.
490        bar_idx: usize,
491        /// Screen position of the click (menu anchor).
492        pos: Pos2,
493        /// Price under the cursor at the click position.
494        price: f64,
495    },
496    /// A drawing object was hit. The drawing is identified by its
497    /// drawing id.
498    Drawing {
499        /// Id of the hit drawing within the drawing manager.
500        drawing_id: usize,
501        /// Screen position of the click (menu anchor).
502        pos: Pos2,
503    },
504    /// Empty chart area was hit (no series, indicator, or drawing).
505    Background {
506        /// Screen position of the click (menu anchor).
507        pos: Pos2,
508        /// Price under the cursor at the click position.
509        price: f64,
510    },
511}
512
513impl Chart {
514    /// Supplies instrument metadata for a chart without a feed, including price precision
515    /// and the default tick-mark grid. An explicit `tick_marks.min_price_step` takes precedence.
516    /// Pass `None` to clear the metadata. Feed metadata takes precedence when available.
517    pub fn set_instrument(&mut self, instrument: Option<crate::model::Instrument>) {
518        self.legend_instrument = instrument;
519        let precision = self
520            .instrument()
521            .and_then(crate::scales::instrument_price_precision);
522        let tick_size = self
523            .instrument()
524            .and_then(|instrument| instrument.tick_size);
525        let scale = self.state.price_scale_mut();
526        scale.set_price_precision(precision);
527        scale.set_tick_size(tick_size);
528    }
529
530    /// Supplies currencies the host can convert the chart into. Choosing one emits an intent;
531    /// the host replaces bars and instrument metadata together after conversion.
532    pub fn set_currency_choices(&mut self, currencies: Vec<String>) {
533        self.currency_choices = currencies;
534    }
535
536    /// Takes the selected currency. No prices change until the host supplies converted data.
537    pub fn take_currency_choice(&mut self) -> Option<String> {
538        self.currency_choice.take()
539    }
540
541    /// Supplies an image for the legend's symbol mark in place of
542    /// [`Instrument::logo`](crate::model::Instrument::logo). `None` shows the instrument's logo,
543    /// or the symbol's initial without one;
544    /// [`ChartConfig::show_legend_badge`](crate::config::ChartConfig::show_legend_badge) hides the mark.
545    pub fn set_legend_badge(&mut self, badge: Option<egui::ImageSource<'static>>) {
546        self.legend_badge = badge;
547    }
548
549    /// Paints `watermark` behind the series of the price pane, in addition to the symbol
550    /// watermark of [`ChartConfig::show_watermark`](crate::config::ChartConfig::show_watermark).
551    /// `None` removes it.
552    pub fn set_watermark(&mut self, watermark: Option<Watermark>) {
553        self.watermark = watermark;
554    }
555
556    /// Supplies status marks and their host-defined meaning and colours.
557    pub fn set_legend_statuses(&mut self, statuses: &[LegendStatus<'_>]) {
558        self.legend_statuses = statuses
559            .iter()
560            .map(|status| (status.text.to_owned(), status.color, status.background))
561            .collect();
562    }
563
564    /// Supplies the current bid and ask, independently of the hovered historical bar.
565    /// Pass `None` when quotes are unavailable or the symbol changes.
566    pub fn set_quote(&mut self, quote: Option<crate::model::Level1Quote>) {
567        if let Some(replay) = &mut self.replay {
568            replay.quote = quote;
569            return;
570        }
571        self.quote = quote;
572    }
573
574    /// Supplies the current server time for a chart without a feed. The clock advances between
575    /// calls; update it when a replay jumps. `None` restores the feed clock or latest quote time.
576    pub fn set_server_time(&mut self, now: Option<chrono::DateTime<chrono::Utc>>) {
577        self.server_time_offset = now.map(|time| time - chrono::Utc::now());
578    }
579
580    /// Supplies an exact closing instant for the newest bar, for irregular sessions or replays.
581    /// `None` derives the close from the timeframe and the instrument's session.
582    pub fn set_bar_close_time(&mut self, close: Option<chrono::DateTime<chrono::Utc>>) {
583        self.bar_close_time = close;
584    }
585
586    /// Sets the right margin in bars and moves to that margin when it changes.
587    pub fn set_right_margin(&mut self, bars: f32) {
588        let options = &mut self.chart_options.time_scale;
589        if bars.is_finite()
590            && bars >= 0.0
591            && (options.right_offset != bars || options.right_offset_pixels.is_some())
592        {
593            options.right_offset = bars;
594            options.right_offset_pixels = None;
595            let scale = self.state.time_scale_mut();
596            scale.apply_options(
597                scale.bar_spacing(),
598                options.min_bar_spacing,
599                options.max_bar_spacing,
600                options.fix_left_edge,
601                options.fix_right_edge,
602                bars,
603                None,
604            );
605        }
606    }
607
608    /// Supplies an independent extended-session price and qualifier. `None` hides the tag.
609    pub fn set_session_quote(&mut self, quote: Option<crate::model::quote::SessionQuote>) {
610        if let Some(replay) = &mut self.replay {
611            replay.session_quote = quote;
612            return;
613        }
614        self.session_quote = quote;
615    }
616
617    /// Supplies event badges at the time-axis boundary, separately from bar annotations.
618    pub fn set_events(&mut self, events: Vec<crate::model::Marker>) {
619        self.events = events.into();
620    }
621
622    /// Takes the ID of the last clicked event badge.
623    pub fn take_event_click(&mut self) -> Option<String> {
624        self.event_click.take()
625    }
626
627    /// Takes a request to open the host's price-scale settings.
628    pub fn take_scale_settings_request(&mut self) -> bool {
629        std::mem::take(&mut self.scale_settings_requested)
630    }
631
632    /// Takes the last legend quote click. No order is submitted by the chart.
633    pub fn take_quote_action(&mut self) -> Option<QuoteSide> {
634        self.quote_action.take()
635    }
636
637    /// Activates or deactivates box-zoom mode.
638    ///
639    /// When active, left-click drag draws a selection rectangle and zooms into
640    /// that region. The mode auto-deactivates after a successful zoom operation
641    /// (check with [`Chart::zoom_was_applied`]).
642    pub fn set_zoom_mode(&mut self, active: bool) {
643        self.zoom_mode_active = active;
644    }
645
646    /// Restore the view preceding the last box zoom. Drawing and chart edit history is unchanged.
647    pub fn zoom_back(&mut self) -> bool {
648        self.pending_scroll_time = None;
649        self.tracking_mode_active = false;
650        self.state.pop_zoom_state()
651    }
652
653    /// Returns `true` if a box-zoom was completed in the most recent frame.
654    ///
655    /// Use this to auto-deactivate zoom mode in your toolbar after the user
656    /// completes a zoom selection.
657    pub fn zoom_was_applied(&self) -> bool {
658        self.zoom_just_applied
659    }
660
661    /// Sets the crosshair rendering style (Full, Dot, Arrow, Demonstration or Magic).
662    ///
663    /// Use this to connect a toolbar cursor-type selector to the chart.
664    /// The style controls how the crosshair lines and labels are drawn
665    /// when the user hovers over the chart area.
666    pub fn set_crosshair_style(&mut self, style: crate::config::CrosshairStyle) {
667        self.chart_options.crosshair.style = style;
668    }
669
670    /// Enables the configured values readout without changing pan or zoom.
671    pub fn set_tooltip_enabled(&mut self, enabled: bool) {
672        self.chart_options.tooltip.enabled = enabled;
673    }
674
675    /// Apply series settings to chart colors, price source and price scale.
676    ///
677    /// Copies candlestick colors (bullish/bearish fill, border, wick) and the
678    /// price source field from the given [`SeriesSettings`] into the chart's
679    /// [`ChartConfig`], and its scale mode and inversion into the main price
680    /// scale. A new scale mode returns the scale to auto-scaling and clears the
681    /// zoom history, whose ranges are in the old mode's units. Call this when
682    /// the user changes series appearance in a settings dialog.
683    pub fn apply_series_settings(&mut self, settings: &SeriesSettings) {
684        self.config.bullish_color = settings.bullish_color;
685        self.config.bearish_color = settings.bearish_color;
686        self.config.bullish_border_color = settings.bullish_border_color;
687        self.config.bearish_border_color = settings.bearish_border_color;
688        self.config.bullish_wick_color = settings.bullish_wick_color;
689        self.config.bearish_wick_color = settings.bearish_wick_color;
690        self.config.price_source = settings.price_source;
691        self.config.show_price_line = settings.show_price_line;
692        self.config.show_prev_close_line = settings.show_prev_close_line;
693        self.main_series_style.alerts = settings.alerts;
694        self.set_chart_type_params(settings.chart_type_params.clone());
695        if self.state.price_scale().options().mode != settings.price_scale_mode {
696            self.state.price_scale_mut().reset();
697            self.state.clear_zoom_history();
698        }
699        let options = self.state.price_scale_mut().options_mut();
700        options.mode = settings.price_scale_mode;
701        options.invert_scale = settings.invert_scale;
702    }
703
704    /// Read the current appearance and chart type parameters for the series settings dialog.
705    /// Pass the result to `SeriesSettingsDialog::open`, then apply its accepted settings with
706    /// [`Self::apply_series_settings`]. The active chart type is dialog context only.
707    pub fn series_settings(&self) -> SeriesSettings {
708        SeriesSettings {
709            chart_type: self.chart_type,
710            chart_type_params: self.chart_type_params.clone(),
711            bullish_color: self.config.bullish_color,
712            bearish_color: self.config.bearish_color,
713            bullish_border_color: self.config.bullish_border_color,
714            bearish_border_color: self.config.bearish_border_color,
715            bullish_wick_color: self.config.bullish_wick_color,
716            bearish_wick_color: self.config.bearish_wick_color,
717            price_source: self.config.price_source,
718            show_price_line: self.config.show_price_line,
719            show_prev_close_line: self.config.show_prev_close_line,
720            alerts: self.main_series_style.alerts,
721            price_scale_mode: self.state.price_scale().options().mode,
722            invert_scale: self.state.price_scale().options().invert_scale,
723            ..Default::default()
724        }
725    }
726
727    /// Get a [`ChartMapping`] for coordinate conversions.
728    ///
729    /// Returns a mapping constructed from the last rendered frame's parameters
730    /// (price rect, bar spacing, right offset, price range). This is used for
731    /// converting between screen coordinates and data coordinates, particularly
732    /// for drawing tool restoration and hit testing. On price-based chart types its indices
733    /// count elements instead of source bars.
734    pub fn get_chart_mapping(&self) -> ChartMapping {
735        ChartMapping::new(
736            self.last_rendered_price_rect,
737            self.state.time_scale().bar_spacing(),
738            self.start_idx,
739            self.state.time_scale().base_idx(),
740            self.state.time_scale().right_offset_precise(),
741            self.last_price_map,
742        )
743    }
744
745    /// Returns a mutable reference to the chart's visual configuration.
746    ///
747    /// Use this to edit visual options in place, e.g.
748    /// `chart.config_mut().show_grid = false`, without rebuilding the
749    /// whole [`ChartConfig`]. Changes take effect on the next rendered frame. Wrap a host UI
750    /// submission in [`Self::user_action`] to include it in chart history.
751    pub fn config_mut(&mut self) -> &mut ChartConfig {
752        &mut self.config
753    }
754
755    /// Apply a theme to this chart and to the UI in `ctx`, without changing data or pan and zoom.
756    ///
757    /// Other charts sharing the context follow the active theme on their next frame, keeping
758    /// colours the host has edited and any per-chart theme override.
759    pub fn set_theme(&mut self, ctx: &egui::Context, theme: &crate::theme::Theme) {
760        self.config = self.config.clone().with_theme(theme);
761        self.theme_override = None;
762        self.inherited_theme = Some(theme.clone());
763        crate::theme::apply_to_egui(ctx, theme);
764        ctx.request_repaint();
765    }
766
767    /// Overrides this chart's palette and text sizes without changing sibling charts or egui.
768    ///
769    /// `Some(theme)` also applies its series colours to the chart configuration. Explicit
770    /// configuration edits made afterwards are kept. `None` follows the host's palette again,
771    /// retaining host-edited colours. `set_theme` clears this override and themes egui.
772    pub fn set_theme_override(&mut self, theme: Option<crate::theme::Theme>) {
773        if let Some(theme) = &theme {
774            self.config = self.config.clone().with_theme(theme);
775            self.inherited_theme = Some(theme.clone());
776        }
777        self.theme_override = theme;
778    }
779
780    /// The chart's visual configuration.
781    pub fn config(&self) -> &ChartConfig {
782        &self.config
783    }
784
785    /// The chart's behaviour options, set with [`Chart::with_chart_options`] or
786    /// [`Chart::set_chart_options`].
787    pub fn chart_options(&self) -> &ChartOptions {
788        &self.chart_options
789    }
790
791    /// Replaces the chart's behaviour options at runtime, taking effect on the next frame.
792    ///
793    /// To change some options and keep the rest, edit a copy of the current ones:
794    ///
795    /// ```rust
796    /// # fn example(chart: &mut egui_charts::Chart) {
797    /// let mut options = chart.chart_options().clone();
798    /// options.crosshair.mode = egui_charts::config::CrosshairMode::Magnet;
799    /// options.time_scale.min_bar_spacing = 0.1;
800    /// chart.set_chart_options(options);
801    /// # }
802    /// ```
803    ///
804    /// The view is kept: the bar spacing changes only when `time_scale.bar_spacing` changes or
805    /// the new spacing bounds exclude it, and the chart scrolls only when `time_scale.right_offset`
806    /// or `right_offset_pixels` changes. A non-finite `bar_spacing`, `right_offset` or
807    /// `right_offset_pixels` keeps its previous value, and a `kinetic_scroll.damping_coeff`
808    /// outside `0.0..1.0` is replaced by the default.
809    pub fn set_chart_options(&mut self, options: ChartOptions) {
810        let options = self.normalize_chart_options(options);
811        let (old, new) = (&self.chart_options.time_scale, &options.time_scale);
812        if (
813            old.bar_spacing,
814            old.min_bar_spacing,
815            old.max_bar_spacing,
816            old.fix_left_edge,
817            old.fix_right_edge,
818            old.right_offset,
819            old.right_offset_pixels,
820        ) != (
821            new.bar_spacing,
822            new.min_bar_spacing,
823            new.max_bar_spacing,
824            new.fix_left_edge,
825            new.fix_right_edge,
826            new.right_offset,
827            new.right_offset_pixels,
828        ) {
829            let scale = self.state.time_scale_mut();
830            let offset = scale.right_offset_precise();
831            let spacing = if new.bar_spacing == old.bar_spacing {
832                scale.bar_spacing()
833            } else {
834                new.bar_spacing
835            };
836            scale.apply_options(
837                spacing,
838                new.min_bar_spacing,
839                new.max_bar_spacing,
840                new.fix_left_edge,
841                new.fix_right_edge,
842                new.right_offset,
843                new.right_offset_pixels,
844            );
845            if (old.right_offset, old.right_offset_pixels)
846                == (new.right_offset, new.right_offset_pixels)
847            {
848                scale.set_right_offset(offset);
849            }
850        }
851        self.chart_options = options;
852    }
853
854    /// Normalize numeric options before storing or comparing them, since NaN never equals itself.
855    fn normalize_chart_options(&self, mut options: ChartOptions) -> ChartOptions {
856        if !(0.0..1.0).contains(&options.kinetic_scroll.damping_coeff) {
857            options.kinetic_scroll.damping_coeff =
858                crate::config::KineticScrollOptions::default().damping_coeff;
859        }
860        let (old, new) = (&self.chart_options.time_scale, &mut options.time_scale);
861        if !new.bar_spacing.is_finite() {
862            new.bar_spacing = old.bar_spacing;
863        }
864        if !new.right_offset.is_finite() {
865            new.right_offset = old.right_offset;
866        }
867        if !new.right_offset_pixels.is_none_or(f32::is_finite) {
868            new.right_offset_pixels = old.right_offset_pixels;
869        }
870        options
871    }
872
873    /// Gives the chart an explicit size in points instead of filling the space `show` is given;
874    /// `None`, the default, fills it again. The size includes the axes and study panes.
875    pub fn set_size(&mut self, size: Option<Vec2>) {
876        self.size = size.filter(|size| size.is_finite() && size.min_elem() >= 0.0);
877    }
878
879    /// The explicit size set with [`Chart::set_size`].
880    pub fn size(&self) -> Option<Vec2> {
881        self.size
882    }
883
884    /// The chart's bars, time scale, price scale and zoom history, to read them.
885    pub fn state(&self) -> &ChartState {
886        &self.state
887    }
888
889    /// The main price scale, to zoom it or change its mode and options.
890    pub fn price_scale_mut(&mut self) -> &mut PriceScale {
891        self.state.price_scale_mut()
892    }
893
894    /// The chart's drawings and drawing tools.
895    #[cfg(feature = "drawings")]
896    #[cfg_attr(docsrs, doc(cfg(feature = "drawings")))]
897    pub fn drawings(&self) -> &DrawingManager {
898        &self.drawings
899    }
900
901    /// The chart's drawings and drawing tools, to set the active tool or to save and load the
902    /// drawings. Direct host edits do not enter chart history; wrap UI-originated edits in
903    /// [`Self::user_action`]. The manager's own history is for standalone drawing use.
904    #[cfg(feature = "drawings")]
905    #[cfg_attr(docsrs, doc(cfg(feature = "drawings")))]
906    pub fn drawings_mut(&mut self) -> &mut DrawingManager {
907        &mut self.drawings
908    }
909}
910
911/// Headless frames for widget tests: a fixed 800x600 screen and explicit input events.
912#[cfg(test)]
913pub(crate) mod test_frames {
914    #[cfg(target_arch = "wasm32")]
915    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
916
917    use super::Chart;
918    use crate::model::{Bar, BarData};
919    use chrono::{TimeZone, Utc};
920    use egui::{Context, CursorIcon, Event, Pos2, RawInput, Rect, Shape, Vec2};
921
922    /// 100 daily bars; bar `i` opens at `lows[i % len]` and closes at `highs[i % len]`.
923    pub(crate) fn chart(lows: &[f64], highs: &[f64]) -> Chart {
924        let bars = (0..100)
925            .map(|i| {
926                let t = Utc
927                    .timestamp_opt(1_700_000_000 + i as i64 * 86_400, 0)
928                    .unwrap();
929                let (lo, hi) = (lows[i % lows.len()], highs[i % highs.len()]);
930                Bar::new(t, lo, hi, lo, hi, 1.0)
931            })
932            .collect();
933        Chart::new(BarData::from_bars(bars))
934    }
935
936    fn run(
937        ctx: &Context,
938        chart: &mut Chart,
939        time: f64,
940        events: Vec<Event>,
941    ) -> (Vec<Shape>, CursorIcon) {
942        let input = RawInput {
943            screen_rect: Some(Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0))),
944            time: Some(time),
945            events,
946            ..Default::default()
947        };
948        let mut output = ctx.run_ui(input, |ui| {
949            egui::CentralPanel::default().show(ui, |ui| {
950                chart.show(ui);
951            });
952        });
953        fn flatten(shape: Shape, out: &mut Vec<Shape>) {
954            match shape {
955                Shape::Vec(shapes) => shapes.into_iter().for_each(|shape| flatten(shape, out)),
956                shape => out.push(shape),
957            }
958        }
959        let mut shapes = Vec::new();
960        for clipped in std::mem::take(&mut output.shapes) {
961            flatten(clipped.shape, &mut shapes);
962        }
963        let cursor = output.platform_output.cursor_icon;
964        output.drop_without_applying_deltas();
965        (shapes, cursor)
966    }
967
968    /// Runs one frame at `time` seconds with `events` and returns the shapes it painted.
969    pub(crate) fn shapes(
970        ctx: &Context,
971        chart: &mut Chart,
972        time: f64,
973        events: Vec<Event>,
974    ) -> Vec<Shape> {
975        run(ctx, chart, time, events).0
976    }
977
978    /// The heights of the candles in `color` among `shapes`.
979    pub(crate) fn candle_ys(shapes: &[Shape], color: egui::Color32) -> Vec<f32> {
980        shapes
981            .iter()
982            .flat_map(|shape| match shape {
983                Shape::Vec(shapes) => candle_ys(shapes, color),
984                Shape::Rect(rect) if rect.fill == color => {
985                    vec![rect.rect.min.y, rect.rect.max.y]
986                }
987                Shape::LineSegment { points, stroke } if stroke.color == color => {
988                    points.iter().map(|point| point.y).collect()
989                }
990                Shape::Mesh(mesh) => mesh
991                    .vertices
992                    .iter()
993                    .filter(|vertex| vertex.color == color)
994                    .map(|vertex| vertex.pos.y)
995                    .collect(),
996                _ => Vec::new(),
997            })
998            .collect()
999    }
1000
1001    /// Runs one frame at `time` seconds with `events` and returns the cursor it asked for.
1002    pub(crate) fn frame(
1003        ctx: &Context,
1004        chart: &mut Chart,
1005        time: f64,
1006        events: Vec<Event>,
1007    ) -> CursorIcon {
1008        run(ctx, chart, time, events).1
1009    }
1010}