egui-charts 0.2.0

High-performance financial charting engine for egui — candlesticks, 95 drawing tools, 130+ indicators, and a full design-token theme system
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
//! TradingView-compatible Series, TimeScale, PriceScale, and Study API traits.
//!
//! These traits mirror the TradingView Lightweight Charts API surface, providing
//! programmatic access to coordinate conversion, bar data, pane management,
//! series options, and price/time scale configuration. They are designed for
//! use by plugin authors and scripting layers.
//!
//! # Key Traits
//!
//! - [`ISeriesApi`] -- coordinate conversion, bar data access, pane movement, options
//! - [`ITimeScaleApi`] -- visible range control, fit-content, subscriptions
//! - [`IPriceScaleApi`] -- price scale mode and options
//! - [`IStudyApi`] -- indicator/study lifecycle and visibility

use egui::Rect;

/// Trait for programmatic control of a chart series.
///
/// Mirrors the TradingView `ISeriesApi` interface, providing coordinate
/// conversion, bar data access, multi-pane support, and options management.
pub trait ISeriesApi {
    /// Get the series unique identifier
    fn series_id(&self) -> String;

    /// Get the series symbol
    fn symbol(&self) -> String;

    /// Get the series current price
    fn current_price(&self) -> Option<f64>;

    /// Get the series visible price range
    fn price_range(&self) -> (f64, f64);

    // =========================================================================
    // Coordinate Conversion (Critical for Pine Script and Trading)
    // =========================================================================

    /// Convert a price value to a Y coordinate in screen space
    ///
    /// This is essential for placing trading primitives (orders, positions)
    /// and for Pine Script overlays.
    ///
    /// # Arguments
    /// * `price` - The price to convert
    ///
    /// # Returns
    /// Y coordinate in screen space, or None if not visible
    fn price_to_coordinate(&self, price: f64) -> Option<f64>;

    /// Convert a Y coordinate to a price value
    ///
    /// Used for translating mouse positions to price values.
    ///
    /// # Arguments
    /// * `y` - Y coordinate in screen space
    ///
    /// # Returns
    /// Price value at that coordinate
    fn coordinate_to_price(&self, y: f64) -> Option<f64>;

    /// Convert a bar index to an X coordinate
    ///
    /// # Arguments
    /// * `bar_index` - The bar index (0-based from left)
    ///
    /// # Returns
    /// X coordinate in screen space
    fn bar_index_to_coordinate(&self, bar_index: usize) -> Option<f64>;

    /// Convert an X coordinate to a bar index
    ///
    /// # Arguments
    /// * `x` - X coordinate in screen space
    ///
    /// # Returns
    /// Bar index, or None if outside visible range
    fn coordinate_to_bar_index(&self, x: f64) -> Option<usize>;

    // =========================================================================
    // Bar Data Access
    // =========================================================================

    /// Get bars in a logical range
    ///
    /// # Arguments
    /// * `from` - Start logical index
    /// * `to` - End logical index (exclusive)
    ///
    /// # Returns
    /// Vector of bar data in the range
    fn bars_in_logical_range(&self, from: usize, to: usize) -> Vec<BarData>;

    /// Get the number of bars in the series
    fn bar_count(&self) -> usize;

    /// Get the first visible bar index
    fn first_visible_bar(&self) -> Option<usize>;

    /// Get the last visible bar index
    fn last_visible_bar(&self) -> Option<usize>;

    // =========================================================================
    // Pane Movement (Multi-Pane Support)
    // =========================================================================

    /// Move this series to a different pane
    ///
    /// # Arguments
    /// * `pane_index` - Target pane index (0-based)
    ///
    /// # Returns
    /// Ok(()) on success, Err if pane doesn't exist
    fn move_to_pane(&mut self, pane_index: usize) -> Result<(), String>;

    /// Merge this series with another pane
    ///
    /// This overlays the series on top of another pane.
    ///
    /// # Arguments
    /// * `pane_index` - Target pane index to merge with
    fn merge_with_pane(&mut self, pane_index: usize) -> Result<(), String>;

    /// Detach this series from its current pane
    ///
    /// Creates a new pane with just this series.
    ///
    /// # Returns
    /// The new pane index
    fn detach_pane(&mut self) -> usize;

    /// Get the current pane index
    fn current_pane(&self) -> usize;

    // =========================================================================
    // Series Options
    // =========================================================================

    /// Apply options to the series
    ///
    /// # Arguments
    /// * `options` - Series options to apply
    fn apply_options(&mut self, options: SeriesOptions);

    /// Get the current series options
    fn options(&self) -> SeriesOptions;

    /// Set series visibility
    fn set_visible(&mut self, visible: bool);

    /// Check if series is visible
    fn is_visible(&self) -> bool;

    /// Get the price scale associated with this series
    fn price_scale(&self) -> Box<dyn IPriceScaleApi>;
}

/// OHLCV bar data returned by [`ISeriesApi::bars_in_logical_range`].
#[derive(Debug, Clone)]
pub struct BarData {
    pub index: usize,
    pub timestamp: i64,
    pub open: f64,
    pub high: f64,
    pub low: f64,
    pub close: f64,
    pub volume: f64,
}

