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}