use egui::Rect;
pub trait ISeriesApi {
fn series_id(&self) -> String;
fn symbol(&self) -> String;
fn current_price(&self) -> Option<f64>;
fn price_range(&self) -> (f64, f64);
fn price_to_coordinate(&self, price: f64) -> Option<f64>;
fn coordinate_to_price(&self, y: f64) -> Option<f64>;
fn bar_index_to_coordinate(&self, bar_index: usize) -> Option<f64>;
fn coordinate_to_bar_index(&self, x: f64) -> Option<usize>;
fn bars_in_logical_range(&self, from: usize, to: usize) -> Vec<BarData>;
fn bar_count(&self) -> usize;
fn first_visible_bar(&self) -> Option<usize>;
fn last_visible_bar(&self) -> Option<usize>;
fn move_to_pane(&mut self, pane_index: usize) -> Result<(), String>;
fn merge_with_pane(&mut self, pane_index: usize) -> Result<(), String>;
fn detach_pane(&mut self) -> usize;
fn current_pane(&self) -> usize;
fn apply_options(&mut self, options: SeriesOptions);
fn options(&self) -> SeriesOptions;
fn set_visible(&mut self, visible: bool);
fn is_visible(&self) -> bool;
fn price_scale(&self) -> Box<dyn IPriceScaleApi>;
}
#[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,
}
#[derive(Debug, Clone, Default)]
pub struct SeriesOptions {
pub line_color: Option<[u8; 4]>,
pub line_width: Option<f32>,
pub area_top_color: Option<[u8; 4]>,
pub area_bottom_color: Option<[u8; 4]>,
pub price_line_visible: Option<bool>,
pub last_value_visible: Option<bool>,
pub title: Option<String>,
}
pub trait IPriceScaleApi {
fn apply_options(&mut self, options: PriceScaleOptions);
fn options(&self) -> PriceScaleOptions;
fn width(&self) -> f32;
fn set_mode(&mut self, mode: PriceScaleMode);
fn mode(&self) -> PriceScaleMode;
}
#[derive(Debug, Clone, Default)]
pub struct PriceScaleOptions {
pub auto_scale: Option<bool>,
pub mode: Option<PriceScaleMode>,
pub invert_scale: Option<bool>,
pub align_labels: Option<bool>,
pub border_visible: Option<bool>,
pub border_color: Option<[u8; 4]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PriceScaleMode {
#[default]
Normal,
Logarithmic,
Percentage,
IndexedTo100,
}
pub trait ITimeScaleApi {
fn get_visible_logical_range(&self) -> LogicalRange;
fn set_visible_logical_range(&mut self, range: LogicalRange);
fn get_visible_range(&self) -> (i64, i64);
fn set_visible_range(&mut self, from: i64, to: i64);
fn fit_content(&mut self);
fn scroll_to_real_time(&mut self);
fn reset_time_scale(&mut self);
fn subscribe_visible_time_range_change(&mut self, callback: Box<dyn Fn(i64, i64)>);
fn unsubscribe_visible_time_range_change(&mut self);
fn subscribe_visible_logical_range_change(&mut self, callback: Box<dyn Fn(LogicalRange)>);
fn unsubscribe_visible_logical_range_change(&mut self);
}
#[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
}
}
pub trait IStudyApi {
fn study_id(&self) -> String;
fn apply_options(&mut self, options: StudyOptions);
fn options(&self) -> StudyOptions;
fn set_visible(&mut self, visible: bool);
fn is_visible(&self) -> bool;
fn move_to_pane(&mut self, pane_index: usize) -> Result<(), String>;
fn merge_with_pane(&mut self, pane_index: usize) -> Result<(), String>;
fn detach_pane(&mut self) -> usize;
fn remove(&mut self);
}
#[derive(Debug, Clone, Default)]
pub struct StudyOptions {
pub visible: Option<bool>,
pub pane_index: Option<usize>,
pub inputs: Option<std::collections::HashMap<String, serde_json::Value>>,
pub styles: Option<std::collections::HashMap<String, serde_json::Value>>,
}
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)
}
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())
}
}
#[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)
}
}