/// Configuration options for a chart series (colors, widths, visibility).
#[derive(Debug, Clone, Default)]
pub struct SeriesOptions {
    /// Line color (for line series)
    pub line_color: Option<[u8; 4]>,
    /// Line width
    pub line_width: Option<f32>,
    /// Area top color (for area series)
    pub area_top_color: Option<[u8; 4]>,
    /// Area bottom color (for area series)
    pub area_bottom_color: Option<[u8; 4]>,
    /// Price line visibility
    pub price_line_visible: Option<bool>,
    /// Last value label visibility
    pub last_value_visible: Option<bool>,
    /// Title
    pub title: Option<String>,
}

/// Trait for programmatic control of a price (Y) axis scale.
pub trait IPriceScaleApi {
    /// Apply price scale options
    fn apply_options(&mut self, options: PriceScaleOptions);

    /// Get current price scale options
    fn options(&self) -> PriceScaleOptions;

    /// Get the width of the price scale in pixels
    fn width(&self) -> f32;

    /// Set the price scale mode (normal, log, percentage)
    fn set_mode(&mut self, mode: PriceScaleMode);

    /// Get current mode
    fn mode(&self) -> PriceScaleMode;
}

/// Configuration options for a price scale axis.
#[derive(Debug, Clone, Default)]
pub struct PriceScaleOptions {
    /// Auto scale
    pub auto_scale: Option<bool>,
    /// Mode
    pub mode: Option<PriceScaleMode>,
    /// Invert scale
    pub invert_scale: Option<bool>,
    /// Align labels
    pub align_labels: Option<bool>,
    /// Border visible
    pub border_visible: Option<bool>,
    /// Border color
    pub border_color: Option<[u8; 4]>,
}

/// Price scale arithmetic mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PriceScaleMode {
    /// Normal arithmetic scale
    #[default]
    Normal,
    /// Logarithmic scale
    Logarithmic,
    /// Percentage scale
    Percentage,
    /// Indexed to 100 scale
    IndexedTo100,
}

/// Trait for programmatic control of the time (X) axis scale.
pub trait ITimeScaleApi {
    /// Get visible logical range
    fn get_visible_logical_range(&self) -> LogicalRange;

    /// Set visible logical range
    fn set_visible_logical_range(&mut self, range: LogicalRange);

    /// Get visible time range
    fn get_visible_range(&self) -> (i64, i64);

    /// Set visible time range
    fn set_visible_range(&mut self, from: i64, to: i64);

    /// Fit content to view
    fn fit_content(&mut self);

    /// Scroll to real-time (right edge)
    fn scroll_to_real_time(&mut self);

    /// Reset time scale to default
    fn reset_time_scale(&mut self);

    /// Subscribe to visible time range changes
    fn subscribe_visible_time_range_change(&mut self, callback: Box<dyn Fn(i64, i64)>);

    /// Unsubscribe from visible time range changes
    fn unsubscribe_visible_time_range_change(&mut self);

    /// Subscribe to visible logical range changes
    fn subscribe_visible_logical_range_change(&mut self, callback: Box<dyn Fn(LogicalRange)>);

    /// Unsubscribe from visible logical range changes
    fn unsubscribe_visible_logical_range_change(&mut self);
}

/// A range expressed in logical bar indices (fractional).
#[derive(Debug, Clone, Copy)]
pub struct LogicalRange {
    pub from: f64,
    pub to: f64,
}

impl LogicalRange {
    pub fn new(from: f64, to: f64) -> Self {
        Self { from, to }
    }

    pub fn length(&self) -> f64 {
        self.to - self.from
    }
}

/// Trait for programmatic control of a study (technical indicator).
pub trait IStudyApi {
    /// Get study ID
    fn study_id(&self) -> String;

    /// Apply study options
    fn apply_options(&mut self, options: StudyOptions);

    /// Get study options
    fn options(&self) -> StudyOptions;

    /// Set study visibility
    fn set_visible(&mut self, visible: bool);

    /// Check if study is visible
    fn is_visible(&self) -> bool;

    /// Move study to a different pane
    fn move_to_pane(&mut self, pane_index: usize) -> Result<(), String>;

    /// Merge study with a pane (overlay)
    fn merge_with_pane(&mut self, pane_index: usize) -> Result<(), String>;

    /// Detach study to its own pane
    fn detach_pane(&mut self) -> usize;

    /// Remove study
    fn remove(&mut self);
}

/// Configuration options for a study (inputs, styles, visibility, pane).
#[derive(Debug, Clone, Default)]
pub struct StudyOptions {
    /// Study visible
    pub visible: Option<bool>,
    /// Pane index
    pub pane_index: Option<usize>,
    /// Input values
    pub inputs: Option<std::collections::HashMap<String, serde_json::Value>>,
    /// Style overrides
    pub styles: Option<std::collections::HashMap<String, serde_json::Value>>,
}

/// Default implementation of [`ISeriesApi`] for the main chart series.
pub struct SeriesApiImpl {
    chart_rect: Rect,
    price_range: (f64, f64),
    bar_count: usize,
    visible_range: (usize, usize),
    current_pane: usize,
    options: SeriesOptions,
    visible: bool,
}

impl SeriesApiImpl {
    pub fn new(chart_rect: Rect, price_range: (f64, f64), bar_count: usize) -> Self {
        Self {
            chart_rect,
            price_range,
            bar_count,
            visible_range: (0, bar_count),
            current_pane: 0,
            options: SeriesOptions::default(),
            visible: true,
        }
    }

    fn price_to_y(&self, price: f64) -> f64 {
        let (min_price, max_price) = self.price_range;
        if max_price == min_price {
            return self.chart_rect.center().y as f64;
        }
        let price_ratio = (price - min_price) / (max_price - min_price);

        self.chart_rect.max.y as f64 - price_ratio * self.chart_rect.height() as f64
    }

    fn y_to_price(&self, y: f64) -> f64 {
        let (min_price, max_price) = self.price_range;
        let y_ratio = (self.chart_rect.max.y as f64 - y) / self.chart_rect.height() as f64;
        min_price + y_ratio * (max_price - min_price)
    }
}

impl ISeriesApi for SeriesApiImpl {
    fn series_id(&self) -> String {
        "main".to_string()
    }

    fn symbol(&self) -> String {
        "SYMBOL".to_string()
    }

    fn current_price(&self) -> Option<f64> {
        None
    }

    fn price_range(&self) -> (f64, f64) {
        self.price_range
    }

    fn price_to_coordinate(&self, price: f64) -> Option<f64> {
        if price < self.price_range.0 || price > self.price_range.1 {
            return None;
        }
        Some(self.price_to_y(price))
    }

    fn coordinate_to_price(&self, y: f64) -> Option<f64> {
        if y < self.chart_rect.min.y as f64 || y > self.chart_rect.max.y as f64 {
            return None;
        }
        Some(self.y_to_price(y))
    }

    fn bar_index_to_coordinate(&self, bar_index: usize) -> Option<f64> {
        if bar_index >= self.bar_count {
            return None;
        }
        let bar_width =
            self.chart_rect.width() as f64 / (self.visible_range.1 - self.visible_range.0) as f64;
        let x = self.chart_rect.min.x as f64
            + (bar_index - self.visible_range.0) as f64 * bar_width
            + bar_width / 2.0;
        Some(x)
    }

    fn coordinate_to_bar_index(&self, x: f64) -> Option<usize> {
        if x < self.chart_rect.min.x as f64 || x > self.chart_rect.max.x as f64 {
            return None;
        }
        let bar_width =
            self.chart_rect.width() as f64 / (self.visible_range.1 - self.visible_range.0) as f64;
        let bar_index =
            ((x - self.chart_rect.min.x as f64) / bar_width) as usize + self.visible_range.0;
        if bar_index >= self.bar_count {
            return None;
        }
        Some(bar_index)
    }

    /// Returns an empty vec by default. Override in your DataSource implementation.
    fn bars_in_logical_range(&self, _from: usize, _to: usize) -> Vec<BarData> {
        Vec::new()
    }

    fn bar_count(&self) -> usize {
        self.bar_count
    }

    fn first_visible_bar(&self) -> Option<usize> {
        Some(self.visible_range.0)
    }

    fn last_visible_bar(&self) -> Option<usize> {
        Some(self.visible_range.1.min(self.bar_count.saturating_sub(1)))
    }

    fn move_to_pane(&mut self, pane_index: usize) -> Result<(), String> {
        self.current_pane = pane_index;
        Ok(())
    }

    fn merge_with_pane(&mut self, pane_index: usize) -> Result<(), String> {
        log::info!("Merging series with pane {pane_index}");
        Ok(())
    }

    fn detach_pane(&mut self) -> usize {
        let new_pane = self.current_pane + 1;
        self.current_pane = new_pane;
        new_pane
    }

    fn current_pane(&self) -> usize {
        self.current_pane
    }

    fn apply_options(&mut self, options: SeriesOptions) {
        self.options = options;
    }

    fn options(&self) -> SeriesOptions {
        self.options.clone()
    }

    fn set_visible(&mut self, visible: bool) {
        self.visible = visible;
    }

    fn is_visible(&self) -> bool {
        self.visible
    }

    fn price_scale(&self) -> Box<dyn IPriceScaleApi> {
        Box::new(PriceScaleApiImpl::default())
    }
}

/// Default implementation of [`IPriceScaleApi`].
#[derive(Default)]
pub struct PriceScaleApiImpl {
    options: PriceScaleOptions,
}

impl IPriceScaleApi for PriceScaleApiImpl {
    fn apply_options(&mut self, options: PriceScaleOptions) {
        self.options = options;
    }

    fn options(&self) -> PriceScaleOptions {
        self.options.clone()
    }

    fn width(&self) -> f32 {
        60.0
    }

    fn set_mode(&mut self, mode: PriceScaleMode) {
        self.options.mode = Some(mode);
    }

    fn mode(&self) -> PriceScaleMode {
        self.options.mode.unwrap_or(PriceScaleMode::Normal)
    }
}