Răsfoiți Sursa

trait based chart composition pattern (#10)

* initial commit

* fix unnecessary render triggers

* chore: clear main cache in render loop

* chore: renamed chart instances to avoid confusion

* reusable axis calculations as composition

* chore: rename chart instance name

* deleted:    src/charts/candlesticks.rs

* carry the components that  can be reusable to super

* feat reusable component across charts

* feat: reusable x axis labels across charts

* carry over x abis labeling to super

* carry over the y axis labeling to super

* feat: reusable y axis labels across charts

* carry over the y-axis calc. to super

* workaround to also get the heatmap's x-axis labels

* carry over x-axis label calc. to super

* chore: getting the x-axis labels to work

* chores and code cleanup

* used more efficient iterator while calculating axis range

* fix y-axis acting weird on the new candle open's first render loop
Berke 1 an în urmă
părinte
comite
f98282aa66
6 a modificat fișierele cu 1439 adăugiri și 2144 ștergeri
  1. 506 2
      src/charts.rs
  2. 570 0
      src/charts/candlestick.rs
  3. 0 973
      src/charts/custom_line.rs
  4. 155 541
      src/charts/footprint.rs
  5. 144 563
      src/charts/heatmap.rs
  6. 64 65
      src/main.rs

+ 506 - 2
src/charts.rs

@@ -1,3 +1,507 @@
-pub mod custom_line;
+use chrono::NaiveDateTime;
+use iced::{
+    widget::{canvas::Cache, button}, Border, Color, Point, Rectangle, Theme, Vector
+};
+use iced::{
+    mouse, widget::canvas, widget::canvas::{event::{self, Event}, stroke::Stroke, Geometry, Path}, Renderer, Size
+};
+
 pub mod heatmap;
-pub mod footprint;
+pub mod footprint;
+pub mod candlestick;
+
+#[derive(Debug, Clone, Copy)]
+pub enum Message {
+    Translated(Vector),
+    Scaled(f32, Option<Vector>),
+    ChartBounds(Rectangle),
+    AutoscaleToggle,
+    CrosshairToggle,
+    CrosshairMoved(Point),
+    YScaling(f32),
+}
+struct CommonChartData {
+    main_cache: Cache,
+
+    mesh_cache: Cache,
+
+    crosshair_cache: Cache,
+    crosshair: bool,
+    crosshair_position: Point,
+
+    x_crosshair_cache: Cache,
+    x_labels_cache: Cache,
+    x_min_time: i64,
+    x_max_time: i64,
+
+    y_crosshair_cache: Cache,
+    y_labels_cache: Cache,
+    y_min_price: f32,
+    y_max_price: f32,
+
+    translation: Vector,
+    scaling: f32,
+    autoscale: bool,
+
+    bounds: Rectangle,
+}
+impl Default for CommonChartData {
+    fn default() -> Self {
+        CommonChartData {
+            main_cache: Cache::default(),
+
+            mesh_cache: Cache::default(),
+
+            crosshair: false,
+            crosshair_cache: Cache::default(),
+            crosshair_position: Point::new(0.0, 0.0),
+
+            x_crosshair_cache: Cache::default(),
+            x_labels_cache: Cache::default(),
+            x_min_time: 0,
+            x_max_time: 0,
+
+            y_crosshair_cache: Cache::default(),
+            y_labels_cache: Cache::default(),
+            y_min_price: 0.0,
+            y_max_price: 0.0,
+
+            translation: Vector::default(),
+            scaling: 1.0,
+            autoscale: true,
+
+            bounds: Rectangle::default(),
+        }
+    }
+}
+
+trait Chart {
+    type DataPoint;
+
+    fn get_common_data(&self) -> &CommonChartData;
+    fn get_common_data_mut(&mut self) -> &mut CommonChartData;
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum Interaction {
+    None,
+    Drawing,
+    Erasing,
+    Panning { translation: Vector, start: Point },
+}
+impl Default for Interaction {
+    fn default() -> Self {
+        Self::None
+    }
+}
+
+fn chart_button(_theme: &Theme, _status: button::Status, is_active: bool) -> button::Style {
+    button::Style {
+        background: Some(Color::from_rgba8(20, 20, 20, 1.0).into()),
+        border: Border {
+            color: {
+                if is_active {
+                    Color::from_rgba8(50, 50, 50, 1.0)
+                } else {
+                    Color::from_rgba8(20, 20, 20, 1.0)
+                }
+            },
+            width: 1.0,
+            radius: 2.0.into(),
+        },
+        text_color: Color::WHITE,
+        ..button::Style::default()
+    }
+}
+
+// price steps, to be used for y-axis labels on all charts
+const PRICE_STEPS: [f32; 15] = [
+    1000.0,
+    500.0,
+    200.0,
+    100.0,
+    50.0,
+    20.0,
+    10.0,
+    5.0,
+    2.0,
+    1.0,
+    0.5,
+    0.2,
+    0.1,
+    0.05,
+    0.01,
+];
+fn calculate_price_step(highest: f32, lowest: f32, labels_can_fit: i32) -> (f32, f32) {
+    let range = highest - lowest;
+    let mut step = 1000.0; 
+
+    for &s in PRICE_STEPS.iter().rev() {
+        if range / s <= labels_can_fit as f32 {
+            step = s;
+            break;
+        }
+    }
+    let rounded_lowest = (lowest / step).floor() * step;
+
+    (step, rounded_lowest)
+}
+
+// time steps in ms, to be used for x-axis labels on candlesticks and footprint charts
+const M1_TIME_STEPS: [i64; 9] = [
+    1000 * 60 * 720, // 12 hour
+    1000 * 60 * 180, // 3 hour
+    1000 * 60 * 60, // 1 hour
+    1000 * 60 * 30, // 30 min
+    1000 * 60 * 15, // 15 min
+    1000 * 60 * 10, // 10 min
+    1000 * 60 * 5, // 5 min
+    1000 * 60 * 2, // 2 min
+    60 * 1000, // 1 min
+];
+const M3_TIME_STEPS: [i64; 9] = [
+    1000 * 60 * 1440, // 24 hour
+    1000 * 60 * 720, // 12 hour
+    1000 * 60 * 180, // 6 hour
+    1000 * 60 * 120, // 2 hour
+    1000 * 60 * 60, // 1 hour
+    1000 * 60 * 30, // 30 min
+    1000 * 60 * 15, // 15 min
+    1000 * 60 * 9, // 9 min
+    1000 * 60 * 3, // 3 min
+];
+const M5_TIME_STEPS: [i64; 9] = [
+    1000 * 60 * 1440, // 24 hour
+    1000 * 60 * 720, // 12 hour
+    1000 * 60 * 480, // 8 hour
+    1000 * 60 * 240, // 4 hour
+    1000 * 60 * 120, // 2 hour
+    1000 * 60 * 60, // 1 hour
+    1000 * 60 * 30, // 30 min
+    1000 * 60 * 15, // 15 min
+    1000 * 60 * 5, // 5 min
+];
+
+// time steps in ms, to be used for x-axis labels on heatmap chart
+const TIME_STEPS: [i64; 8] = [
+    60 * 1000, // 1 minute
+    30 * 1000, // 30 seconds
+    15 * 1000, // 15 seconds
+    10 * 1000, // 10 seconds
+    5 * 1000,  // 5 seconds
+    2 * 1000,  // 2 seconds
+    1000,  // 1 second
+    500,       // 500 milliseconds
+];
+
+fn calculate_time_step(earliest: i64, latest: i64, labels_can_fit: i32, timeframe: Option<u16>) -> (i64, i64) {
+    let duration = latest - earliest;
+
+    if let Some(timeframe) = timeframe {
+        let time_steps = match timeframe {
+            1 => &M1_TIME_STEPS,
+            3 => &M3_TIME_STEPS,
+            5 => &M5_TIME_STEPS,
+            15 => &M5_TIME_STEPS[..7],
+            30 => &M5_TIME_STEPS[..6],
+            _ => &M1_TIME_STEPS,
+        };
+
+        let mut selected_step = time_steps[0];
+        for &step in time_steps.iter() {
+            if duration / step >= labels_can_fit as i64 {
+                selected_step = step;
+                break;
+            }
+            if step <= duration {
+                selected_step = step;
+            }
+        }
+
+        let rounded_earliest = (earliest / selected_step) * selected_step;
+
+        (selected_step, rounded_earliest)
+
+    } else {
+        let mut selected_step = TIME_STEPS[0];
+        for &step in &TIME_STEPS {
+            if duration / step >= labels_can_fit as i64 {
+                selected_step = step;
+                break;
+            }
+        }
+
+        let rounded_earliest = (earliest / selected_step) * selected_step;
+
+        (selected_step, rounded_earliest)
+    }
+}
+
+pub struct AxisLabelXCanvas<'a> {
+    labels_cache: &'a Cache,
+    crosshair_cache: &'a Cache,
+    crosshair_position: Point,
+    crosshair: bool,
+    min: i64,
+    max: i64,
+    timeframe: Option<u16>,
+}
+impl canvas::Program<Message> for AxisLabelXCanvas<'_> {
+    type State = Interaction;
+
+    fn update(
+        &self,
+        _interaction: &mut Interaction,
+        _event: Event,
+        _bounds: Rectangle,
+        _cursor: mouse::Cursor,
+    ) -> (event::Status, Option<Message>) {
+        (event::Status::Ignored, None)
+    }
+    
+    fn draw(
+        &self,
+        _state: &Self::State,
+        renderer: &Renderer,
+        _theme: &Theme,
+        bounds: Rectangle,
+        _cursor: mouse::Cursor,
+    ) -> Vec<Geometry> {
+        if self.max == 0 {
+            return vec![];
+        }
+        let latest_in_millis = self.max; 
+        let earliest_in_millis = self.min; 
+
+        let x_labels_can_fit = (bounds.width / 120.0) as i32;
+
+        let (time_step, rounded_earliest) = calculate_time_step(earliest_in_millis, latest_in_millis, x_labels_can_fit, self.timeframe);
+        
+        let labels = self.labels_cache.draw(renderer, bounds.size(), |frame| {
+            frame.with_save(|frame| {
+                let mut time: i64 = rounded_earliest;
+
+                while time <= latest_in_millis {                    
+                    let x_position = ((time - earliest_in_millis) as f64 / (latest_in_millis - earliest_in_millis) as f64) * bounds.width as f64;
+
+                    if x_position >= 0.0 && x_position <= bounds.width as f64 {
+                        let text_size = 12.0;
+                        let time_as_datetime = NaiveDateTime::from_timestamp(time / 1000, 0);
+                        
+                        let time_format: &str;
+                        if let Some(_) = self.timeframe {
+                            time_format = "%H:%M";
+                        } else {
+                            time_format = "%M:%S";
+                        }
+
+                        let label = canvas::Text {
+                            content: time_as_datetime.format(time_format).to_string(),
+                            position: Point::new(x_position as f32 - (text_size*4.0/3.0), bounds.height - 20.0),
+                            size: iced::Pixels(text_size),
+                            color: Color::from_rgba8(200, 200, 200, 1.0),
+                            ..canvas::Text::default()
+                        };  
+
+                        label.draw_with(|path, color| {
+                            frame.fill(&path, color);
+                        });
+                    }
+                    
+                    time += time_step;
+                }
+
+                let line = Path::line(
+                    Point::new(0.0, bounds.height - 30.0), 
+                    Point::new(bounds.width, bounds.height - 30.0)
+                );
+                frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(81, 81, 81, 0.2)).with_width(1.0));
+            });
+        });
+        
+        let crosshair = self.crosshair_cache.draw(renderer, bounds.size(), |frame| {
+            if self.crosshair && self.crosshair_position.x > 0.0 {
+                let crosshair_ratio = self.crosshair_position.x as f64 / bounds.width as f64;
+                let crosshair_millis = earliest_in_millis as f64 + crosshair_ratio * (latest_in_millis - earliest_in_millis) as f64;
+        
+                let (snap_ratio, text_content) = if let Some(timeframe) = self.timeframe {
+                    let crosshair_time = NaiveDateTime::from_timestamp((crosshair_millis / 1000.0) as i64, 0);
+                    let crosshair_timestamp = crosshair_time.timestamp();
+                    let rounded_timestamp = (crosshair_timestamp as f64 / (timeframe as f64 * 60.0)).round() as i64 * timeframe as i64 * 60;
+                    let rounded_time = NaiveDateTime::from_timestamp(rounded_timestamp, 0);
+        
+                    let snap_ratio = (rounded_timestamp as f64 * 1000.0 - earliest_in_millis as f64) / (latest_in_millis as f64 - earliest_in_millis as f64);
+                    (snap_ratio, rounded_time.format("%H:%M").to_string())
+                } else {
+                    let crosshair_millis = (crosshair_millis / 100.0).round() * 100.0;
+                    let crosshair_time = NaiveDateTime::from_timestamp((crosshair_millis / 1000.0).floor() as i64, ((crosshair_millis % 1000.0) * 1_000_000.0).round() as u32);
+                    let crosshair_timestamp = crosshair_time.timestamp_millis();
+        
+                    let snap_ratio = (crosshair_timestamp as f64 - earliest_in_millis as f64) / (latest_in_millis as f64 - earliest_in_millis as f64);
+                    (snap_ratio, crosshair_time.format("%M:%S:%3f").to_string().replace('.', ""))
+                };
+        
+                let snap_x = snap_ratio * bounds.width as f64;
+        
+                let text_size = 12.0;
+                let growth_amount = 6.0;
+                let (rectangle_position, text_position) = if self.timeframe.is_some() {
+                    (Point::new(snap_x as f32 - 14.0 - growth_amount, bounds.height - 20.0),
+                     Point::new(snap_x as f32 - 14.0, bounds.height - 20.0))
+                } else {
+                    (Point::new(snap_x as f32 - 26.0 - growth_amount, bounds.height - 20.0),
+                     Point::new(snap_x as f32 - 26.0, bounds.height - 20.0))
+                };
+        
+                let text_background = canvas::Path::rectangle(rectangle_position, Size::new(text_content.len() as f32 * text_size/2.0 + 2.0 * growth_amount + 1.0, text_size + text_size/2.0));
+                frame.fill(&text_background, Color::from_rgba8(200, 200, 200, 1.0));
+        
+                let crosshair_label = canvas::Text {
+                    content: text_content,
+                    position: text_position,
+                    size: iced::Pixels(text_size),
+                    color: Color::from_rgba8(0, 0, 0, 1.0),
+                    ..canvas::Text::default()
+                };
+        
+                crosshair_label.draw_with(|path, color| {
+                    frame.fill(&path, color);
+                });
+            }
+        });
+
+        vec![labels, crosshair]
+    }
+
+    fn mouse_interaction(
+        &self,
+        interaction: &Interaction,
+        bounds: Rectangle,
+        cursor: mouse::Cursor,
+    ) -> mouse::Interaction {
+        match interaction {
+            Interaction::Drawing => mouse::Interaction::Crosshair,
+            Interaction::Erasing => mouse::Interaction::Crosshair,
+            Interaction::Panning { .. } => mouse::Interaction::ResizingHorizontally,
+            Interaction::None if cursor.is_over(bounds) => {
+                mouse::Interaction::ResizingHorizontally
+            }
+            Interaction::None => mouse::Interaction::default(),
+        }
+    }
+}
+
+pub struct AxisLabelYCanvas<'a> {
+    labels_cache: &'a Cache,
+    y_croshair_cache: &'a Cache,
+    min: f32,
+    max: f32,
+    crosshair_position: Point,
+    crosshair: bool,
+}
+impl canvas::Program<Message> for AxisLabelYCanvas<'_> {
+    type State = Interaction;
+
+    fn update(
+        &self,
+        _interaction: &mut Interaction,
+        _event: Event,
+        _bounds: Rectangle,
+        _cursor: mouse::Cursor,
+    ) -> (event::Status, Option<Message>) {
+        (event::Status::Ignored, None)
+    }
+    
+    fn draw(
+        &self,
+        _state: &Self::State,
+        renderer: &Renderer,
+        _theme: &Theme,
+        bounds: Rectangle,
+        _cursor: mouse::Cursor,
+    ) -> Vec<Geometry> {
+        if self.max == 0.0 {
+            return vec![];
+        }
+
+        let y_labels_can_fit = (bounds.height / 32.0) as i32;
+        let (step, rounded_lowest) = calculate_price_step(self.max, self.min, y_labels_can_fit);
+
+        let volume_area_height = bounds.height / 8.0; 
+        let candlesticks_area_height = bounds.height - volume_area_height;
+
+        let labels = self.labels_cache.draw(renderer, bounds.size(), |frame| {
+            frame.with_save(|frame| {
+                let y_range = self.max - self.min;
+                let mut y = rounded_lowest;
+
+                while y <= self.max {
+                    let y_position = candlesticks_area_height - ((y - self.min) / y_range * candlesticks_area_height);
+
+                    let text_size = 12.0;
+                    let decimal_places = if step < 0.5 { 2 } else { usize::from(step < 1.0) };
+                    let label_content = format!("{y:.decimal_places$}");
+                    let label = canvas::Text {
+                        content: label_content,
+                        position: Point::new(10.0, y_position - text_size / 2.0),
+                        size: iced::Pixels(text_size),
+                        color: Color::from_rgba8(200, 200, 200, 1.0),
+                        ..canvas::Text::default()
+                    };  
+
+                    label.draw_with(|path, color| {
+                        frame.fill(&path, color);
+                    });
+
+                    y += step;
+                }
+            });
+        });
+        let crosshair = self.y_croshair_cache.draw(renderer, bounds.size(), |frame| {
+            if self.crosshair && self.crosshair_position.y > 0.0 {
+                let text_size = 12.0;
+                let y_range = self.max - self.min;
+                let decimal_places = if step < 1.0 { 2 } else { 1 };
+                let label_content = format!("{:.*}", decimal_places, self.min + (y_range * (candlesticks_area_height - self.crosshair_position.y) / candlesticks_area_height));
+                
+                let growth_amount = 3.0; 
+                let rectangle_position = Point::new(8.0 - growth_amount, self.crosshair_position.y - text_size / 2.0 - 3.0);
+                let text_position = Point::new(8.0, self.crosshair_position.y - text_size / 2.0 - 3.0);
+
+                let text_background = canvas::Path::rectangle(rectangle_position, Size::new(label_content.len() as f32 * text_size / 2.0 + 2.0 * growth_amount + 4.0, text_size + text_size / 1.8));
+                frame.fill(&text_background, Color::from_rgba8(200, 200, 200, 1.0));
+
+                let label = canvas::Text {
+                    content: label_content,
+                    position: text_position,
+                    size: iced::Pixels(text_size),
+                    color: Color::from_rgba8(0, 0, 0, 1.0),
+                    ..canvas::Text::default()
+                };
+
+                label.draw_with(|path, color| {
+                    frame.fill(&path, color);
+                });
+            }
+        });
+
+        vec![labels, crosshair]
+    }
+
+    fn mouse_interaction(
+        &self,
+        interaction: &Interaction,
+        bounds: Rectangle,
+        cursor: mouse::Cursor,
+    ) -> mouse::Interaction {
+        match interaction {
+            Interaction::Drawing => mouse::Interaction::Crosshair,
+            Interaction::Erasing => mouse::Interaction::Crosshair,
+            Interaction::Panning { .. } => mouse::Interaction::ResizingVertically,
+            Interaction::None if cursor.is_over(bounds) => {
+                mouse::Interaction::ResizingVertically
+            }
+            Interaction::None => mouse::Interaction::default(),
+        }
+    }
+}

+ 570 - 0
src/charts/candlestick.rs

@@ -0,0 +1,570 @@
+use std::collections::BTreeMap;
+use iced::{
+    alignment, mouse, widget::{button, canvas::{self, event::{self, Event}, stroke::Stroke, Cache, Canvas, Geometry, Path}}, Color, Element, Length, Point, Rectangle, Renderer, Size, Theme
+};
+use iced::widget::{Column, Row, Container, Text};
+use crate::{market_data::Kline, Timeframe};
+
+use super::{Chart, CommonChartData, Message, Interaction, AxisLabelXCanvas, AxisLabelYCanvas};
+use super::{chart_button, calculate_price_step, calculate_time_step};
+
+pub struct CandlestickChart {
+    chart: CommonChartData,
+    data_points: BTreeMap<i64, (f32, f32, f32, f32, f32, f32)>,
+    timeframe: u16,
+}
+
+impl Chart for CandlestickChart {
+    type DataPoint = BTreeMap<i64, (f32, f32, f32, f32, f32, f32)>;
+
+    fn get_common_data(&self) -> &CommonChartData {
+        &self.chart
+    }
+    fn get_common_data_mut(&mut self) -> &mut CommonChartData {
+        &mut self.chart
+    }
+}
+
+impl CandlestickChart {
+    const MIN_SCALING: f32 = 0.1;
+    const MAX_SCALING: f32 = 2.0;
+
+    pub fn new(klines: Vec<Kline>, timeframe: Timeframe) -> CandlestickChart {
+        let mut klines_raw = BTreeMap::new();
+
+        for kline in klines {
+            let buy_volume = kline.taker_buy_base_asset_volume;
+            let sell_volume = kline.volume - buy_volume;
+            klines_raw.insert(kline.time as i64, (kline.open, kline.high, kline.low, kline.close, buy_volume, sell_volume));
+        }
+
+        let timeframe = match timeframe {
+            Timeframe::M1 => 1,
+            Timeframe::M3 => 3,
+            Timeframe::M5 => 5,
+            Timeframe::M15 => 15,
+            Timeframe::M30 => 30,
+        };
+
+        CandlestickChart {
+            chart: CommonChartData::default(),
+            data_points: klines_raw,
+            timeframe,
+        }
+    }
+
+    pub fn insert_datapoint(&mut self, kline: &Kline) {
+        let buy_volume: f32 = kline.taker_buy_base_asset_volume;
+        let sell_volume: f32 = if buy_volume != -1.0 {
+            kline.volume - buy_volume
+        } else {
+            kline.volume
+        };
+
+        self.data_points.insert(kline.time as i64, (kline.open, kline.high, kline.low, kline.close, buy_volume, sell_volume));
+
+        self.render_start();
+    }
+
+    pub fn render_start(&mut self) {
+        let (latest, earliest, highest, lowest) = self.calculate_range();
+
+        if latest == 0 || highest == 0.0 {
+            return;
+        }
+
+        let chart_state = self.get_common_data_mut();
+
+        if earliest != chart_state.x_min_time || latest != chart_state.x_max_time || lowest != chart_state.y_min_price || highest != chart_state.y_max_price {
+            chart_state.x_labels_cache.clear();
+            chart_state.mesh_cache.clear();
+        }
+
+        chart_state.x_min_time = earliest;
+        chart_state.x_max_time = latest;
+        chart_state.y_min_price = lowest;
+        chart_state.y_max_price = highest;
+
+        chart_state.y_labels_cache.clear();
+        chart_state.crosshair_cache.clear();
+
+        chart_state.main_cache.clear();
+    }
+
+    fn calculate_range(&self) -> (i64, i64, f32, f32) {
+        let chart = self.get_common_data();
+    
+        let latest: i64 = self.data_points.keys().last().map_or(0, |time| time - ((chart.translation.x*10000.0)*(self.timeframe as f32)) as i64);
+        let earliest: i64 = latest - ((6400000.0*self.timeframe as f32) / (chart.scaling / (chart.bounds.width/800.0))) as i64;
+    
+        let visible_klines = self.data_points.range(earliest..=latest);
+    
+        let (highest, lowest, avg_body_height, count) = visible_klines.fold((f32::MIN, f32::MAX, 0.0f32, 0), |(highest, lowest, total_body_height, count), (_, kline)| {
+            let body_height = (kline.0 - kline.3).abs();
+            (
+                highest.max(kline.1),
+                lowest.min(kline.2),
+                total_body_height + body_height,
+                count + 1,
+            )
+        });
+    
+        if count <= 1 {
+            return (0, 0, 0.0, 0.0);
+        }
+    
+        let avg_body_height = if count > 1 { avg_body_height / (count - 1) as f32 } else { 0.0 };
+        let (highest, lowest) = (highest + avg_body_height, lowest - avg_body_height);
+    
+        (latest, earliest, highest, lowest)
+    }
+
+    pub fn update(&mut self, message: &Message) {
+        match message {
+            Message::Translated(translation) => {
+                let chart = self.get_common_data_mut();
+
+                if chart.autoscale {
+                    chart.translation.x = translation.x;
+                } else {
+                    chart.translation = *translation;
+                }
+                chart.crosshair_position = Point::new(0.0, 0.0);
+
+                self.render_start();
+            },
+            Message::Scaled(scaling, translation) => {
+                let chart = self.get_common_data_mut();
+
+                chart.scaling = *scaling;
+                
+                if let Some(translation) = translation {
+                    if chart.autoscale {
+                        chart.translation.x = translation.x;
+                    } else {
+                        chart.translation = *translation;
+                    }
+                }
+                chart.crosshair_position = Point::new(0.0, 0.0);
+
+                self.render_start();
+            },
+            Message::ChartBounds(bounds) => {
+                self.chart.bounds = *bounds;
+            },
+            Message::AutoscaleToggle => {
+                self.chart.autoscale = !self.chart.autoscale;
+            },
+            Message::CrosshairToggle => {
+                self.chart.crosshair = !self.chart.crosshair;
+            },
+            Message::CrosshairMoved(position) => {
+                let chart = self.get_common_data_mut();
+
+                chart.crosshair_position = *position;
+                if chart.crosshair {
+                    chart.crosshair_cache.clear();
+                    chart.y_crosshair_cache.clear();
+                    chart.x_crosshair_cache.clear();
+                }
+            },
+            _ => {}
+        }
+    }
+
+    pub fn view(&self) -> Element<Message> {
+        let chart = Canvas::new(self)
+            .width(Length::FillPortion(10))
+            .height(Length::FillPortion(10));
+
+        let chart_state = self.get_common_data();
+    
+        let axis_labels_x = Canvas::new(
+            AxisLabelXCanvas { 
+                labels_cache: &chart_state.x_labels_cache, 
+                min: chart_state.x_min_time, 
+                max: chart_state.x_max_time, 
+                crosshair_cache: &chart_state.x_crosshair_cache, 
+                crosshair_position: chart_state.crosshair_position, 
+                crosshair: chart_state.crosshair,
+                timeframe: Some(self.timeframe)
+            })
+            .width(Length::FillPortion(10))
+            .height(Length::Fixed(26.0));
+    
+        let axis_labels_y = Canvas::new(
+            AxisLabelYCanvas { 
+                labels_cache: &chart_state.y_labels_cache, 
+                y_croshair_cache: &chart_state.y_crosshair_cache, 
+                min: chart_state.y_min_price,
+                max: chart_state.y_max_price,
+                crosshair_position: chart_state.crosshair_position, 
+                crosshair: chart_state.crosshair
+            })
+            .width(Length::Fixed(60.0))
+            .height(Length::FillPortion(10));
+
+        let autoscale_button = button(
+            Text::new("A")
+                .size(12)
+                .horizontal_alignment(alignment::Horizontal::Center)
+            )
+            .width(Length::Fill)
+            .height(Length::Fill)
+            .on_press(Message::AutoscaleToggle)
+            .style(|_theme: &Theme, _status: iced::widget::button::Status| chart_button(_theme, _status, chart_state.autoscale));
+        let crosshair_button = button(
+            Text::new("+")
+                .size(12)
+                .horizontal_alignment(alignment::Horizontal::Center)
+            ) 
+            .width(Length::Fill)
+            .height(Length::Fill)
+            .on_press(Message::CrosshairToggle)
+            .style(|_theme: &Theme, _status: iced::widget::button::Status| chart_button(_theme, _status, chart_state.crosshair));
+    
+        let chart_controls = Container::new(
+            Row::new()
+                .push(autoscale_button)
+                .push(crosshair_button).spacing(2)
+            ).padding([0, 2, 0, 2])
+            .width(Length::Fixed(60.0))
+            .height(Length::Fixed(26.0));
+
+        let chart_and_y_labels = Row::new()
+            .push(chart)
+            .push(axis_labels_y);
+    
+        let bottom_row = Row::new()
+            .push(axis_labels_x)
+            .push(chart_controls);
+    
+        let content = Column::new()
+            .push(chart_and_y_labels)
+            .push(bottom_row)
+            .spacing(0)
+            .padding(5);
+    
+        content.into()
+    }
+}
+
+impl canvas::Program<Message> for CandlestickChart {
+    type State = Interaction;
+
+    fn update(
+        &self,
+        interaction: &mut Interaction,
+        event: Event,
+        bounds: Rectangle,
+        cursor: mouse::Cursor,
+    ) -> (event::Status, Option<Message>) {  
+        let chart_state = self.get_common_data();
+
+        if bounds != chart_state.bounds {
+            return (event::Status::Ignored, Some(Message::ChartBounds(bounds)));
+        } 
+        
+        if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event {
+            *interaction = Interaction::None;
+        }
+
+        let Some(cursor_position) = cursor.position_in(bounds) else {
+            return (event::Status::Ignored, 
+                if chart_state.crosshair {
+                    Some(Message::CrosshairMoved(Point::new(0.0, 0.0)))
+                } else {
+                    None
+                }
+                );
+        };
+
+        match event {
+            Event::Mouse(mouse_event) => match mouse_event {
+                mouse::Event::ButtonPressed(button) => {
+                    let message = match button {
+                        mouse::Button::Right => {
+                            *interaction = Interaction::Drawing;
+                            None
+                        }
+                        mouse::Button::Left => {
+                            *interaction = Interaction::Panning {
+                                translation: chart_state.translation,
+                                start: cursor_position,
+                            };
+                            None
+                        }
+                        _ => None,
+                    };
+
+                    (event::Status::Captured, message)
+                }
+                mouse::Event::CursorMoved { .. } => {
+                    let message = match *interaction {
+                        Interaction::Drawing => None,
+                        Interaction::Erasing => None,
+                        Interaction::Panning { translation, start } => {
+                            Some(Message::Translated(
+                                translation
+                                    + (cursor_position - start)
+                                        * (1.0 / chart_state.scaling),
+                            ))
+                        }
+                        Interaction::None => 
+                            if chart_state.crosshair && cursor.is_over(bounds) {
+                                Some(Message::CrosshairMoved(cursor_position))
+                            } else {
+                                None
+                            },
+                    };
+
+                    let event_status = match interaction {
+                        Interaction::None => event::Status::Ignored,
+                        _ => event::Status::Captured,
+                    };
+
+                    (event_status, message)
+                }
+                mouse::Event::WheelScrolled { delta } => match delta {
+                    mouse::ScrollDelta::Lines { y, .. }
+                    | mouse::ScrollDelta::Pixels { y, .. } => {
+                        if y < 0.0 && chart_state.scaling > Self::MIN_SCALING
+                            || y > 0.0 && chart_state.scaling < Self::MAX_SCALING
+                        {
+                            //let old_scaling = self.scaling;
+
+                            let scaling = (chart_state.scaling * (1.0 + y / 30.0))
+                                .clamp(
+                                    Self::MIN_SCALING,  // 0.1
+                                    Self::MAX_SCALING,  // 2.0
+                                );
+
+                            //let translation =
+                            //    if let Some(cursor_to_center) =
+                            //        cursor.position_from(bounds.center())
+                            //    {
+                            //        let factor = scaling - old_scaling;
+
+                            //        Some(
+                            //            self.translation
+                            //                - Vector::new(
+                            //                    cursor_to_center.x * factor
+                            //                        / (old_scaling
+                            //                            * old_scaling),
+                            //                    cursor_to_center.y * factor
+                            //                        / (old_scaling
+                            //                            * old_scaling),
+                            //                ),
+                            //        )
+                            //    } else {
+                            //        None
+                            //    };
+
+                            (
+                                event::Status::Captured,
+                                Some(Message::Scaled(scaling, None)),
+                            )
+                        } else {
+                            (event::Status::Captured, None)
+                        }
+                    }
+                },
+                _ => (event::Status::Ignored, None),
+            },
+            _ => (event::Status::Ignored, None),
+        }
+    }
+    
+    fn draw(
+        &self,
+        _state: &Self::State,
+        renderer: &Renderer,
+        _theme: &Theme,
+        bounds: Rectangle,
+        cursor: mouse::Cursor,
+    ) -> Vec<Geometry> {    
+        let chart = self.get_common_data();
+
+        let (latest, earliest) = (chart.x_max_time, chart.x_min_time);    
+        let (lowest, highest) = (chart.y_min_price, chart.y_max_price);
+
+        let y_range = highest - lowest;
+
+        let volume_area_height = bounds.height / 8.0; 
+        let candlesticks_area_height = bounds.height - volume_area_height;
+
+        let y_labels_can_fit = (bounds.height / 32.0) as i32;
+        let (step, rounded_lowest) = calculate_price_step(highest, lowest, y_labels_can_fit);
+
+        let x_labels_can_fit = (bounds.width / 90.0) as i32;
+        let (time_step, rounded_earliest) = calculate_time_step(earliest, latest, x_labels_can_fit, Some(self.timeframe));
+
+        let background = chart.mesh_cache.draw(renderer, bounds.size(), |frame| {
+            frame.with_save(|frame| {
+                let mut time = rounded_earliest;
+
+                while time <= latest {                    
+                    let x_position = ((time - earliest) as f64 / (latest - earliest) as f64) * bounds.width as f64;
+
+                    if x_position >= 0.0 && x_position <= bounds.width as f64 {
+                        let line = Path::line(
+                            Point::new(x_position as f32, 0.0), 
+                            Point::new(x_position as f32, bounds.height)
+                        );
+                        frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(27, 27, 27, 1.0)).with_width(1.0))
+                    };
+                    
+                    time += time_step;
+                }
+            });
+            
+            frame.with_save(|frame| {
+                let mut y = rounded_lowest;
+
+                while y <= highest {
+                    let y_position = candlesticks_area_height - ((y - lowest) / y_range * candlesticks_area_height);
+                    let line = Path::line(
+                        Point::new(0.0, y_position), 
+                        Point::new(bounds.width, y_position)
+                    );
+                    frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(27, 27, 27, 1.0)).with_width(1.0));
+                    y += step;
+                }
+            });
+        });
+
+        let candlesticks = chart.main_cache.draw(renderer, bounds.size(), |frame| {
+            let mut max_volume: f32 = 0.0;
+
+            for (_, kline) in self.data_points.range(earliest..=latest) {
+                max_volume = max_volume.max(kline.4.max(kline.5));
+            }
+
+            for (time, (open, high, low, close, buy_volume, sell_volume)) in self.data_points.range(earliest..=latest) {
+                let x_position: f64 = ((time - earliest) as f64 / (latest - earliest) as f64) * bounds.width as f64;
+                
+                let y_open = candlesticks_area_height - ((open - lowest) / y_range * candlesticks_area_height);
+                let y_high = candlesticks_area_height - ((high - lowest) / y_range * candlesticks_area_height);
+                let y_low = candlesticks_area_height - ((low - lowest) / y_range * candlesticks_area_height);
+                let y_close = candlesticks_area_height - ((close - lowest) / y_range * candlesticks_area_height);
+                
+                let color = if close >= open { Color::from_rgb8(81, 205, 160) } else { Color::from_rgb8(192, 80, 77) };
+
+                let body = Path::rectangle(
+                    Point::new(x_position as f32 - (2.0 * chart.scaling), y_open.min(y_close)), 
+                    Size::new(4.0 * chart.scaling, (y_open - y_close).abs())
+                );                    
+                frame.fill(&body, color);
+                
+                let wick = Path::line(
+                    Point::new(x_position as f32, y_high), 
+                    Point::new(x_position as f32, y_low)
+                );
+                frame.stroke(&wick, Stroke::default().with_color(color).with_width(1.0));
+
+                if *buy_volume != -1.0 {
+                    let buy_bar_height = (buy_volume / max_volume) * volume_area_height;
+                    let sell_bar_height = (sell_volume / max_volume) * volume_area_height;
+                    
+                    let buy_bar = Path::rectangle(
+                        Point::new(x_position as f32, bounds.height - buy_bar_height), 
+                        Size::new(2.0 * chart.scaling, buy_bar_height)
+                    );
+                    frame.fill(&buy_bar, Color::from_rgb8(81, 205, 160)); 
+                    
+                    let sell_bar = Path::rectangle(
+                        Point::new(x_position as f32 - (2.0 * chart.scaling), bounds.height - sell_bar_height), 
+                        Size::new(2.0 * chart.scaling, sell_bar_height)
+                    );
+                    frame.fill(&sell_bar, Color::from_rgb8(192, 80, 77)); 
+                } else {
+                    let bar_height = ((sell_volume) / max_volume) * volume_area_height;
+                    
+                    let bar = Path::rectangle(
+                        Point::new(x_position as f32 - (2.0 * chart.scaling), bounds.height - bar_height), 
+                        Size::new(4.0 * chart.scaling, bar_height)
+                    );
+                    let color = if close >= open { Color::from_rgba8(81, 205, 160, 0.8) } else { Color::from_rgba8(192, 80, 77, 0.8) };
+
+                    frame.fill(&bar, color);
+                }
+            }
+        });
+
+        if chart.crosshair {
+            let crosshair = chart.crosshair_cache.draw(renderer, bounds.size(), |frame| {
+                if let Some(cursor_position) = cursor.position_in(bounds) {
+                    let line = Path::line(
+                        Point::new(0.0, cursor_position.y), 
+                        Point::new(bounds.width, cursor_position.y)
+                    );
+                    frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(200, 200, 200, 0.6)).with_width(1.0));
+
+                    let crosshair_ratio = cursor_position.x as f64 / bounds.width as f64;
+                    let crosshair_millis = earliest as f64 + crosshair_ratio * (latest - earliest) as f64;
+                    let rounded_timestamp = (crosshair_millis / (self.timeframe as f64 * 60.0 * 1000.0)).round() as i64 * self.timeframe as i64 * 60 * 1000;
+
+                    let snap_ratio = (rounded_timestamp as f64 - earliest as f64) / (latest as f64 - earliest as f64);
+                    let snap_x = snap_ratio * bounds.width as f64;
+
+                    let line = Path::line(
+                        Point::new(snap_x as f32, 0.0), 
+                        Point::new(snap_x as f32, bounds.height)
+                    );
+                    frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(200, 200, 200, 0.6)).with_width(1.0));
+
+                    if let Some((_, kline)) = self.data_points.iter()
+                        .find(|(time, _)| **time == rounded_timestamp as i64) {
+
+                        
+                        let tooltip_text: String = if kline.4 != -1.0 {
+                            format!(
+                                "O: {} H: {} L: {} C: {}\nBuyV: {:.0} SellV: {:.0}",
+                                kline.0, kline.1, kline.2, kline.3, kline.4, kline.5
+                            )
+                        } else {
+                            format!(
+                                "O: {} H: {} L: {} C: {}\nVolume: {:.0}",
+                                kline.0, kline.1, kline.2, kline.3, kline.5
+                            )
+                        };
+
+                        let text = canvas::Text {
+                            content: tooltip_text,
+                            position: Point::new(10.0, 10.0),
+                            size: iced::Pixels(12.0),
+                            color: Color::from_rgba8(120, 120, 120, 1.0),
+                            ..canvas::Text::default()
+                        };
+                        frame.fill_text(text);
+                    }
+                }
+            });
+
+            vec![background, crosshair, candlesticks]
+        }   else {
+            vec![background, candlesticks]
+        }
+    }
+
+    fn mouse_interaction(
+        &self,
+        interaction: &Interaction,
+        bounds: Rectangle,
+        cursor: mouse::Cursor,
+    ) -> mouse::Interaction {
+        match interaction {
+            Interaction::Drawing => mouse::Interaction::Crosshair,
+            Interaction::Erasing => mouse::Interaction::Crosshair,
+            Interaction::Panning { .. } => mouse::Interaction::Grabbing,
+            Interaction::None if cursor.is_over(bounds) => {
+                if self.chart.crosshair {
+                    mouse::Interaction::Crosshair
+                } else {
+                    mouse::Interaction::default()
+                }
+            }
+            Interaction::None => { mouse::Interaction::default() }
+        }
+    }
+}

+ 0 - 973
src/charts/custom_line.rs

@@ -1,973 +0,0 @@
-use std::{collections::BTreeMap, vec};
-use chrono::{DateTime, Utc, TimeZone, LocalResult, Duration, NaiveDateTime, Timelike};
-use iced::{
-    alignment, mouse, widget::{button, canvas::{self, event::{self, Event}, stroke::Stroke, Cache, Canvas, Geometry, Path}}, window, Border, Color, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector
-};
-use iced::widget::{Column, Row, Container, Text};
-use crate::{market_data::Kline, Timeframe};
-
-#[derive(Debug, Clone, Copy)]
-pub enum Message {
-    Translated(Vector),
-    Scaled(f32, Option<Vector>),
-    ChartBounds(Rectangle),
-    AutoscaleToggle,
-    CrosshairToggle,
-    CrosshairMoved(Point),
-}
-
-#[derive(Debug)]
-pub struct CustomLine {
-    mesh_cache: Cache,
-    candles_cache: Cache,
-    crosshair_cache: Cache,
-    x_labels_cache: Cache,
-    y_labels_cache: Cache,
-    y_croshair_cache: Cache,
-    x_crosshair_cache: Cache,
-    translation: Vector,
-    scaling: f32,
-    klines_raw: BTreeMap<i64, (f32, f32, f32, f32, f32, f32)>,
-    timeframe: i16,
-    autoscale: bool,
-    crosshair: bool,
-    crosshair_position: Point,
-    x_min_time: i64,
-    x_max_time: i64,
-    y_min_price: f32,
-    y_max_price: f32,
-    bounds: Rectangle,
-}
-impl CustomLine {
-    const MIN_SCALING: f32 = 0.1;
-    const MAX_SCALING: f32 = 2.0;
-
-    pub fn new(klines: Vec<Kline>, timeframe: Timeframe) -> CustomLine {
-        let _size = window::Settings::default().size;
-        let mut klines_raw = BTreeMap::new();
-
-        for kline in klines {
-            let buy_volume = kline.taker_buy_base_asset_volume;
-            let sell_volume = kline.volume - buy_volume;
-            klines_raw.insert(kline.time as i64, (kline.open, kline.high, kline.low, kline.close, buy_volume, sell_volume));
-        }
-
-        let timeframe = match timeframe {
-            Timeframe::M1 => 1,
-            Timeframe::M3 => 3,
-            Timeframe::M5 => 5,
-            Timeframe::M15 => 15,
-            Timeframe::M30 => 30,
-        };
-    
-        CustomLine {
-            mesh_cache: canvas::Cache::default(),
-            candles_cache: canvas::Cache::default(),
-            crosshair_cache: canvas::Cache::default(),
-            x_labels_cache: canvas::Cache::default(),
-            y_labels_cache: canvas::Cache::default(),
-            y_croshair_cache: canvas::Cache::default(),
-            x_crosshair_cache: canvas::Cache::default(),
-            timeframe,
-            klines_raw,
-            translation: Vector::default(),
-            scaling: 1.0,
-            autoscale: true,
-            crosshair: false,
-            crosshair_position: Point::new(0.0, 0.0),
-            x_min_time: 0,
-            x_max_time: 0,
-            y_min_price: 0.0,
-            y_max_price: 0.0,
-            bounds: Rectangle::default(),
-        }
-    }
-
-    pub fn insert_datapoint(&mut self, kline: &Kline) {
-        let buy_volume: f32 = kline.taker_buy_base_asset_volume;
-        let sell_volume: f32 = if buy_volume != -1.0 {
-            kline.volume - buy_volume
-        } else {
-            kline.volume
-        };
-
-        self.klines_raw.insert(kline.time as i64, (kline.open, kline.high, kline.low, kline.close, buy_volume, sell_volume));
-
-        self.render_start();
-    }
-    
-    pub fn render_start(&mut self) {
-        self.candles_cache.clear();
-
-        let latest: i64 = self.klines_raw.keys().last().map_or(0, |time| time - ((self.translation.x*10000.0)*(self.timeframe as f32)) as i64);
-        let earliest: i64 = latest - ((6400000.0*self.timeframe as f32) / (self.scaling / (self.bounds.width/800.0))) as i64;
-
-        let (visible_klines, highest, lowest, avg_body_height, _, _) = self.klines_raw.iter()
-            .filter(|(time, _)| {
-                **time >= earliest && **time <= latest
-            })
-            .fold((vec![], f32::MIN, f32::MAX, 0.0f32, 0.0f32, None), |(mut klines, highest, lowest, total_body_height, max_vol, latest_kline), (time, kline)| {
-                let body_height = (kline.0 - kline.3).abs();
-                klines.push((*time, *kline));
-                let total_body_height = match latest_kline {
-                    Some(_) => total_body_height + body_height,
-                    None => total_body_height,
-                };
-                (
-                    klines,
-                    highest.max(kline.1),
-                    lowest.min(kline.2),
-                    total_body_height,
-                    max_vol.max(kline.4.max(kline.5)),
-                    Some(kline)
-                )
-            });
-
-        if visible_klines.is_empty() || visible_klines.len() == 1 {
-            return;
-        }
-
-        let avg_body_height = avg_body_height / (visible_klines.len() - 1) as f32;
-        let (highest, lowest) = (highest + avg_body_height, lowest - avg_body_height);
-
-        if earliest != self.x_min_time || latest != self.x_max_time || lowest != self.y_min_price || highest != self.y_max_price {
-            self.x_labels_cache.clear();
-            self.mesh_cache.clear();
-        }
-
-        self.x_min_time = earliest;
-        self.x_max_time = latest;
-        self.y_min_price = lowest;
-        self.y_max_price = highest;
-
-        self.y_labels_cache.clear();
-        self.crosshair_cache.clear();
-    }
-
-    pub fn update(&mut self, message: &Message) {
-        match message {
-            Message::Translated(translation) => {
-                if self.autoscale {
-                    self.translation.x = translation.x;
-                } else {
-                    self.translation = *translation;
-                }
-                self.crosshair_position = Point::new(0.0, 0.0);
-
-                self.render_start();
-            }
-            Message::Scaled(scaling, translation) => {
-                self.scaling = *scaling;
-                
-                if let Some(translation) = translation {
-                    if self.autoscale {
-                        self.translation.x = translation.x;
-                    } else {
-                        self.translation = *translation;
-                    }
-                }
-                self.crosshair_position = Point::new(0.0, 0.0);
-
-                self.render_start();
-            }
-            Message::ChartBounds(bounds) => {
-                self.bounds = *bounds;
-            }
-            Message::AutoscaleToggle => {
-                self.autoscale = !self.autoscale;
-            }
-            Message::CrosshairToggle => {
-                self.crosshair = !self.crosshair;
-            }
-            Message::CrosshairMoved(position) => {
-                self.crosshair_position = *position;
-                if self.crosshair {
-                    self.crosshair_cache.clear();
-                    self.y_croshair_cache.clear();
-                    self.x_crosshair_cache.clear();
-                }
-            }
-        }
-    }
-
-    pub fn view(&self) -> Element<Message> {
-        let chart = Canvas::new(self)
-            .width(Length::FillPortion(10))
-            .height(Length::FillPortion(10));
-    
-        let axis_labels_x = Canvas::new(
-            AxisLabelXCanvas { 
-                labels_cache: &self.x_labels_cache, 
-                min: self.x_min_time, 
-                max: self.x_max_time, 
-                crosshair_cache: &self.x_crosshair_cache, 
-                crosshair_position: self.crosshair_position, 
-                crosshair: self.crosshair,
-                timeframe: self.timeframe
-            })
-            .width(Length::FillPortion(10))
-            .height(Length::Fixed(26.0));
-
-        let last_close_price = self.klines_raw.values().last().map_or(0.0, |kline| kline.3);
-        let last_open_price = self.klines_raw.values().last().map_or(0.0, |kline| kline.0);
-    
-        let axis_labels_y = Canvas::new(
-            AxisLabelYCanvas { 
-                labels_cache: &self.y_labels_cache, 
-                y_croshair_cache: &self.y_croshair_cache, 
-                min: self.y_min_price,
-                max: self.y_max_price,
-                last_close_price, 
-                last_open_price, 
-                crosshair_position: self.crosshair_position, 
-                crosshair: self.crosshair
-            })
-            .width(Length::Fixed(60.0))
-            .height(Length::FillPortion(10));
-
-        let autoscale_button = button(
-            Text::new("A")
-                .size(12)
-                .horizontal_alignment(alignment::Horizontal::Center)
-            )
-            .width(Length::Fill)
-            .height(Length::Fill)
-            .on_press(Message::AutoscaleToggle)
-            .style(|_theme: &Theme, _status: iced::widget::button::Status| chart_button(_theme, _status, self.autoscale));
-        let crosshair_button = button(
-            Text::new("+")
-                .size(12)
-                .horizontal_alignment(alignment::Horizontal::Center)
-            ) 
-            .width(Length::Fill)
-            .height(Length::Fill)
-            .on_press(Message::CrosshairToggle)
-            .style(|_theme: &Theme, _status: iced::widget::button::Status| chart_button(_theme, _status, self.crosshair));
-    
-        let chart_controls = Container::new(
-            Row::new()
-                .push(autoscale_button)
-                .push(crosshair_button).spacing(2)
-            ).padding([0, 2, 0, 2])
-            .width(Length::Fixed(60.0))
-            .height(Length::Fixed(26.0));
-
-        let chart_and_y_labels = Row::new()
-            .push(chart)
-            .push(axis_labels_y);
-    
-        let bottom_row = Row::new()
-            .push(axis_labels_x)
-            .push(chart_controls);
-    
-        let content = Column::new()
-            .push(chart_and_y_labels)
-            .push(bottom_row)
-            .spacing(0)
-            .padding(5);
-    
-        content.into()
-    }
-}
-
-fn chart_button(_theme: &Theme, _status: button::Status, is_active: bool) -> button::Style {
-    button::Style {
-        background: Some(Color::from_rgba8(20, 20, 20, 1.0).into()),
-        border: Border {
-            color: {
-                if is_active {
-                    Color::from_rgba8(50, 50, 50, 1.0)
-                } else {
-                    Color::from_rgba8(20, 20, 20, 1.0)
-                }
-            },
-            width: 1.0,
-            radius: 2.0.into(),
-        },
-        text_color: Color::WHITE,
-        ..button::Style::default()
-    }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub enum Interaction {
-    None,
-    Drawing,
-    Erasing,
-    Panning { translation: Vector, start: Point },
-}
-
-impl Default for Interaction {
-    fn default() -> Self {
-        Self::None
-    }
-}
-impl canvas::Program<Message> for CustomLine {
-    type State = Interaction;
-
-    fn update(
-        &self,
-        interaction: &mut Interaction,
-        event: Event,
-        bounds: Rectangle,
-        cursor: mouse::Cursor,
-    ) -> (event::Status, Option<Message>) {        
-        if bounds != self.bounds {
-            return (event::Status::Ignored, Some(Message::ChartBounds(bounds)));
-        } 
-        
-        if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event {
-            *interaction = Interaction::None;
-        }
-
-        let Some(cursor_position) = cursor.position_in(bounds) else {
-            return (event::Status::Ignored, 
-                if self.crosshair {
-                    Some(Message::CrosshairMoved(Point::new(0.0, 0.0)))
-                } else {
-                    None
-                }
-                );
-        };
-
-        match event {
-            Event::Mouse(mouse_event) => match mouse_event {
-                mouse::Event::ButtonPressed(button) => {
-                    let message = match button {
-                        mouse::Button::Right => {
-                            *interaction = Interaction::Drawing;
-                            None
-                        }
-                        mouse::Button::Left => {
-                            *interaction = Interaction::Panning {
-                                translation: self.translation,
-                                start: cursor_position,
-                            };
-                            None
-                        }
-                        _ => None,
-                    };
-
-                    (event::Status::Captured, message)
-                }
-                mouse::Event::CursorMoved { .. } => {
-                    let message = match *interaction {
-                        Interaction::Drawing => None,
-                        Interaction::Erasing => None,
-                        Interaction::Panning { translation, start } => {
-                            Some(Message::Translated(
-                                translation
-                                    + (cursor_position - start)
-                                        * (1.0 / self.scaling),
-                            ))
-                        }
-                        Interaction::None => 
-                            if self.crosshair && cursor.is_over(bounds) {
-                                Some(Message::CrosshairMoved(cursor_position))
-                            } else {
-                                None
-                            },
-                    };
-
-                    let event_status = match interaction {
-                        Interaction::None => event::Status::Ignored,
-                        _ => event::Status::Captured,
-                    };
-
-                    (event_status, message)
-                }
-                mouse::Event::WheelScrolled { delta } => match delta {
-                    mouse::ScrollDelta::Lines { y, .. }
-                    | mouse::ScrollDelta::Pixels { y, .. } => {
-                        if y < 0.0 && self.scaling > Self::MIN_SCALING
-                            || y > 0.0 && self.scaling < Self::MAX_SCALING
-                        {
-                            //let old_scaling = self.scaling;
-
-                            let scaling = (self.scaling * (1.0 + y / 30.0))
-                                .clamp(
-                                    Self::MIN_SCALING,  // 0.1
-                                    Self::MAX_SCALING,  // 2.0
-                                );
-
-                            //let translation =
-                            //    if let Some(cursor_to_center) =
-                            //        cursor.position_from(bounds.center())
-                            //    {
-                            //        let factor = scaling - old_scaling;
-
-                            //        Some(
-                            //            self.translation
-                            //                - Vector::new(
-                            //                    cursor_to_center.x * factor
-                            //                        / (old_scaling
-                            //                            * old_scaling),
-                            //                    cursor_to_center.y * factor
-                            //                        / (old_scaling
-                            //                            * old_scaling),
-                            //                ),
-                            //        )
-                            //    } else {
-                            //        None
-                            //    };
-
-                            (
-                                event::Status::Captured,
-                                Some(Message::Scaled(scaling, None)),
-                            )
-                        } else {
-                            (event::Status::Captured, None)
-                        }
-                    }
-                },
-                _ => (event::Status::Ignored, None),
-            },
-            _ => (event::Status::Ignored, None),
-        }
-    }
-    
-    fn draw(
-        &self,
-        _state: &Self::State,
-        renderer: &Renderer,
-        _theme: &Theme,
-        bounds: Rectangle,
-        cursor: mouse::Cursor,
-    ) -> Vec<Geometry> {    
-        let latest: i64 = self.klines_raw.keys().last().map_or(0, |time| time - ((self.translation.x*10000.0)*(self.timeframe as f32)) as i64);
-        let earliest: i64 = latest - ((6400000.0*self.timeframe as f32) / (self.scaling / (bounds.width/800.0))) as i64;
-
-        let (visible_klines, highest, lowest, avg_body_height, max_volume, _) = self.klines_raw.iter()
-            .filter(|(time, _)| {
-                **time >= earliest && **time <= latest
-            })
-            .fold((vec![], f32::MIN, f32::MAX, 0.0f32, 0.0f32, None), |(mut klines, highest, lowest, total_body_height, max_vol, latest_kline), (time, kline)| {
-                let body_height = (kline.0 - kline.3).abs();
-                klines.push((*time, *kline));
-                let total_body_height = match latest_kline {
-                    Some(_) => total_body_height + body_height,
-                    None => total_body_height,
-                };
-                (
-                    klines,
-                    highest.max(kline.1),
-                    lowest.min(kline.2),
-                    total_body_height,
-                    max_vol.max(kline.4.max(kline.5)),
-                    Some(kline)
-                )
-            });
-
-        if visible_klines.is_empty() || visible_klines.len() == 1 {
-            return vec![];
-        }
-
-        let avg_body_height = avg_body_height / (visible_klines.len() - 1) as f32;
-        let (highest, lowest) = (highest + avg_body_height, lowest - avg_body_height);
-        let y_range = highest - lowest;
-
-        let volume_area_height = bounds.height / 8.0; 
-        let candlesticks_area_height = bounds.height - volume_area_height;
-
-        let y_labels_can_fit = (bounds.height / 32.0) as i32;
-        let (step, rounded_lowest) = calculate_price_step(highest, lowest, y_labels_can_fit);
-
-        let x_labels_can_fit = (bounds.width / 90.0) as i32;
-        let (time_step, rounded_earliest) = calculate_time_step(earliest, latest, x_labels_can_fit, self.timeframe);
-
-        let background = self.mesh_cache.draw(renderer, bounds.size(), |frame| {
-            frame.with_save(|frame| {
-                let mut time = rounded_earliest;
-
-                while time <= latest {                    
-                    let x_position = ((time - earliest) as f64 / (latest - earliest) as f64) * bounds.width as f64;
-
-                    if x_position >= 0.0 && x_position <= bounds.width as f64 {
-                        let line = Path::line(
-                            Point::new(x_position as f32, 0.0), 
-                            Point::new(x_position as f32, bounds.height)
-                        );
-                        frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(27, 27, 27, 1.0)).with_width(1.0))
-                    };
-                    
-                    time += time_step;
-                }
-            });
-            
-            frame.with_save(|frame| {
-                let mut y = rounded_lowest;
-
-                while y <= highest {
-                    let y_position = candlesticks_area_height - ((y - lowest) / y_range * candlesticks_area_height);
-                    let line = Path::line(
-                        Point::new(0.0, y_position), 
-                        Point::new(bounds.width, y_position)
-                    );
-                    frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(27, 27, 27, 1.0)).with_width(1.0));
-                    y += step;
-                }
-            });
-        });
-
-        let candlesticks = self.candles_cache.draw(renderer, bounds.size(), |frame| {
-            for (time, (open, high, low, close, buy_volume, sell_volume)) in visible_klines {
-                let x_position: f64 = ((time - earliest) as f64 / (latest - earliest) as f64) * bounds.width as f64;
-                
-                let y_open = candlesticks_area_height - ((open - lowest) / y_range * candlesticks_area_height);
-                let y_high = candlesticks_area_height - ((high - lowest) / y_range * candlesticks_area_height);
-                let y_low = candlesticks_area_height - ((low - lowest) / y_range * candlesticks_area_height);
-                let y_close = candlesticks_area_height - ((close - lowest) / y_range * candlesticks_area_height);
-                
-                let color = if close >= open { Color::from_rgb8(81, 205, 160) } else { Color::from_rgb8(192, 80, 77) };
-
-                let body = Path::rectangle(
-                    Point::new(x_position as f32 - (2.0 * self.scaling), y_open.min(y_close)), 
-                    Size::new(4.0 * self.scaling, (y_open - y_close).abs())
-                );                    
-                frame.fill(&body, color);
-                
-                let wick = Path::line(
-                    Point::new(x_position as f32, y_high), 
-                    Point::new(x_position as f32, y_low)
-                );
-                frame.stroke(&wick, Stroke::default().with_color(color).with_width(1.0));
-
-                if buy_volume != -1.0 {
-                    let buy_bar_height = (buy_volume / max_volume) * volume_area_height;
-                    let sell_bar_height = (sell_volume / max_volume) * volume_area_height;
-                    
-                    let buy_bar = Path::rectangle(
-                        Point::new(x_position as f32, bounds.height - buy_bar_height), 
-                        Size::new(2.0 * self.scaling, buy_bar_height)
-                    );
-                    frame.fill(&buy_bar, Color::from_rgb8(81, 205, 160)); 
-                    
-                    let sell_bar = Path::rectangle(
-                        Point::new(x_position as f32 - (2.0 * self.scaling), bounds.height - sell_bar_height), 
-                        Size::new(2.0 * self.scaling, sell_bar_height)
-                    );
-                    frame.fill(&sell_bar, Color::from_rgb8(192, 80, 77)); 
-                } else {
-                    let bar_height = ((sell_volume) / max_volume) * volume_area_height;
-                    
-                    let bar = Path::rectangle(
-                        Point::new(x_position as f32 - (2.0 * self.scaling), bounds.height - bar_height), 
-                        Size::new(4.0 * self.scaling, bar_height)
-                    );
-                    let color = if close >= open { Color::from_rgba8(81, 205, 160, 0.8) } else { Color::from_rgba8(192, 80, 77, 0.8) };
-
-                    frame.fill(&bar, color);
-                }
-            }
-        });
-
-        if self.crosshair {
-            let crosshair = self.crosshair_cache.draw(renderer, bounds.size(), |frame| {
-                if let Some(cursor_position) = cursor.position_in(bounds) {
-                    let line = Path::line(
-                        Point::new(0.0, cursor_position.y), 
-                        Point::new(bounds.width, cursor_position.y)
-                    );
-                    frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(200, 200, 200, 0.6)).with_width(1.0));
-
-                    let crosshair_ratio = cursor_position.x as f64 / bounds.width as f64;
-                    let crosshair_millis = earliest as f64 + crosshair_ratio * (latest - earliest) as f64;
-                    let rounded_timestamp = (crosshair_millis / (self.timeframe as f64 * 60.0 * 1000.0)).round() as i64 * self.timeframe as i64 * 60 * 1000;
-
-                    let snap_ratio = (rounded_timestamp as f64 - earliest as f64) / (latest as f64 - earliest as f64);
-                    let snap_x = snap_ratio * bounds.width as f64;
-
-                    let line = Path::line(
-                        Point::new(snap_x as f32, 0.0), 
-                        Point::new(snap_x as f32, bounds.height)
-                    );
-                    frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(200, 200, 200, 0.6)).with_width(1.0));
-
-                    if let Some((_, kline)) = self.klines_raw.iter()
-                        .find(|(time, _)| **time == rounded_timestamp as i64) {
-
-                        
-                        let tooltip_text: String = if kline.4 != -1.0 {
-                            format!(
-                                "O: {} H: {} L: {} C: {}\nBuyV: {:.0} SellV: {:.0}",
-                                kline.0, kline.1, kline.2, kline.3, kline.4, kline.5
-                            )
-                        } else {
-                            format!(
-                                "O: {} H: {} L: {} C: {}\nVolume: {:.0}",
-                                kline.0, kline.1, kline.2, kline.3, kline.5
-                            )
-                        };
-
-                        let text = canvas::Text {
-                            content: tooltip_text,
-                            position: Point::new(10.0, 10.0),
-                            size: iced::Pixels(12.0),
-                            color: Color::from_rgba8(120, 120, 120, 1.0),
-                            ..canvas::Text::default()
-                        };
-                        frame.fill_text(text);
-                    }
-                }
-            });
-
-            vec![background, crosshair, candlesticks]
-        }   else {
-            vec![background, candlesticks]
-        }
-    }
-
-    fn mouse_interaction(
-        &self,
-        interaction: &Interaction,
-        bounds: Rectangle,
-        cursor: mouse::Cursor,
-    ) -> mouse::Interaction {
-        match interaction {
-            Interaction::Drawing => mouse::Interaction::Crosshair,
-            Interaction::Erasing => mouse::Interaction::Crosshair,
-            Interaction::Panning { .. } => mouse::Interaction::Grabbing,
-            Interaction::None if cursor.is_over(bounds) => {
-                if self.crosshair {
-                    mouse::Interaction::Crosshair
-                } else {
-                    mouse::Interaction::default()
-                }
-            }
-            Interaction::None => { mouse::Interaction::default() }
-        }
-    }
-}
-
-fn calculate_price_step(highest: f32, lowest: f32, labels_can_fit: i32) -> (f32, f32) {
-    let range = highest - lowest;
-    let mut step = 1000.0; 
-
-    let steps = [1000.0, 500.0, 200.0, 100.0, 50.0, 20.0, 10.0, 5.0, 2.0, 1.0, 0.5, 0.2, 0.1, 0.05];
-
-    for &s in steps.iter().rev() {
-        if range / s <= labels_can_fit as f32 {
-            step = s;
-            break;
-        }
-    }
-    let rounded_lowest = (lowest / step).floor() * step;
-
-    (step, rounded_lowest)
-}
-
-const M1_TIME_STEPS: [i64; 9] = [
-    1000 * 60 * 720, // 12 hour
-    1000 * 60 * 180, // 3 hour
-    1000 * 60 * 60, // 1 hour
-    1000 * 60 * 30, // 30 minutes
-    1000 * 60 * 15, // 15 minutes
-    1000 * 60 * 10, // 10 minutes
-    1000 * 60 * 5, // 5 minutes
-    1000 * 60 * 2, // 2 minutes
-    60 * 1000, // 1 minute
-];
-const M3_TIME_STEPS: [i64; 9] = [
-    1000 * 60 * 1440, // 24 hour
-    1000 * 60 * 720, // 12 hour
-    1000 * 60 * 180, // 6 hour
-    1000 * 60 * 120, // 6 hour
-    1000 * 60 * 60, // 1 hour
-    1000 * 60 * 30, // 30 minutes
-    1000 * 60 * 15, // 15 minutes
-    1000 * 60 * 9, // 9 minutes
-    1000 * 60 * 3, // 3 minutes
-];
-const M5_TIME_STEPS: [i64; 9] = [
-    1000 * 60 * 1440, // 24 hour
-    1000 * 60 * 720, // 12 hour
-    1000 * 60 * 480, // 8 hour
-    1000 * 60 * 240, // 4 hour
-    1000 * 60 * 120, // 2 hour
-    1000 * 60 * 60, // 1 hour
-    1000 * 60 * 30, // 30 minutes
-    1000 * 60 * 15, // 15 minutes
-    1000 * 60 * 5, // 5 minutes
-];
-fn calculate_time_step(earliest: i64, latest: i64, labels_can_fit: i32, timeframe: i16) -> (i64, i64) {
-    let duration = latest - earliest;
-
-    let time_steps = match timeframe {
-        1 => &M1_TIME_STEPS,
-        3 => &M3_TIME_STEPS,
-        5 => &M5_TIME_STEPS,
-        15 => &M5_TIME_STEPS[..7],
-        30 => &M5_TIME_STEPS[..6],
-        _ => &M1_TIME_STEPS,
-    };
-
-    let mut selected_step = time_steps[0];
-    for &step in time_steps.iter() {
-        if duration / step >= labels_can_fit as i64 {
-            selected_step = step;
-            break;
-        }
-        if step <= duration {
-            selected_step = step;
-        }
-    }
-
-    let rounded_earliest = (earliest / selected_step) * selected_step;
-
-    (selected_step, rounded_earliest)
-}
-
-pub struct AxisLabelXCanvas<'a> {
-    labels_cache: &'a Cache,
-    crosshair_cache: &'a Cache,
-    crosshair_position: Point,
-    crosshair: bool,
-    min: i64,
-    max: i64,
-    timeframe: i16,
-}
-impl canvas::Program<Message> for AxisLabelXCanvas<'_> {
-    type State = Interaction;
-
-    fn update(
-        &self,
-        _interaction: &mut Interaction,
-        _event: Event,
-        _bounds: Rectangle,
-        _cursor: mouse::Cursor,
-    ) -> (event::Status, Option<Message>) {
-        (event::Status::Ignored, None)
-    }
-    
-    fn draw(
-        &self,
-        _state: &Self::State,
-        renderer: &Renderer,
-        _theme: &Theme,
-        bounds: Rectangle,
-        _cursor: mouse::Cursor,
-    ) -> Vec<Geometry> {
-        if self.max == 0 {
-            return vec![];
-        }
-        let x_labels_can_fit = (bounds.width / 90.0) as i32;
-        let (time_step, rounded_earliest) = calculate_time_step(self.min, self.max, x_labels_can_fit, self.timeframe);
-
-        let labels = self.labels_cache.draw(renderer, bounds.size(), |frame| {
-            frame.with_save(|frame| {
-                let mut time = rounded_earliest;
-
-                while time <= self.max {                    
-                    let x_position = ((time - self.min) as f64 / (self.max - self.min) as f64) * bounds.width as f64;
-
-                    if x_position >= 0.0 && x_position <= bounds.width as f64 {
-                        let text_size = 12.0;
-                        let time_as_datetime = NaiveDateTime::from_timestamp(time / 1000, 0);
-                        let label = canvas::Text {
-                            content: time_as_datetime.format("%H:%M").to_string(),
-                            position: Point::new(x_position as f32 - (text_size*4.0/3.0), bounds.height - 20.0),
-                            size: iced::Pixels(text_size),
-                            color: Color::from_rgba8(200, 200, 200, 1.0),
-                            ..canvas::Text::default()
-                        };  
-
-                        label.draw_with(|path, color| {
-                            frame.fill(&path, color);
-                        });
-                    }
-                    
-                    time += time_step;
-                }
-            });
-        });
-        let crosshair = self.crosshair_cache.draw(renderer, bounds.size(), |frame| {
-            if self.crosshair && self.crosshair_position.x > 0.0 {
-                let crosshair_ratio = self.crosshair_position.x as f64 / bounds.width as f64;
-                let crosshair_millis = self.min as f64 + crosshair_ratio * (self.max - self.min) as f64;
-                let crosshair_time = NaiveDateTime::from_timestamp((crosshair_millis / 1000.0) as i64, 0);
-
-                let crosshair_timestamp = crosshair_time.timestamp();
-                let rounded_timestamp = (crosshair_timestamp as f64 / (self.timeframe as f64 * 60.0)).round() as i64 * self.timeframe as i64 * 60;
-                let rounded_time = NaiveDateTime::from_timestamp(rounded_timestamp, 0);
-
-                let snap_ratio = (rounded_timestamp as f64 * 1000.0 - self.min as f64) / (self.max as f64 - self.min as f64);
-                let snap_x = snap_ratio * bounds.width as f64;
-
-                let text_size: f32 = 12.0;
-                let text_content: String = rounded_time.format("%H:%M").to_string();
-                let growth_amount: f32 = 6.0; 
-                let rectangle_position: Point = Point::new(snap_x as f32 - (text_size*4.0/3.0) - growth_amount, bounds.height - 20.0);
-                let text_position: Point = Point::new(snap_x as f32 - (text_size*4.0/3.0), bounds.height - 20.0);
-
-                let text_background = canvas::Path::rectangle(rectangle_position, Size::new(text_content.len() as f32 * text_size/2.0 + 2.0 * growth_amount + 1.0, text_size + text_size/2.0));
-                frame.fill(&text_background, Color::from_rgba8(200, 200, 200, 1.0));
-
-                let crosshair_label = canvas::Text {
-                    content: text_content,
-                    position: text_position,
-                    size: iced::Pixels(text_size),
-                    color: Color::from_rgba8(0, 0, 0, 1.0),
-                    ..canvas::Text::default()
-                };
-
-                crosshair_label.draw_with(|path, color| {
-                    frame.fill(&path, color);
-                });
-            }
-        });
-
-        vec![labels, crosshair]
-    }
-
-    fn mouse_interaction(
-        &self,
-        interaction: &Interaction,
-        bounds: Rectangle,
-        cursor: mouse::Cursor,
-    ) -> mouse::Interaction {
-        match interaction {
-            Interaction::Drawing => mouse::Interaction::Crosshair,
-            Interaction::Erasing => mouse::Interaction::Crosshair,
-            Interaction::Panning { .. } => mouse::Interaction::ResizingHorizontally,
-            Interaction::None if cursor.is_over(bounds) => {
-                mouse::Interaction::ResizingHorizontally
-            }
-            Interaction::None => mouse::Interaction::default(),
-        }
-    }
-}
-pub struct AxisLabelYCanvas<'a> {
-    labels_cache: &'a Cache,
-    y_croshair_cache: &'a Cache,
-    min: f32,
-    max: f32,
-    last_close_price: f32,
-    last_open_price: f32,
-    crosshair_position: Point,
-    crosshair: bool,
-}
-impl canvas::Program<Message> for AxisLabelYCanvas<'_> {
-    type State = Interaction;
-
-    fn update(
-        &self,
-        _interaction: &mut Interaction,
-        _event: Event,
-        _bounds: Rectangle,
-        _cursor: mouse::Cursor,
-    ) -> (event::Status, Option<Message>) {
-        (event::Status::Ignored, None)
-    }
-    
-    fn draw(
-        &self,
-        _state: &Self::State,
-        renderer: &Renderer,
-        _theme: &Theme,
-        bounds: Rectangle,
-        _cursor: mouse::Cursor,
-    ) -> Vec<Geometry> {
-        if self.max == 0.0 {
-            return vec![];
-        }
-
-        let y_labels_can_fit = (bounds.height / 32.0) as i32;
-        let (step, rounded_lowest) = calculate_price_step(self.max, self.min, y_labels_can_fit);
-
-        let volume_area_height = bounds.height / 8.0; 
-        let candlesticks_area_height = bounds.height - volume_area_height;
-
-        let labels = self.labels_cache.draw(renderer, bounds.size(), |frame| {
-            frame.with_save(|frame| {
-                let y_range = self.max - self.min;
-                let mut y = rounded_lowest;
-
-                while y <= self.max {
-                    let y_position = candlesticks_area_height - ((y - self.min) / y_range * candlesticks_area_height);
-
-                    let text_size = 12.0;
-                    let decimal_places = i32::from(step.fract() != 0.0);
-                    let label_content = match decimal_places {
-                        0 => format!("{y:.0}"),
-                        _ => format!("{y:.1}"),
-                    };
-                    let label = canvas::Text {
-                        content: label_content,
-                        position: Point::new(10.0, y_position - text_size / 2.0),
-                        size: iced::Pixels(text_size),
-                        color: Color::from_rgba8(200, 200, 200, 1.0),
-                        ..canvas::Text::default()
-                    };  
-
-                    label.draw_with(|path, color| {
-                        frame.fill(&path, color);
-                    });
-
-                    y += step;
-                }
-
-                let last_close_y_position = candlesticks_area_height - ((self.last_close_price - self.min) / y_range * candlesticks_area_height);
-
-                let triangle_color = if self.last_close_price >= self.last_open_price {
-                    Color::from_rgba8(81, 205, 160, 0.9) 
-                } else {
-                    Color::from_rgba8(192, 80, 77, 0.9) 
-                };
-
-                let triangle = Path::new(|path| {
-                    path.move_to(Point::new(5.0, last_close_y_position));
-                    path.line_to(Point::new(0.0, last_close_y_position - 5.0));
-                    path.line_to(Point::new(0.0, last_close_y_position + 5.0));
-                    path.close();
-                });
-
-                frame.fill(&triangle, triangle_color);
-            });
-        });
-        let crosshair = self.y_croshair_cache.draw(renderer, bounds.size(), |frame| {
-            if self.crosshair && self.crosshair_position.y > 0.0 {
-                let text_size = 12.0;
-                let y_range = self.max - self.min;
-                let label_content = format!("{:.1}", self.min + (y_range * (candlesticks_area_height - self.crosshair_position.y) / candlesticks_area_height));
-                
-                let growth_amount = 3.0; 
-                let rectangle_position = Point::new(8.0 - growth_amount, self.crosshair_position.y - text_size / 2.0 - 3.0);
-                let text_position = Point::new(8.0, self.crosshair_position.y - text_size / 2.0 - 3.0);
-
-                let text_background = canvas::Path::rectangle(rectangle_position, Size::new(label_content.len() as f32 * text_size / 2.0 + 2.0 * growth_amount + 4.0, text_size + text_size / 1.8));
-                frame.fill(&text_background, Color::from_rgba8(200, 200, 200, 1.0));
-
-                let label = canvas::Text {
-                    content: label_content,
-                    position: text_position,
-                    size: iced::Pixels(text_size),
-                    color: Color::from_rgba8(0, 0, 0, 1.0),
-                    ..canvas::Text::default()
-                };
-
-                label.draw_with(|path, color| {
-                    frame.fill(&path, color);
-                });
-            }
-        });
-
-        vec![labels, crosshair]
-    }
-
-    fn mouse_interaction(
-        &self,
-        interaction: &Interaction,
-        bounds: Rectangle,
-        cursor: mouse::Cursor,
-    ) -> mouse::Interaction {
-        match interaction {
-            Interaction::Drawing => mouse::Interaction::Crosshair,
-            Interaction::Erasing => mouse::Interaction::Crosshair,
-            Interaction::Panning { .. } => mouse::Interaction::ResizingVertically,
-            Interaction::None if cursor.is_over(bounds) => {
-                mouse::Interaction::ResizingVertically
-            }
-            Interaction::None => mouse::Interaction::default(),
-        }
-    }
-}

+ 155 - 541
src/charts/footprint.rs

@@ -1,59 +1,44 @@
 use std::collections::{BTreeMap, HashMap};
-use chrono::NaiveDateTime;
 use iced::{
-    alignment, color, mouse, widget::{button, canvas::{self, event::{self, Event}, stroke::Stroke, Cache, Canvas, Geometry, Path}}, window, Border, Color, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector
+    alignment, mouse, widget::{button, canvas::{self, event::{self, Event}, stroke::Stroke, Canvas, Geometry, Path}}, Color, Element, Length, Point, Rectangle, Renderer, Size, Theme
 };
 use iced::widget::{Column, Row, Container, Text};
 use crate::data_providers::binance::market_data::{Kline, Trade};
 
-#[derive(Debug, Clone, Copy)]
-pub enum Message {
-    Translated(Vector),
-    Scaled(f32, Option<Vector>),
-    ChartBounds(Rectangle),
-    AutoscaleToggle,
-    CrosshairToggle,
-    CrosshairMoved(Point),
-}
+use super::{Chart, CommonChartData, Message, Interaction, AxisLabelXCanvas, AxisLabelYCanvas};
+use super::chart_button;
 
-#[derive(Debug)]
-pub struct Footprint {
-    heatmap_cache: Cache,
-    crosshair_cache: Cache,
-    x_labels_cache: Cache,
-    y_labels_cache: Cache,
-    y_croshair_cache: Cache,
-    x_crosshair_cache: Cache,
-    translation: Vector,
-    scaling: f32,
-    
+pub struct FootprintChart {
+    chart: CommonChartData,
     data_points: BTreeMap<i64, (HashMap<i64, (f32, f32)>, (f32, f32, f32, f32, f32, f32))>,
+    timeframe: u16,
+    tick_size: f32,
     raw_trades: Vec<Trade>,
+}
 
-    autoscale: bool,
-    crosshair: bool,
-    crosshair_position: Point,
-    x_min_time: i64,
-    x_max_time: i64,
-    y_min_price: f32,
-    y_max_price: f32,
-    bounds: Rectangle,
+impl Chart for FootprintChart {
+    type DataPoint = BTreeMap<i64, (HashMap<i64, (f32, f32)>, (f32, f32, f32, f32, f32, f32))>;
 
-    timeframe: u16,
-    tick_size: f32,
+    fn get_common_data(&self) -> &CommonChartData {
+        &self.chart
+    }
+    fn get_common_data_mut(&mut self) -> &mut CommonChartData {
+        &mut self.chart
+    }
 }
-impl Footprint {
+
+impl FootprintChart {
     const MIN_SCALING: f32 = 0.4;
     const MAX_SCALING: f32 = 3.6;
 
-    pub fn new(timeframe: u16, tick_size: f32, klines_raw: Vec<(i64, f32, f32, f32, f32, f32, f32)>, raw_trades: Vec<Trade>) -> Footprint {
+    pub fn new(timeframe: u16, tick_size: f32, klines_raw: Vec<(i64, f32, f32, f32, f32, f32, f32)>, raw_trades: Vec<Trade>) -> Self {
         let mut data_points = BTreeMap::new();
         let aggregate_time = 1000 * 60 * timeframe as i64;
 
         for kline in klines_raw {
             let kline_raw = (kline.1, kline.2, kline.3, kline.4, kline.5, kline.6);
             data_points.entry(kline.0).or_insert((HashMap::new(), kline_raw));
-        }
+        };
         for trade in &raw_trades {
             let rounded_time = (trade.time / aggregate_time) * aggregate_time;
             let price_level: i64 = (trade.price * (1.0 / tick_size)).round() as i64;
@@ -73,33 +58,13 @@ impl Footprint {
             } else {
                 entry.0.insert(price_level, (trade.qty, 0.0));
             }
-        }
-    
-        Footprint {
-            bounds: Rectangle::default(),
-                
-            heatmap_cache: canvas::Cache::default(),
-            crosshair_cache: canvas::Cache::default(),
-            x_labels_cache: canvas::Cache::default(),
-            y_labels_cache: canvas::Cache::default(),
-            y_croshair_cache: canvas::Cache::default(),
-            x_crosshair_cache: canvas::Cache::default(),
-
-            translation: Vector::default(),
-            scaling: 1.0,
-            autoscale: true,
-            
-            crosshair: false,
-            crosshair_position: Point::new(0.0, 0.0),
+        };
 
-            x_min_time: 0,
-            x_max_time: 0,
-            y_min_price: 0.0,
-            y_max_price: 0.0,
-            
+        FootprintChart {
+            chart: CommonChartData::default(),
+            data_points,
             timeframe,
             tick_size,
-            data_points,
             raw_trades,
         }
     }
@@ -128,10 +93,26 @@ impl Footprint {
 
             self.raw_trades.push(trade);
         }
-    
-        self.render_start();
     }
 
+    pub fn update_latest_kline(&mut self, kline: &Kline) {
+        if let Some((_, kline_value)) = self.data_points.get_mut(&(kline.time as i64)) {
+            kline_value.0 = kline.open;
+            kline_value.1 = kline.high;
+            kline_value.2 = kline.low;
+            kline_value.3 = kline.close;
+            kline_value.4 = kline.taker_buy_base_asset_volume;
+            
+            if kline_value.4 != -1.0 {
+                kline_value.5 = kline.volume - kline.taker_buy_base_asset_volume;
+            } else {
+                kline_value.5 = kline.volume;
+            }
+        }
+
+        self.render_start();
+    }
+    
     pub fn change_tick_size(&mut self, new_tick_size: f32) {
         let mut new_data_points = BTreeMap::new();
         let aggregate_time = 1000 * 60 * self.timeframe as i64;
@@ -165,31 +146,45 @@ impl Footprint {
         self.tick_size = new_tick_size;
     }
 
-    pub fn update_latest_kline(&mut self, kline: &Kline) {
-        if let Some((_, kline_value)) = self.data_points.get_mut(&(kline.time as i64)) {
-            kline_value.0 = kline.open;
-            kline_value.1 = kline.high;
-            kline_value.2 = kline.low;
-            kline_value.3 = kline.close;
-            kline_value.4 = kline.taker_buy_base_asset_volume;
-
-            if kline_value.4 != -1.0 {
-                kline_value.5 = kline.volume - kline.taker_buy_base_asset_volume;
-            } else {
-                kline_value.5 = kline.volume;
-            }
+    pub fn render_start(&mut self) {
+        let (latest, earliest, highest, lowest) = self.calculate_range();
+        if highest <= 0.0 || lowest <= 0.0 {
+            return;
         }
-    }
+
+        let chart_state = &mut self.chart;
+
+        if earliest != chart_state.x_min_time || latest != chart_state.x_max_time {
+            chart_state.x_min_time = earliest;
+            chart_state.x_max_time = latest;
+
+            chart_state.x_labels_cache.clear();
+            chart_state.x_crosshair_cache.clear();
+        };
     
-    pub fn render_start(&mut self) {
+        if lowest != chart_state.y_min_price || highest != chart_state.y_max_price {
+            chart_state.y_min_price = lowest;
+            chart_state.y_max_price = highest;
+
+            chart_state.y_labels_cache.clear();
+            chart_state.y_crosshair_cache.clear();
+        };
+    
+        chart_state.crosshair_cache.clear();
+        chart_state.main_cache.clear();
+    }
+
+    fn calculate_range(&self) -> (i64, i64, f32, f32) {
+        let chart = self.get_common_data();
+
         let timestamp_latest = self.data_points.keys().last().unwrap_or(&0);
 
-        let latest: i64 = *timestamp_latest - ((self.translation.x*1000.0)*(self.timeframe as f32)) as i64;
-        let earliest: i64 = latest - ((640000.0*self.timeframe as f32) / (self.scaling / (self.bounds.width/800.0))) as i64;
+        let latest: i64 = *timestamp_latest - ((chart.translation.x*1000.0)*(self.timeframe as f32)) as i64;
+        let earliest: i64 = latest - ((640000.0*self.timeframe as f32) / (chart.scaling / (chart.bounds.width/800.0))) as i64;
     
         let mut highest: f32 = 0.0;
         let mut lowest: f32 = std::f32::MAX;
-    
+
         for (_, (_, kline)) in self.data_points.range(earliest..=latest) {
             if kline.1 > highest {
                 highest = kline.1;
@@ -198,74 +193,63 @@ impl Footprint {
                 lowest = kline.2;
             }
         }
-        if highest == 0.0 || lowest == std::f32::MAX || lowest == 0.0 {
-            return;
-        }
+
         highest = highest + (highest - lowest) * 0.05;
         lowest = lowest - (highest - lowest) * 0.05;
-    
-        if earliest != self.x_min_time || latest != self.x_max_time {            
-            self.x_labels_cache.clear();
-            self.x_crosshair_cache.clear();
-        }
-        if lowest != self.y_min_price || highest != self.y_max_price {            
-            self.y_labels_cache.clear();
-            self.y_croshair_cache.clear();
-        }
-    
-        self.x_min_time = earliest;
-        self.x_max_time = latest;
-        self.y_min_price = lowest;
-        self.y_max_price = highest;
-        
-        self.crosshair_cache.clear();   
 
-        self.heatmap_cache.clear();     
+        (latest, earliest, highest, lowest)
     }
 
     pub fn update(&mut self, message: &Message) {
         match message {
             Message::Translated(translation) => {
-                if self.autoscale {
-                    self.translation.x = translation.x;
+                let chart = self.get_common_data_mut();
+
+                if chart.autoscale {
+                    chart.translation.x = translation.x;
                 } else {
-                    self.translation = *translation;
+                    chart.translation = *translation;
                 }
-                self.crosshair_position = Point::new(0.0, 0.0);
+                chart.crosshair_position = Point::new(0.0, 0.0);
 
                 self.render_start();
-            }
+            },
             Message::Scaled(scaling, translation) => {
-                self.scaling = *scaling;
+                let chart = self.get_common_data_mut();
+
+                chart.scaling = *scaling;
                 
                 if let Some(translation) = translation {
-                    if self.autoscale {
-                        self.translation.x = translation.x;
+                    if chart.autoscale {
+                        chart.translation.x = translation.x;
                     } else {
-                        self.translation = *translation;
+                        chart.translation = *translation;
                     }
                 }
-                self.crosshair_position = Point::new(0.0, 0.0);
+                chart.crosshair_position = Point::new(0.0, 0.0);
 
                 self.render_start();
-            }
+            },
             Message::ChartBounds(bounds) => {
-                self.bounds = *bounds;
-            }
+                self.chart.bounds = *bounds;
+            },
             Message::AutoscaleToggle => {
-                self.autoscale = !self.autoscale;
-            }
+                self.chart.autoscale = !self.chart.autoscale;
+            },
             Message::CrosshairToggle => {
-                self.crosshair = !self.crosshair;
-            }
+                self.chart.crosshair = !self.chart.crosshair;
+            },
             Message::CrosshairMoved(position) => {
-                self.crosshair_position = *position;
-                if self.crosshair {
-                    self.crosshair_cache.clear();
-                    self.y_croshair_cache.clear();
-                    self.x_crosshair_cache.clear();
+                let chart = self.get_common_data_mut();
+
+                chart.crosshair_position = *position;
+                if chart.crosshair {
+                    chart.crosshair_cache.clear();
+                    chart.y_crosshair_cache.clear();
+                    chart.x_crosshair_cache.clear();
                 }
-            }
+            },
+            _ => {}
         }
     }
 
@@ -274,27 +258,29 @@ impl Footprint {
             .width(Length::FillPortion(10))
             .height(Length::FillPortion(10));
 
+        let chart_state = self.get_common_data();
+
         let axis_labels_x = Canvas::new(
             AxisLabelXCanvas { 
-                labels_cache: &self.x_labels_cache, 
-                min: self.x_min_time, 
-                max: self.x_max_time, 
-                crosshair_cache: &self.x_crosshair_cache, 
-                crosshair_position: self.crosshair_position, 
-                crosshair: self.crosshair,
-                timeframe: self.timeframe
+                labels_cache: &chart_state.x_labels_cache, 
+                min: chart_state.x_min_time, 
+                max: chart_state.x_max_time, 
+                crosshair_cache: &chart_state.x_crosshair_cache, 
+                crosshair_position: chart_state.crosshair_position, 
+                crosshair: chart_state.crosshair,
+                timeframe: Some(self.timeframe)
             })
             .width(Length::FillPortion(10))
             .height(Length::Fixed(26.0));
 
         let axis_labels_y = Canvas::new(
             AxisLabelYCanvas { 
-                labels_cache: &self.y_labels_cache, 
-                y_croshair_cache: &self.y_croshair_cache, 
-                min: self.y_min_price,
-                max: self.y_max_price,
-                crosshair_position: self.crosshair_position, 
-                crosshair: self.crosshair
+                labels_cache: &chart_state.y_labels_cache, 
+                y_croshair_cache: &chart_state.y_crosshair_cache, 
+                min: chart_state.y_min_price,
+                max: chart_state.y_max_price,
+                crosshair_position: chart_state.crosshair_position, 
+                crosshair: chart_state.crosshair
             })
             .width(Length::Fixed(60.0))
             .height(Length::FillPortion(10));
@@ -307,7 +293,7 @@ impl Footprint {
             .width(Length::Fill)
             .height(Length::Fill)
             .on_press(Message::AutoscaleToggle)
-            .style(|_theme: &Theme, _status: iced::widget::button::Status| chart_button(_theme, _status, self.autoscale));
+            .style(|_theme: &Theme, _status: iced::widget::button::Status| chart_button(_theme, _status, chart_state.autoscale));
         let crosshair_button = button(
             Text::new("+")
                 .size(12)
@@ -316,7 +302,7 @@ impl Footprint {
             .width(Length::Fill)
             .height(Length::Fill)
             .on_press(Message::CrosshairToggle)
-            .style(|_theme: &Theme, _status: iced::widget::button::Status| chart_button(_theme, _status, self.crosshair));
+            .style(|_theme: &Theme, _status: iced::widget::button::Status| chart_button(_theme, _status, chart_state.crosshair));
     
         let chart_controls = Container::new(
             Row::new()
@@ -344,39 +330,7 @@ impl Footprint {
     }
 }
 
-fn chart_button(_theme: &Theme, _status: button::Status, is_active: bool) -> button::Style {
-    button::Style {
-        background: Some(Color::from_rgba8(20, 20, 20, 1.0).into()),
-        border: Border {
-            color: {
-                if is_active {
-                    Color::from_rgba8(50, 50, 50, 1.0)
-                } else {
-                    Color::from_rgba8(20, 20, 20, 1.0)
-                }
-            },
-            width: 1.0,
-            radius: 2.0.into(),
-        },
-        text_color: Color::WHITE,
-        ..button::Style::default()
-    }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub enum Interaction {
-    None,
-    Drawing,
-    Erasing,
-    Panning { translation: Vector, start: Point },
-}
-
-impl Default for Interaction {
-    fn default() -> Self {
-        Self::None
-    }
-}
-impl canvas::Program<Message> for Footprint {
+impl canvas::Program<Message> for FootprintChart {
     type State = Interaction;
 
     fn update(
@@ -385,8 +339,10 @@ impl canvas::Program<Message> for Footprint {
         event: Event,
         bounds: Rectangle,
         cursor: mouse::Cursor,
-    ) -> (event::Status, Option<Message>) {        
-        if bounds != self.bounds {
+    ) -> (event::Status, Option<Message>) {       
+        let chart_state = self.get_common_data();
+
+        if bounds != chart_state.bounds {
             return (event::Status::Ignored, Some(Message::ChartBounds(bounds)));
         } 
         
@@ -396,7 +352,7 @@ impl canvas::Program<Message> for Footprint {
 
         let Some(cursor_position) = cursor.position_in(bounds) else {
             return (event::Status::Ignored, 
-                if self.crosshair {
+                if chart_state.crosshair {
                     Some(Message::CrosshairMoved(Point::new(0.0, 0.0)))
                 } else {
                     None
@@ -414,7 +370,7 @@ impl canvas::Program<Message> for Footprint {
                         }
                         mouse::Button::Left => {
                             *interaction = Interaction::Panning {
-                                translation: self.translation,
+                                translation: chart_state.translation,
                                 start: cursor_position,
                             };
                             None
@@ -432,11 +388,11 @@ impl canvas::Program<Message> for Footprint {
                             Some(Message::Translated(
                                 translation
                                     + (cursor_position - start)
-                                        * (1.0 / self.scaling),
+                                        * (1.0 / chart_state.scaling),
                             ))
                         }
                         Interaction::None => 
-                            if self.crosshair && cursor.is_over(bounds) {
+                            if chart_state.crosshair && cursor.is_over(bounds) {
                                 Some(Message::CrosshairMoved(cursor_position))
                             } else {
                                 None
@@ -453,12 +409,12 @@ impl canvas::Program<Message> for Footprint {
                 mouse::Event::WheelScrolled { delta } => match delta {
                     mouse::ScrollDelta::Lines { y, .. }
                     | mouse::ScrollDelta::Pixels { y, .. } => {
-                        if y < 0.0 && self.scaling > Self::MIN_SCALING
-                            || y > 0.0 && self.scaling < Self::MAX_SCALING
+                        if y < 0.0 && chart_state.scaling > Self::MIN_SCALING
+                            || y > 0.0 && chart_state.scaling < Self::MAX_SCALING
                         {
                             //let old_scaling = self.scaling;
 
-                            let scaling = (self.scaling * (1.0 + y / 30.0))
+                            let scaling = (chart_state.scaling * (1.0 + y / 30.0))
                                 .clamp(
                                     Self::MIN_SCALING,  // 0.1
                                     Self::MAX_SCALING,  // 2.0
@@ -508,19 +464,17 @@ impl canvas::Program<Message> for Footprint {
         bounds: Rectangle,
         cursor: mouse::Cursor,
     ) -> Vec<Geometry> {    
-        let (latest, earliest) = (self.x_max_time, self.x_min_time);    
-        let (lowest, highest) = (self.y_min_price, self.y_max_price);
+        let chart = self.get_common_data();
 
-        let y_range: f32 = highest - lowest;
+        let (latest, earliest) = (chart.x_max_time, chart.x_min_time);    
+        let (lowest, highest) = (chart.y_min_price, chart.y_max_price);
 
-        if y_range == 0.0 {
-            return vec![];
-        }
+        let y_range: f32 = highest - lowest;
 
         let volume_area_height: f32 = bounds.height / 8.0; 
         let heatmap_area_height: f32 = bounds.height - volume_area_height;
 
-        let heatmap = self.heatmap_cache.draw(renderer, bounds.size(), |frame| {
+        let heatmap = chart.main_cache.draw(renderer, bounds.size(), |frame| {
             let mut max_trade_qty: f32 = 0.0;
             let mut max_volume: f32 = 0.0;
 
@@ -553,8 +507,8 @@ impl canvas::Program<Message> for Footprint {
                 frame.stroke(&wick, Stroke::default().with_color(wick_color).with_width(1.0));
 
                 let body = Path::rectangle(
-                    Point::new(x_position - self.scaling, y_open.min(y_close)), 
-                    Size::new(2.0 * self.scaling, (y_open - y_close).abs())
+                    Point::new(x_position - chart.scaling, y_open.min(y_close)), 
+                    Size::new(2.0 * chart.scaling, (y_open - y_close).abs())
                 );                    
                 frame.fill(&body, body_color);
 
@@ -563,17 +517,17 @@ impl canvas::Program<Message> for Footprint {
                     let y_position = heatmap_area_height - ((price - lowest) / y_range * heatmap_area_height);
 
                     if trade.1.0 > 0.0 {
-                        let bar_width = (trade.1.0 / max_trade_qty) * bounds.width / 28.0 * self.scaling;
+                        let bar_width = (trade.1.0 / max_trade_qty) * bounds.width / 28.0 * chart.scaling;
                         let bar = Path::rectangle(
-                            Point::new(x_position + (3.0 * self.scaling), y_position), 
+                            Point::new(x_position + (3.0 * chart.scaling), y_position), 
                             Size::new(bar_width, 1.0) 
                         );
                         frame.fill(&bar, Color::from_rgba8(81, 205, 160, 1.0));
                     } 
                     if trade.1.1 > 0.0 {
-                        let bar_width = -(trade.1.1 / max_trade_qty) * bounds.width / 28.0 * self.scaling;
+                        let bar_width = -(trade.1.1 / max_trade_qty) * bounds.width / 28.0 * chart.scaling;
                         let bar = Path::rectangle(
-                            Point::new(x_position - (3.0 * self.scaling), y_position), 
+                            Point::new(x_position - (3.0 * chart.scaling), y_position), 
                             Size::new(bar_width, 1.0) 
                         );
                         frame.fill(&bar, Color::from_rgba8(192, 80, 77, 1.0));
@@ -585,8 +539,8 @@ impl canvas::Program<Message> for Footprint {
                         let buy_bar_height = (kline.4 / max_volume) * volume_area_height;
                         let sell_bar_height = (kline.5 / max_volume) * volume_area_height;
 
-                        let sell_bar_width = 8.0 * self.scaling;
-                        let sell_bar_x_position = x_position - (5.0*self.scaling) - sell_bar_width;
+                        let sell_bar_width = 8.0 * chart.scaling;
+                        let sell_bar_x_position = x_position - (5.0*chart.scaling) - sell_bar_width;
                         let sell_bar = Path::rectangle(
                             Point::new(sell_bar_x_position, bounds.height - sell_bar_height), 
                             Size::new(sell_bar_width, sell_bar_height)
@@ -594,15 +548,15 @@ impl canvas::Program<Message> for Footprint {
                         frame.fill(&sell_bar, Color::from_rgb8(192, 80, 77)); 
 
                         let buy_bar = Path::rectangle(
-                            Point::new(x_position + (5.0*self.scaling), bounds.height - buy_bar_height), 
-                            Size::new(8.0 * self.scaling, buy_bar_height)
+                            Point::new(x_position + (5.0*chart.scaling), bounds.height - buy_bar_height), 
+                            Size::new(8.0 * chart.scaling, buy_bar_height)
                         );
                         frame.fill(&buy_bar, Color::from_rgb8(81, 205, 160));
                     } else {
                         let bar_height = (kline.5 / max_volume) * volume_area_height;
                         let bar = Path::rectangle(
-                            Point::new(x_position - (3.0*self.scaling), bounds.height - bar_height), 
-                            Size::new(6.0 * self.scaling, bar_height)
+                            Point::new(x_position - (3.0*chart.scaling), bounds.height - bar_height), 
+                            Size::new(6.0 * chart.scaling, bar_height)
                         );
                         let color = if kline.3 >= kline.0 { Color::from_rgba8(81, 205, 160, 0.8) } else { Color::from_rgba8(192, 80, 77, 0.8) };
 
@@ -626,8 +580,8 @@ impl canvas::Program<Message> for Footprint {
             });
         });
 
-        if self.crosshair {
-            let crosshair = self.crosshair_cache.draw(renderer, bounds.size(), |frame| {
+        if chart.crosshair {
+            let crosshair = chart.crosshair_cache.draw(renderer, bounds.size(), |frame| {
                 if let Some(cursor_position) = cursor.position_in(bounds) {
                     let line = Path::line(
                         Point::new(0.0, cursor_position.y), 
@@ -692,7 +646,7 @@ impl canvas::Program<Message> for Footprint {
             Interaction::Erasing => mouse::Interaction::Crosshair,
             Interaction::Panning { .. } => mouse::Interaction::Grabbing,
             Interaction::None if cursor.is_over(bounds) => {
-                if self.crosshair {
+                if self.chart.crosshair {
                     mouse::Interaction::Crosshair
                 } else {
                     mouse::Interaction::default()
@@ -701,344 +655,4 @@ impl canvas::Program<Message> for Footprint {
             Interaction::None => { mouse::Interaction::default() }
         }
     }
-}
-
-const PRICE_STEPS: [f32; 15] = [
-    1000.0,
-    500.0,
-    200.0,
-    100.0,
-    50.0,
-    20.0,
-    10.0,
-    5.0,
-    2.0,
-    1.0,
-    0.5,
-    0.2,
-    0.1,
-    0.05,
-    0.01,
-];
-fn calculate_price_step(highest: f32, lowest: f32, labels_can_fit: i32) -> (f32, f32) {
-    let range = highest - lowest;
-    let mut step = 1000.0; 
-
-    for &s in PRICE_STEPS.iter().rev() {
-        if range / s <= labels_can_fit as f32 {
-            step = s;
-            break;
-        }
-    }
-    let rounded_lowest = (lowest / step).floor() * step;
-
-    (step, rounded_lowest)
-}
-
-const M1_TIME_STEPS: [i64; 5] = [
-    1000 * 60 * 30, // 30 minutes
-    1000 * 60 * 15, // 15 minutes
-    1000 * 60 * 5, // 5 minutes
-    1000 * 60 * 2, // 2 minutes
-    60 * 1000, // 1 minute
-];
-const M3_TIME_STEPS: [i64; 5] = [
-    1000 * 60 * 60, // 1 hour
-    1000 * 60 * 30, // 30 minutes
-    1000 * 60 * 15, // 15 minutes
-    1000 * 60 * 9, // 9 minutes
-    1000 * 60 * 3, // 3 minutes
-];
-const M5_TIME_STEPS: [i64; 5] = [
-    1000 * 60 * 60, // 1 hour
-    1000 * 60 * 30, // 30 minutes
-    1000 * 60 * 15, // 15 minutes
-    1000 * 60 * 5, // 5 minutes
-    1000 * 60 * 2, // 2 minutes
-];
-const M15_TIME_STEPS: [i64; 5] = [
-    1000 * 60 * 240, // 4 hour
-    1000 * 60 * 120, // 2 hour
-    1000 * 60 * 60, // 1 hour
-    1000 * 60 * 30, // 30 minutes
-    1000 * 60 * 15, // 15 minutes
-];
-const M30_TIME_STEPS: [i64; 5] = [
-    1000 * 60 * 480, // 8 hour
-    1000 * 60 * 240, // 4 hour
-    1000 * 60 * 120, // 2 hour
-    1000 * 60 * 60, // 1 hour
-    1000 * 60 * 30, // 30 minutes
-];
-
-fn calculate_time_step(earliest: i64, latest: i64, labels_can_fit: i32, timeframe: u16) -> (i64, i64) {
-    let duration = latest - earliest;
-
-    let time_steps = match timeframe {
-        1 => &M1_TIME_STEPS,
-        3 => &M3_TIME_STEPS,
-        5 => &M5_TIME_STEPS,
-        15 => &M15_TIME_STEPS,
-        30 => &M30_TIME_STEPS,
-        _ => &M1_TIME_STEPS,
-    };
-
-    let mut selected_step = time_steps[0];
-    for &step in time_steps.iter() {
-        if duration / step >= labels_can_fit as i64 {
-            selected_step = step;
-            break;
-        }
-        if step <= duration {
-            selected_step = step;
-        }
-    }
-
-    let rounded_earliest = (earliest / selected_step) * selected_step;
-
-    (selected_step, rounded_earliest)
-}
-pub struct AxisLabelXCanvas<'a> {
-    labels_cache: &'a Cache,
-    crosshair_cache: &'a Cache,
-    crosshair_position: Point,
-    crosshair: bool,
-    min: i64,
-    max: i64,
-    timeframe: u16,
-}
-impl canvas::Program<Message> for AxisLabelXCanvas<'_> {
-    type State = Interaction;
-
-    fn update(
-        &self,
-        _interaction: &mut Interaction,
-        _event: Event,
-        _bounds: Rectangle,
-        _cursor: mouse::Cursor,
-    ) -> (event::Status, Option<Message>) {
-        (event::Status::Ignored, None)
-    }
-    
-    fn draw(
-        &self,
-        _state: &Self::State,
-        renderer: &Renderer,
-        _theme: &Theme,
-        bounds: Rectangle,
-        _cursor: mouse::Cursor,
-    ) -> Vec<Geometry> {
-        if self.max == 0 {
-            return vec![];
-        }
-        let latest_in_millis = self.max; 
-        let earliest_in_millis = self.min; 
-
-        let x_labels_can_fit = (bounds.width / 120.0) as i32;
-        let (time_step, rounded_earliest) = calculate_time_step(self.min, self.max, x_labels_can_fit, self.timeframe);
-
-        let labels = self.labels_cache.draw(renderer, bounds.size(), |frame| {
-            frame.with_save(|frame| {
-                let mut time: i64 = rounded_earliest;
-                let latest_time: i64 = latest_in_millis;
-
-                while time <= latest_time {                    
-                    let x_position = ((time - earliest_in_millis) as f64 / (latest_in_millis - earliest_in_millis) as f64) * bounds.width as f64;
-
-                    if x_position >= 0.0 && x_position <= bounds.width as f64 {
-                        let text_size = 12.0;
-                        let time_as_datetime = NaiveDateTime::from_timestamp(time / 1000, 0);
-                        let label = canvas::Text {
-                            content: time_as_datetime.format("%H:%M").to_string(),
-                            position: Point::new(x_position as f32 - (text_size*4.0/3.0), bounds.height - 20.0),
-                            size: iced::Pixels(text_size),
-                            color: Color::from_rgba8(200, 200, 200, 1.0),
-                            ..canvas::Text::default()
-                        };  
-
-                        label.draw_with(|path, color| {
-                            frame.fill(&path, color);
-                        });
-                    }
-                    
-                    time += time_step;
-                }
-
-                let line = Path::line(
-                    Point::new(0.0, bounds.height - 30.0), 
-                    Point::new(bounds.width, bounds.height - 30.0)
-                );
-                frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(81, 81, 81, 0.2)).with_width(1.0));
-            });
-        });
-        let crosshair = self.crosshair_cache.draw(renderer, bounds.size(), |frame| {
-            if self.crosshair && self.crosshair_position.x > 0.0 {
-                let crosshair_ratio = self.crosshair_position.x as f64 / bounds.width as f64;
-                let crosshair_millis = earliest_in_millis as f64 + crosshair_ratio * (latest_in_millis - earliest_in_millis) as f64;
-                let crosshair_time = NaiveDateTime::from_timestamp((crosshair_millis / 1000.0) as i64, 0);
-
-                let crosshair_timestamp = crosshair_time.timestamp();
-                let rounded_timestamp = (crosshair_timestamp as f64 / (self.timeframe as f64 * 60.0)).round() as i64 * self.timeframe as i64 * 60;
-                let rounded_time = NaiveDateTime::from_timestamp(rounded_timestamp, 0);
-
-                let snap_ratio = (rounded_timestamp as f64 * 1000.0 - earliest_in_millis as f64) / (latest_in_millis as f64 - earliest_in_millis as f64);
-                let snap_x = snap_ratio * bounds.width as f64;
-
-                let text_size = 12.0;
-                let text_content = rounded_time.format("%H:%M").to_string();
-                let growth_amount = 6.0; 
-                let rectangle_position = Point::new(snap_x as f32 - 14.0 - growth_amount, bounds.height - 20.0);
-                let text_position = Point::new(snap_x as f32 - 14.0, bounds.height - 20.0);
-
-                let text_background = canvas::Path::rectangle(rectangle_position, Size::new(text_content.len() as f32 * text_size/2.0 + 2.0 * growth_amount + 1.0, text_size + text_size/2.0));
-                frame.fill(&text_background, Color::from_rgba8(200, 200, 200, 1.0));
-
-                let crosshair_label = canvas::Text {
-                    content: text_content,
-                    position: text_position,
-                    size: iced::Pixels(text_size),
-                    color: Color::from_rgba8(0, 0, 0, 1.0),
-                    ..canvas::Text::default()
-                };
-
-                crosshair_label.draw_with(|path, color| {
-                    frame.fill(&path, color);
-                });
-            }
-        });
-
-        vec![labels, crosshair]
-    }
-
-    fn mouse_interaction(
-        &self,
-        interaction: &Interaction,
-        bounds: Rectangle,
-        cursor: mouse::Cursor,
-    ) -> mouse::Interaction {
-        match interaction {
-            Interaction::Drawing => mouse::Interaction::Crosshair,
-            Interaction::Erasing => mouse::Interaction::Crosshair,
-            Interaction::Panning { .. } => mouse::Interaction::ResizingHorizontally,
-            Interaction::None if cursor.is_over(bounds) => {
-                mouse::Interaction::ResizingHorizontally
-            }
-            Interaction::None => mouse::Interaction::default(),
-        }
-    }
-}
-
-pub struct AxisLabelYCanvas<'a> {
-    labels_cache: &'a Cache,
-    y_croshair_cache: &'a Cache,
-    min: f32,
-    max: f32,
-    crosshair_position: Point,
-    crosshair: bool,
-}
-impl canvas::Program<Message> for AxisLabelYCanvas<'_> {
-    type State = Interaction;
-
-    fn update(
-        &self,
-        _interaction: &mut Interaction,
-        _event: Event,
-        _bounds: Rectangle,
-        _cursor: mouse::Cursor,
-    ) -> (event::Status, Option<Message>) {
-        (event::Status::Ignored, None)
-    }
-    
-    fn draw(
-        &self,
-        _state: &Self::State,
-        renderer: &Renderer,
-        _theme: &Theme,
-        bounds: Rectangle,
-        _cursor: mouse::Cursor,
-    ) -> Vec<Geometry> {
-        if self.max == 0.0 {
-            return vec![];
-        }
-
-        let y_labels_can_fit = (bounds.height / 32.0) as i32;
-        let (step, rounded_lowest) = calculate_price_step(self.max, self.min, y_labels_can_fit);
-
-        let volume_area_height = bounds.height / 8.0; 
-        let candlesticks_area_height = bounds.height - volume_area_height;
-
-        let labels = self.labels_cache.draw(renderer, bounds.size(), |frame| {
-            frame.with_save(|frame| {
-                let y_range = self.max - self.min;
-                let mut y = rounded_lowest;
-
-                while y <= self.max {
-                    let y_position = candlesticks_area_height - ((y - self.min) / y_range * candlesticks_area_height);
-
-                    let text_size = 12.0;
-                    let decimal_places = if step < 0.5 { 2 } else { usize::from(step < 1.0) };
-                    let label_content = format!("{y:.decimal_places$}");
-                    let label = canvas::Text {
-                        content: label_content,
-                        position: Point::new(10.0, y_position - text_size / 2.0),
-                        size: iced::Pixels(text_size),
-                        color: Color::from_rgba8(200, 200, 200, 1.0),
-                        ..canvas::Text::default()
-                    };  
-
-                    label.draw_with(|path, color| {
-                        frame.fill(&path, color);
-                    });
-
-                    y += step;
-                }
-            });
-        });
-        let crosshair = self.y_croshair_cache.draw(renderer, bounds.size(), |frame| {
-            if self.crosshair && self.crosshair_position.y > 0.0 {
-                let text_size = 12.0;
-                let y_range = self.max - self.min;
-                let decimal_places = if step < 1.0 { 2 } else { 1 };
-                let label_content = format!("{:.*}", decimal_places, self.min + (y_range * (candlesticks_area_height - self.crosshair_position.y) / candlesticks_area_height));
-                
-                let growth_amount = 3.0; 
-                let rectangle_position = Point::new(8.0 - growth_amount, self.crosshair_position.y - text_size / 2.0 - 3.0);
-                let text_position = Point::new(8.0, self.crosshair_position.y - text_size / 2.0 - 3.0);
-
-                let text_background = canvas::Path::rectangle(rectangle_position, Size::new(label_content.len() as f32 * text_size / 2.0 + 2.0 * growth_amount + 4.0, text_size + text_size / 1.8));
-                frame.fill(&text_background, Color::from_rgba8(200, 200, 200, 1.0));
-
-                let label = canvas::Text {
-                    content: label_content,
-                    position: text_position,
-                    size: iced::Pixels(text_size),
-                    color: Color::from_rgba8(0, 0, 0, 1.0),
-                    ..canvas::Text::default()
-                };
-
-                label.draw_with(|path, color| {
-                    frame.fill(&path, color);
-                });
-            }
-        });
-
-        vec![labels, crosshair]
-    }
-
-    fn mouse_interaction(
-        &self,
-        interaction: &Interaction,
-        bounds: Rectangle,
-        cursor: mouse::Cursor,
-    ) -> mouse::Interaction {
-        match interaction {
-            Interaction::Drawing => mouse::Interaction::Crosshair,
-            Interaction::Erasing => mouse::Interaction::Crosshair,
-            Interaction::Panning { .. } => mouse::Interaction::ResizingVertically,
-            Interaction::None if cursor.is_over(bounds) => {
-                mouse::Interaction::ResizingVertically
-            }
-            Interaction::None => mouse::Interaction::default(),
-        }
-    }
 }

+ 144 - 563
src/charts/heatmap.rs

@@ -1,78 +1,43 @@
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, HashMap};
 use chrono::NaiveDateTime;
 use iced::{
-    alignment, mouse, widget::{button, canvas::{self, event::{self, Event}, stroke::Stroke, Cache, Canvas, Geometry, Path}}, window, Border, Color, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector
+    alignment, color, mouse, widget::{button, canvas::{self, event::{self, Event}, stroke::Stroke, Cache, Canvas, Geometry, Path}}, window, Border, Color, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector
 };
 use iced::widget::{Column, Row, Container, Text};
 use crate::data_providers::binance::market_data::{LocalDepthCache, Trade};
 
-#[derive(Debug, Clone, Copy)]
-pub enum Message {
-    Translated(Vector),
-    Scaled(f32, Option<Vector>),
-    ChartBounds(Rectangle),
-    AutoscaleToggle,
-    CrosshairToggle,
-    CrosshairMoved(Point),
-    YScaling(f32),
-}
+use super::{Chart, CommonChartData, Message, chart_button, Interaction, AxisLabelYCanvas, AxisLabelXCanvas};
 
-#[derive(Debug)]
-pub struct Heatmap {
-    heatmap_cache: Cache,
-    crosshair_cache: Cache,
-    x_labels_cache: Cache,
-    y_labels_cache: Cache,
-    y_croshair_cache: Cache,
-    x_crosshair_cache: Cache,
-    translation: Vector,
-    scaling: f32,
-    y_scaling: f32,
-    
+pub struct HeatmapChart {
+    chart: CommonChartData,
     data_points: BTreeMap<i64, (LocalDepthCache, Box<[Trade]>)>,
+    tick_size: f32,
+    y_scaling: f32,
     size_filter: f32,
+}
 
-    autoscale: bool,
-    crosshair: bool,
-    crosshair_position: Point,
-    x_min_time: i64,
-    x_max_time: i64,
-    y_min_price: f32,
-    y_max_price: f32,
-    bounds: Rectangle,
+impl Chart for HeatmapChart {
+    type DataPoint = BTreeMap<i64, (LocalDepthCache, Box<[Trade]>)>;
+
+    fn get_common_data(&self) -> &CommonChartData {
+        &self.chart
+    }
+    fn get_common_data_mut(&mut self) -> &mut CommonChartData {
+        &mut self.chart
+    }
 }
-impl Heatmap {
+
+impl HeatmapChart {
     const MIN_SCALING: f32 = 0.6;
     const MAX_SCALING: f32 = 3.6;
 
-    const THREE_MIN: i64 = 3 * 60 * 1000;
-    const ONE_MIN: i64 = 1 * 60 * 1000;
-
-    pub fn new() -> Heatmap {
-        let _size = window::Settings::default().size;
-    
-        Heatmap {
-            heatmap_cache: canvas::Cache::default(),
-            crosshair_cache: canvas::Cache::default(),
-            x_labels_cache: canvas::Cache::default(),
-            y_labels_cache: canvas::Cache::default(),
-            y_croshair_cache: canvas::Cache::default(),
-            x_crosshair_cache: canvas::Cache::default(),
-
+    pub fn new() -> Self {
+        HeatmapChart {
+            chart: CommonChartData::default(),
             data_points: BTreeMap::new(),
-            size_filter: 0.0,
-
-            translation: Vector::default(),
-            scaling: 1.0,
+            tick_size: 0.0,
             y_scaling: 0.0001,
-            autoscale: true,
-            crosshair: false,
-            crosshair_position: Point::new(0.0, 0.0),
-            x_min_time: 0,
-            x_max_time: 0,
-            y_min_price: 0.0,
-            y_max_price: 0.0,
-            bounds: Rectangle::default(),
+            size_filter: 0.0,
         }
     }
 
@@ -80,12 +45,22 @@ impl Heatmap {
         self.size_filter = size_filter;
     }
 
+    pub fn get_raw_trades(&mut self) -> Vec<Trade> {
+        let mut trades_source = vec![];
+
+        for (_, trades) in self.data_points.values() {
+            trades_source.extend(trades.iter().cloned());
+        }
+
+        trades_source
+    }
+
     pub fn insert_datapoint(&mut self, trades_buffer: Vec<Trade>, depth_update: i64, depth: LocalDepthCache) {
         let aggregate_time = 100; // 100 ms
         let rounded_depth_update = (depth_update / aggregate_time) * aggregate_time;
         
         self.data_points.entry(rounded_depth_update).or_insert((depth, trades_buffer.into_boxed_slice()));
-
+        
         if self.data_points.len() > 3600 {
             while let Some((&key_to_remove, _)) = self.data_points.iter().next() {
                 self.data_points.remove(&key_to_remove);
@@ -93,117 +68,116 @@ impl Heatmap {
                     break;
                 }
             }
-        }
+        }     
 
         self.render_start();
     }
 
-    pub fn get_raw_trades(&mut self) -> Vec<Trade> {
-        let mut trades_source = vec![];
+    pub fn render_start(&mut self) {  
+        let (latest, earliest, highest, lowest) = self.calculate_range();
 
-        for (_, trades) in self.data_points.values() {
-            trades_source.extend(trades.iter().cloned());
+        if latest == 0 || highest == 0.0 {
+            return;
         }
 
-        trades_source
-    }
-    
-    pub fn render_start(&mut self) {    
-        self.heatmap_cache.clear();
+        let chart_state = self.get_common_data_mut();
 
-        let timestamp_latest: &i64 = self.data_points.keys().last().unwrap_or(&0);
+        if earliest != chart_state.x_min_time || latest != chart_state.x_max_time {         
+            chart_state.x_min_time = earliest;
+            chart_state.x_max_time = latest;
 
-        let latest: i64 = *timestamp_latest - (self.translation.x*80.0) as i64;
-        let earliest: i64 = latest - (64000.0 / (self.scaling / (self.bounds.width/800.0))) as i64;
-            
-        if self.data_points.len() > 1 {
-            let mut max_ask_price = f32::MIN;
-            let mut min_bid_price = f32::MAX;
-
-            for (_, (depth, _)) in self.data_points.range(earliest..=latest) {
-                if !depth.asks.is_empty() && !depth.bids.is_empty() {        
-                    let ask_price: f32 = depth.asks[std::cmp::min(20, depth.asks.len() - 1)].price;
-                    let bid_price: f32 = depth.bids[std::cmp::min(20, depth.bids.len() - 1)].price;
+            chart_state.x_labels_cache.clear();
+            chart_state.x_crosshair_cache.clear();
+        };
+
+        if lowest != chart_state.y_min_price || highest != chart_state.y_max_price {   
+            chart_state.y_min_price = lowest;
+            chart_state.y_max_price = highest;
+
+            chart_state.y_labels_cache.clear();
+            chart_state.y_crosshair_cache.clear();
+        };
         
-                    if ask_price > max_ask_price {
-                        max_ask_price = ask_price;
-                    };
-                    if bid_price < min_bid_price {
-                        min_bid_price = bid_price;
-                    };
-                };
-            };
+        chart_state.crosshair_cache.clear();     
+        chart_state.main_cache.clear();   
+    }
 
-            let lowest = min_bid_price - (min_bid_price * self.y_scaling);
-            let highest = max_ask_price + (max_ask_price * self.y_scaling);
+    fn calculate_range(&self) -> (i64, i64, f32, f32) {
+        let timestamp_latest: &i64 = self.data_points.keys().last().unwrap_or(&0);
 
-            if lowest != self.y_min_price || highest != self.y_max_price {   
-                self.y_min_price = lowest;
-                self.y_max_price = highest;
+        let latest: i64 = *timestamp_latest - (self.chart.translation.x*80.0) as i64;
+        let earliest: i64 = latest - (64000.0 / (self.chart.scaling / (self.chart.bounds.width/800.0))) as i64;
+    
+        let mut max_ask_price = f32::MIN;
+        let mut min_bid_price = f32::MAX;
 
-                self.y_labels_cache.clear();
-                self.y_croshair_cache.clear();
+        for (_, (depth, _)) in self.data_points.range(earliest..=latest) {
+            if !depth.asks.is_empty() && !depth.bids.is_empty() {        
+                let ask_price: f32 = depth.asks[std::cmp::min(20, depth.asks.len() - 1)].price;
+                let bid_price: f32 = depth.bids[std::cmp::min(20, depth.bids.len() - 1)].price;
+    
+                if ask_price > max_ask_price {
+                    max_ask_price = ask_price;
+                };
+                if bid_price < min_bid_price {
+                    min_bid_price = bid_price;
+                };
             };
         };
 
-        if earliest != self.x_min_time || latest != self.x_max_time {         
-            self.x_min_time = earliest;
-            self.x_max_time = latest;
-
-            self.x_labels_cache.clear();
-            self.x_crosshair_cache.clear();
-        };
-        
-        self.crosshair_cache.clear();        
+        (latest, earliest, max_ask_price, min_bid_price)
     }
 
     pub fn update(&mut self, message: &Message) {
         match message {
             Message::Translated(translation) => {
-                if self.autoscale {
-                    self.translation.x = translation.x;
+                let chart = self.get_common_data_mut();
+
+                if chart.autoscale {
+                    chart.translation.x = translation.x;
                 } else {
-                    self.translation = *translation;
+                    chart.translation = *translation;
                 }
-                self.crosshair_position = Point::new(0.0, 0.0);
+                chart.crosshair_position = Point::new(0.0, 0.0);
 
                 self.render_start();
-            }
+            },
             Message::Scaled(scaling, translation) => {
-                self.scaling = *scaling;
+                let chart = self.get_common_data_mut();
+
+                chart.scaling = *scaling;
                 
                 if let Some(translation) = translation {
-                    if self.autoscale {
-                        self.translation.x = translation.x;
+                    if chart.autoscale {
+                        chart.translation.x = translation.x;
                     } else {
-                        self.translation = *translation;
+                        chart.translation = *translation;
                     }
                 }
-                self.crosshair_position = Point::new(0.0, 0.0);
+                chart.crosshair_position = Point::new(0.0, 0.0);
 
                 self.render_start();
-            }
+            },
             Message::ChartBounds(bounds) => {
-                self.bounds = *bounds;
-            }
+                self.chart.bounds = *bounds;
+            },
             Message::AutoscaleToggle => {
-                self.autoscale = !self.autoscale;
-            }
+                self.chart.autoscale = !self.chart.autoscale;
+            },
             Message::CrosshairToggle => {
-                self.crosshair = !self.crosshair;
-            }
+                self.chart.crosshair = !self.chart.crosshair;
+            },
             Message::CrosshairMoved(position) => {
-                self.crosshair_position = *position;
-                if self.crosshair {
-                    self.crosshair_cache.clear();
-                    self.y_croshair_cache.clear();
-                    self.x_crosshair_cache.clear();
+                let chart = self.get_common_data_mut();
+
+                chart.crosshair_position = *position;
+                if chart.crosshair {
+                    chart.crosshair_cache.clear();
+                    chart.y_crosshair_cache.clear();
+                    chart.x_crosshair_cache.clear();
                 }
-            }
-            Message::YScaling(scaling) => {
-                self.y_scaling = *scaling;
-                self.render_start();
-            }
+            },
+            _ => {}
         }
     }
 
@@ -211,28 +185,30 @@ impl Heatmap {
         let chart = Canvas::new(self)
             .width(Length::FillPortion(10))
             .height(Length::FillPortion(10));
+
+        let chart_state = self.get_common_data();
         
         let axis_labels_x = Canvas::new(
             AxisLabelXCanvas { 
-                labels_cache: &self.x_labels_cache, 
-                min: self.x_min_time, 
-                max: self.x_max_time, 
-                crosshair_cache: &self.x_crosshair_cache, 
-                crosshair_position: self.crosshair_position, 
-                crosshair: self.crosshair,
+                labels_cache: &chart_state.x_labels_cache, 
+                min: chart_state.x_min_time, 
+                max: chart_state.x_max_time, 
+                crosshair_cache: &chart_state.x_crosshair_cache, 
+                crosshair_position: chart_state.crosshair_position, 
+                crosshair: chart_state.crosshair,
+                timeframe: None,
             })
             .width(Length::FillPortion(10))
             .height(Length::Fixed(26.0));
 
         let axis_labels_y = Canvas::new(
             AxisLabelYCanvas { 
-                labels_cache: &self.y_labels_cache, 
-                y_croshair_cache: &self.y_croshair_cache, 
-                min: self.y_min_price,
-                max: self.y_max_price,
-                crosshair_position: self.crosshair_position, 
-                crosshair: self.crosshair,
-                y_scaling: self.y_scaling,
+                labels_cache: &chart_state.y_labels_cache, 
+                y_croshair_cache: &chart_state.y_crosshair_cache, 
+                min: chart_state.y_min_price,
+                max: chart_state.y_max_price,
+                crosshair_position: chart_state.crosshair_position, 
+                crosshair: chart_state.crosshair,
             })
             .width(Length::Fixed(60.0))
             .height(Length::FillPortion(10));
@@ -245,7 +221,7 @@ impl Heatmap {
             .width(Length::Fill)
             .height(Length::Fill)
             .on_press(Message::AutoscaleToggle)
-            .style(|_theme: &Theme, _status: iced::widget::button::Status| chart_button(_theme, _status, self.autoscale));
+            .style(|_theme: &Theme, _status: iced::widget::button::Status| chart_button(_theme, _status, chart_state.autoscale));
         let crosshair_button = button(
             Text::new("+")
                 .size(12)
@@ -254,7 +230,7 @@ impl Heatmap {
             .width(Length::Fill)
             .height(Length::Fill)
             .on_press(Message::CrosshairToggle)
-            .style(|_theme: &Theme, _status: iced::widget::button::Status| chart_button(_theme, _status, self.crosshair));
+            .style(|_theme: &Theme, _status: iced::widget::button::Status| chart_button(_theme, _status, chart_state.crosshair));
     
         let chart_controls = Container::new(
             Row::new()
@@ -282,39 +258,7 @@ impl Heatmap {
     }
 }
 
-fn chart_button(_theme: &Theme, _status: button::Status, is_active: bool) -> button::Style {
-    button::Style {
-        background: Some(Color::from_rgba8(20, 20, 20, 1.0).into()),
-        border: Border {
-            color: {
-                if is_active {
-                    Color::from_rgba8(50, 50, 50, 1.0)
-                } else {
-                    Color::from_rgba8(20, 20, 20, 1.0)
-                }
-            },
-            width: 1.0,
-            radius: 2.0.into(),
-        },
-        text_color: Color::WHITE,
-        ..button::Style::default()
-    }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub enum Interaction {
-    None,
-    Drawing,
-    Erasing,
-    Panning { translation: Vector, start: Point },
-}
-
-impl Default for Interaction {
-    fn default() -> Self {
-        Self::None
-    }
-}
-impl canvas::Program<Message> for Heatmap {
+impl canvas::Program<Message> for HeatmapChart {
     type State = Interaction;
 
     fn update(
@@ -323,8 +267,10 @@ impl canvas::Program<Message> for Heatmap {
         event: Event,
         bounds: Rectangle,
         cursor: mouse::Cursor,
-    ) -> (event::Status, Option<Message>) {        
-        if bounds != self.bounds {
+    ) -> (event::Status, Option<Message>) {
+        let chart_state = self.get_common_data();
+
+        if bounds != chart_state.bounds {
             return (event::Status::Ignored, Some(Message::ChartBounds(bounds)));
         } 
     
@@ -334,7 +280,7 @@ impl canvas::Program<Message> for Heatmap {
 
         let Some(cursor_position) = cursor.position_in(bounds) else {
             return (event::Status::Ignored, 
-                if self.crosshair {
+                if chart_state.crosshair {
                     Some(Message::CrosshairMoved(Point::new(0.0, 0.0)))
                 } else {
                     None
@@ -352,7 +298,7 @@ impl canvas::Program<Message> for Heatmap {
                         }
                         mouse::Button::Left => {
                             *interaction = Interaction::Panning {
-                                translation: self.translation,
+                                translation: chart_state.translation,
                                 start: cursor_position,
                             };
                             None
@@ -370,11 +316,11 @@ impl canvas::Program<Message> for Heatmap {
                             Some(Message::Translated(
                                 translation
                                     + (cursor_position - start)
-                                        * (1.0 / self.scaling),
+                                        * (1.0 / chart_state.scaling),
                             ))
                         }
                         Interaction::None => 
-                            if self.crosshair && cursor.is_over(bounds) {
+                            if chart_state.crosshair && cursor.is_over(bounds) {
                                 Some(Message::CrosshairMoved(cursor_position))
                             } else {
                                 None
@@ -391,12 +337,12 @@ impl canvas::Program<Message> for Heatmap {
                 mouse::Event::WheelScrolled { delta } => match delta {
                     mouse::ScrollDelta::Lines { y, .. }
                     | mouse::ScrollDelta::Pixels { y, .. } => {
-                        if y < 0.0 && self.scaling > Self::MIN_SCALING
-                            || y > 0.0 && self.scaling < Self::MAX_SCALING
+                        if y < 0.0 && chart_state.scaling > Self::MIN_SCALING
+                            || y > 0.0 && chart_state.scaling < Self::MAX_SCALING
                         {
                             //let old_scaling = self.scaling;
 
-                            let scaling = (self.scaling * (1.0 + y / 30.0))
+                            let scaling = (chart_state.scaling * (1.0 + y / 30.0))
                                 .clamp(
                                     Self::MIN_SCALING, 
                                     Self::MAX_SCALING,  
@@ -446,8 +392,10 @@ impl canvas::Program<Message> for Heatmap {
         bounds: Rectangle,
         cursor: mouse::Cursor,
     ) -> Vec<Geometry> {    
-        let (latest, earliest) = (self.x_max_time, self.x_min_time);    
-        let (lowest, highest) = (self.y_min_price, self.y_max_price);
+        let chart = self.get_common_data();
+
+        let (latest, earliest) = (chart.x_max_time, chart.x_min_time);    
+        let (lowest, highest) = (chart.y_min_price, chart.y_max_price);
 
         let y_range: f32 = highest - lowest;
         
@@ -456,7 +404,7 @@ impl canvas::Program<Message> for Heatmap {
 
         let depth_area_width: f32 = bounds.width / 20.0;
 
-        let heatmap = self.heatmap_cache.draw(renderer, bounds.size(), |frame| {
+        let heatmap = chart.main_cache.draw(renderer, bounds.size(), |frame| {
             let (mut min_trade_qty, mut max_trade_qty) = (f32::MAX, 0.0f32);
 
             let mut max_volume: f32 = 0.0;
@@ -674,8 +622,8 @@ impl canvas::Program<Message> for Heatmap {
             };
         });
 
-        if self.crosshair {
-            let crosshair = self.crosshair_cache.draw(renderer, bounds.size(), |frame| {
+        if chart.crosshair {
+            let crosshair = chart.crosshair_cache.draw(renderer, bounds.size(), |frame| {
                 if let Some(cursor_position) = cursor.position_in(bounds) {
                     let line = Path::line(
                         Point::new(0.0, cursor_position.y), 
@@ -717,7 +665,7 @@ impl canvas::Program<Message> for Heatmap {
             Interaction::Erasing => mouse::Interaction::Crosshair,
             Interaction::Panning { .. } => mouse::Interaction::Grabbing,
             Interaction::None if cursor.is_over(bounds) => {
-                if self.crosshair {
+                if self.chart.crosshair {
                     mouse::Interaction::Crosshair
                 } else {
                     mouse::Interaction::default()
@@ -726,371 +674,4 @@ impl canvas::Program<Message> for Heatmap {
             Interaction::None => { mouse::Interaction::default() }
         }
     }
-}
-
-const PRICE_STEPS: [f32; 15] = [
-    1000.0,
-    500.0,
-    200.0,
-    100.0,
-    50.0,
-    20.0,
-    10.0,
-    5.0,
-    2.0,
-    1.0,
-    0.5,
-    0.2,
-    0.1,
-    0.05,
-    0.01,
-];
-fn calculate_price_step(highest: f32, lowest: f32, labels_can_fit: i32) -> (f32, f32) {
-    let range = highest - lowest;
-    let mut step = 1000.0; 
-
-    for &s in PRICE_STEPS.iter().rev() {
-        if range / s <= labels_can_fit as f32 {
-            step = s;
-            break;
-        }
-    }
-    let rounded_lowest = (lowest / step).floor() * step;
-
-    (step, rounded_lowest)
-}
-
-const TIME_STEPS: [i64; 8] = [
-    60 * 1000, // 1 minute
-    30 * 1000, // 30 seconds
-    15 * 1000, // 15 seconds
-    10 * 1000, // 10 seconds
-    5 * 1000,  // 5 seconds
-    2 * 1000,  // 2 seconds
-    1000,  // 1 second
-    500,       // 500 milliseconds
-];
-fn calculate_time_step(earliest: i64, latest: i64, labels_can_fit: i32) -> (i64, i64) {
-    let duration = latest - earliest;
-
-    let mut selected_step = TIME_STEPS[0];
-    for &step in &TIME_STEPS {
-        if duration / step >= labels_can_fit as i64 {
-            selected_step = step;
-            break;
-        }
-    }
-
-    let rounded_earliest = (earliest / selected_step) * selected_step;
-
-    (selected_step, rounded_earliest)
-}
-
-pub struct AxisLabelXCanvas<'a> {
-    labels_cache: &'a Cache,
-    crosshair_cache: &'a Cache,
-    crosshair_position: Point,
-    crosshair: bool,
-    min: i64,
-    max: i64,
-}
-impl canvas::Program<Message> for AxisLabelXCanvas<'_> {
-    type State = Interaction;
-
-    fn update(
-        &self,
-        _interaction: &mut Interaction,
-        _event: Event,
-        _bounds: Rectangle,
-        _cursor: mouse::Cursor,
-    ) -> (event::Status, Option<Message>) {
-        (event::Status::Ignored, None)
-    }
-    
-    fn draw(
-        &self,
-        _state: &Self::State,
-        renderer: &Renderer,
-        _theme: &Theme,
-        bounds: Rectangle,
-        _cursor: mouse::Cursor,
-    ) -> Vec<Geometry> {
-        if self.max == 0 {
-            return vec![];
-        }
-        let latest_in_millis = self.max; 
-        let earliest_in_millis = self.min; 
-
-        let x_labels_can_fit = (bounds.width / 120.0) as i32;
-        let (time_step, rounded_earliest) = calculate_time_step(self.min, self.max, x_labels_can_fit);
-
-        let labels = self.labels_cache.draw(renderer, bounds.size(), |frame| {
-            frame.with_save(|frame| {
-                let mut time: i64 = rounded_earliest;
-                let latest_time: i64 = latest_in_millis;
-
-                while time <= latest_time {                    
-                    let x_position = ((time - earliest_in_millis) as f64 / (latest_in_millis - earliest_in_millis) as f64) * bounds.width as f64;
-
-                    if x_position >= 0.0 && x_position <= bounds.width as f64 {
-                        let text_size = 12.0;
-                        let time_as_datetime = NaiveDateTime::from_timestamp(time / 1000, 0);
-                        let label = canvas::Text {
-                            content: time_as_datetime.format("%M:%S").to_string(),
-                            position: Point::new(x_position as f32 - text_size, bounds.height - 20.0),
-                            size: iced::Pixels(text_size),
-                            color: Color::from_rgba8(200, 200, 200, 1.0),
-                            ..canvas::Text::default()
-                        };  
-
-                        label.draw_with(|path, color| {
-                            frame.fill(&path, color);
-                        });
-                    }
-                    
-                    time += time_step;
-                }
-
-                let line = Path::line(
-                    Point::new(0.0, bounds.height - 30.0), 
-                    Point::new(bounds.width, bounds.height - 30.0)
-                );
-                frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(81, 81, 81, 0.2)).with_width(1.0));
-            });
-        });
-        let crosshair = self.crosshair_cache.draw(renderer, bounds.size(), |frame| {
-            if self.crosshair && self.crosshair_position.x > 0.0 {
-                let crosshair_ratio = self.crosshair_position.x as f64 / bounds.width as f64;
-                let crosshair_millis = (earliest_in_millis as f64 + crosshair_ratio * (latest_in_millis as f64 - earliest_in_millis as f64)).round() / 100.0 * 100.0;
-                let crosshair_time = NaiveDateTime::from_timestamp((crosshair_millis / 1000.0).floor() as i64, ((crosshair_millis % 1000.0) * 1_000_000.0).round() as u32);
-                
-                let crosshair_timestamp = crosshair_time.timestamp_millis();
-
-                let snap_ratio = (crosshair_timestamp as f64 - earliest_in_millis as f64) / (latest_in_millis as f64 - earliest_in_millis as f64);
-                let snap_x = snap_ratio * bounds.width as f64;
-
-                let text_size = 12.0;
-                let text_content = crosshair_time.format("%M:%S:%3f").to_string().replace('.', "");
-                let growth_amount = 6.0; 
-                let rectangle_position = Point::new(snap_x as f32 - 26.0 - growth_amount, bounds.height - 20.0);
-                let text_position = Point::new(snap_x as f32 - 26.0, bounds.height - 20.0);
-
-                let text_background = canvas::Path::rectangle(rectangle_position, Size::new(text_content.len() as f32 * text_size/2.0 + 2.0 * growth_amount, text_size + text_size/2.0));
-                frame.fill(&text_background, Color::from_rgba8(200, 200, 200, 1.0));
-
-                let crosshair_label = canvas::Text {
-                    content: text_content,
-                    position: text_position,
-                    size: iced::Pixels(text_size),
-                    color: Color::from_rgba8(0, 0, 0, 1.0),
-                    ..canvas::Text::default()
-                };
-
-                crosshair_label.draw_with(|path, color| {
-                    frame.fill(&path, color);
-                });
-            }
-        });
-
-        vec![labels, crosshair]
-    }
-
-    fn mouse_interaction(
-        &self,
-        interaction: &Interaction,
-        bounds: Rectangle,
-        cursor: mouse::Cursor,
-    ) -> mouse::Interaction {
-        match interaction {
-            Interaction::Drawing => mouse::Interaction::Crosshair,
-            Interaction::Erasing => mouse::Interaction::Crosshair,
-            Interaction::Panning { .. } => mouse::Interaction::ResizingHorizontally,
-            Interaction::None if cursor.is_over(bounds) => {
-                mouse::Interaction::ResizingHorizontally
-            }
-            Interaction::None => mouse::Interaction::default(),
-        }
-    }
-}
-
-pub struct AxisLabelYCanvas<'a> {
-    labels_cache: &'a Cache,
-    y_croshair_cache: &'a Cache,
-    min: f32,
-    max: f32,
-    crosshair_position: Point,
-    crosshair: bool,
-    y_scaling: f32,
-}
-impl canvas::Program<Message> for AxisLabelYCanvas<'_> {
-    type State = Interaction;
-
-    fn update(
-        &self,
-        interaction: &mut Interaction,
-        event: Event,
-        bounds: Rectangle,
-        cursor: mouse::Cursor,
-    ) -> (event::Status, Option<Message>) {        
-        if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event {
-            *interaction = Interaction::None;
-        }
-
-        if !cursor.is_over(bounds) {
-            return (event::Status::Ignored, None);
-        };
-
-        match event {
-            Event::Mouse(mouse_event) => match mouse_event {
-                mouse::Event::ButtonPressed(button) => {
-                    let message = match button {
-                        mouse::Button::Right => {
-                            *interaction = Interaction::Drawing;
-                            None
-                        }
-                        mouse::Button::Left => {
-                            None
-                        }
-                        _ => None,
-                    };
-
-                    (event::Status::Captured, message)
-                }
-                mouse::Event::CursorMoved { .. } => {
-                    let message = match *interaction {
-                        Interaction::Drawing => None,
-                        Interaction::Erasing => None,
-                        Interaction::Panning { translation, start } => {
-                            None
-                        }
-                        Interaction::None => 
-                            None
-                    };
-
-                    let event_status = match interaction {
-                        Interaction::None => event::Status::Ignored,
-                        _ => event::Status::Captured,
-                    };
-
-                    (event_status, message)
-                }
-                mouse::Event::WheelScrolled { delta } => match delta {
-                    mouse::ScrollDelta::Lines { y, .. }
-                    | mouse::ScrollDelta::Pixels { y, .. } => {
-                        if y > 0.0 && self.y_scaling > 0.00001
-                            || y < 0.0 && self.y_scaling < 0.001
-                        {
-                            let scaling = (self.y_scaling * (1.0 - y / 30.0))
-                                .clamp(
-                                    0.00001, 
-                                    0.001,  
-                                );
-
-                            (
-                                event::Status::Captured,
-                                Some(Message::YScaling(scaling)),
-                            )
-                        } else {
-                            (event::Status::Captured, None)
-                        }
-                    }
-                },
-                _ => (event::Status::Ignored, None),
-            },
-            _ => (event::Status::Ignored, None),
-        }
-    }
-    
-    fn draw(
-        &self,
-        _state: &Self::State,
-        renderer: &Renderer,
-        _theme: &Theme,
-        bounds: Rectangle,
-        _cursor: mouse::Cursor,
-    ) -> Vec<Geometry> {
-        if self.max == 0.0 {
-            return vec![];
-        }
-
-        let y_labels_can_fit = (bounds.height / 32.0) as i32;
-        let (step, rounded_lowest) = calculate_price_step(self.max, self.min, y_labels_can_fit);
-
-        let volume_area_height = bounds.height / 8.0; 
-        let candlesticks_area_height = bounds.height - volume_area_height;
-
-        let labels = self.labels_cache.draw(renderer, bounds.size(), |frame| {
-            frame.with_save(|frame| {
-                let y_range = self.max - self.min;
-                let mut y = rounded_lowest;
-
-                while y <= self.max {
-                    let y_position = candlesticks_area_height - ((y - self.min) / y_range * candlesticks_area_height);
-
-                    let text_size = 12.0;
-                    let decimal_places = if step < 0.5 { 2 } else { usize::from(step < 1.0) };
-                    let label_content = format!("{y:.decimal_places$}");
-                    let label = canvas::Text {
-                        content: label_content,
-                        position: Point::new(10.0, y_position - text_size / 2.0),
-                        size: iced::Pixels(text_size),
-                        color: Color::from_rgba8(200, 200, 200, 1.0),
-                        ..canvas::Text::default()
-                    };  
-
-                    label.draw_with(|path, color| {
-                        frame.fill(&path, color);
-                    });
-
-                    y += step;
-                }
-            });
-        });
-        let crosshair = self.y_croshair_cache.draw(renderer, bounds.size(), |frame| {
-            if self.crosshair && self.crosshair_position.y > 0.0 {
-                let text_size = 12.0;
-                let y_range = self.max - self.min;
-                let decimal_places = if step < 1.0 { 2 } else { 1 };
-                let label_content = format!("{:.*}", decimal_places, self.min + (y_range * (candlesticks_area_height - self.crosshair_position.y) / candlesticks_area_height));
-                
-                let growth_amount = 3.0; 
-                let rectangle_position = Point::new(8.0 - growth_amount, self.crosshair_position.y - text_size / 2.0 - 3.0);
-                let text_position = Point::new(8.0, self.crosshair_position.y - text_size / 2.0 - 3.0);
-
-                let text_background = canvas::Path::rectangle(rectangle_position, Size::new(label_content.len() as f32 * text_size / 2.0 + 2.0 * growth_amount + 4.0, text_size + text_size / 1.8));
-                frame.fill(&text_background, Color::from_rgba8(200, 200, 200, 1.0));
-
-                let label = canvas::Text {
-                    content: label_content,
-                    position: text_position,
-                    size: iced::Pixels(text_size),
-                    color: Color::from_rgba8(0, 0, 0, 1.0),
-                    ..canvas::Text::default()
-                };
-
-                label.draw_with(|path, color| {
-                    frame.fill(&path, color);
-                });
-            }
-        });
-
-        vec![labels, crosshair]
-    }
-
-    fn mouse_interaction(
-        &self,
-        interaction: &Interaction,
-        bounds: Rectangle,
-        cursor: mouse::Cursor,
-    ) -> mouse::Interaction {
-        match interaction {
-            Interaction::Drawing => mouse::Interaction::Crosshair,
-            Interaction::Erasing => mouse::Interaction::Crosshair,
-            Interaction::Panning { .. } => mouse::Interaction::ResizingVertically,
-            Interaction::None if cursor.is_over(bounds) => {
-                mouse::Interaction::ResizingVertically
-            }
-            Interaction::None => mouse::Interaction::default(),
-        }
-    }
 }

+ 64 - 65
src/main.rs

@@ -4,9 +4,9 @@ mod data_providers;
 use data_providers::binance::market_data::{self, FeedLatency};
 use data_providers::{binance, bybit};
 mod charts;
-use charts::custom_line::{self, CustomLine};
-use charts::heatmap::{self, Heatmap};
-use charts::footprint::{self, Footprint};
+use charts::footprint::{self, FootprintChart};
+use charts::heatmap::{self, HeatmapChart};
+use charts::candlestick::{self, CandlestickChart};
 
 use std::collections::{VecDeque, HashMap};
 use std::vec;
@@ -165,8 +165,8 @@ impl Default for UserWsState {
 pub enum PaneId {
     HeatmapChart,
     FootprintChart,
-    CandlestickChart,
-    CustomChart,
+    CandlestickChartA,
+    CandlestickChartB,
     TimeAndSales,
     TradePanel,
 }
@@ -220,10 +220,10 @@ pub enum Message {
 
     RestartStream(Option<pane_grid::Pane>, (Option<Ticker>, Option<Timeframe>, Option<f32>)),
 
-    CustomLine(custom_line::Message),
-    Candlestick(custom_line::Message),
-    Heatmap(heatmap::Message),
-    Footprint(footprint::Message),
+    CandlestickA(charts::Message),
+    CandlestickB(charts::Message),
+    Heatmap(charts::Message),
+    Footprint(charts::Message),
 
     // Market&User data stream
     UserKeySucceed(String),
@@ -267,11 +267,11 @@ pub enum Message {
 struct State {
     show_layout_modal: bool,
 
-    candlestick_chart: Option<CustomLine>,
+    candlestick_chart_a: Option<CandlestickChart>,
     time_and_sales: Option<TimeAndSales>,
-    custom_line: Option<CustomLine>,
-    heatmap_chart: Option<Heatmap>,
-    footprint_chart: Option<Footprint>,
+    candlestick_chart_b: Option<CandlestickChart>,
+    heatmap_chart: Option<HeatmapChart>,
+    footprint_chart: Option<FootprintChart>,
 
     // data streams
     listen_key: Option<String>,
@@ -321,16 +321,16 @@ impl State {
                     ratio: 0.5,
                     a: Box::new(Configuration::Pane(
                         PaneSpec { 
-                            id: PaneId::CandlestickChart, 
+                            id: PaneId::CandlestickChartA, 
                             show_modal: false, 
-                            stream: (Some(Ticker::BTCUSDT), Some(Timeframe::M1), None)
+                            stream: (Some(Ticker::BTCUSDT), Some(Timeframe::M15), None)
                         })
                     ),
                     b: Box::new(Configuration::Pane(
                         PaneSpec { 
-                            id: PaneId::CustomChart, 
+                            id: PaneId::CandlestickChartB, 
                             show_modal: false, 
-                            stream: (Some(Ticker::BTCUSDT), Some(Timeframe::M15), None)
+                            stream: (Some(Ticker::BTCUSDT), Some(Timeframe::M1), None)
                         })
                     ),
                 }),
@@ -372,9 +372,9 @@ impl State {
             sync_heatmap: false,
             kline_stream: true,
 
-            candlestick_chart: None,
+            candlestick_chart_a: None,
             time_and_sales: None,
-            custom_line: None,
+            candlestick_chart_b: None,
             heatmap_chart: None,
             footprint_chart: None,
 
@@ -404,15 +404,15 @@ impl State {
 
     fn update(&mut self, message: Message) -> Task<Message> {
         match message {
-            Message::CustomLine(message) => {
-                if let Some(custom_line) = &mut self.custom_line {
-                    custom_line.update(&message);
+            Message::CandlestickA(message) => {
+                if let Some(chart) = &mut self.candlestick_chart_a {
+                    chart.update(&message);
                 }
                 Task::none()
             },
-            Message::Candlestick(message) => {
-                if let Some(candlesticks) = &mut self.candlestick_chart {
-                    candlesticks.update(&message);
+            Message::CandlestickB(message) => {
+                if let Some(chart) = &mut self.candlestick_chart_b {
+                    chart.update(&message);
                 }
                 Task::none()
             },
@@ -569,7 +569,7 @@ impl State {
         
                     for (_, pane_state) in self.panes.iter() {
                         if pane_state.id == PaneId::HeatmapChart {
-                            self.heatmap_chart = Some(Heatmap::new());
+                            self.heatmap_chart = Some(HeatmapChart::new());
                         }
                         if pane_state.id == PaneId::TimeAndSales {
                             self.time_and_sales = Some(TimeAndSales::new());
@@ -665,9 +665,9 @@ impl State {
                     self.bybit_ws_state = BybitWsState::Disconnected;
 
                     self.heatmap_chart = None;
-                    self.candlestick_chart = None;
                     self.time_and_sales = None;
-                    self.custom_line = None;
+                    self.candlestick_chart_a = None;
+                    self.candlestick_chart_b = None;
                     self.footprint_chart = None;
 
                     self.exchange_latency = None;
@@ -680,11 +680,11 @@ impl State {
                 match klines {
                     Ok(klines) => {
                         match target_pane {
-                            PaneId::CustomChart => {
-                                self.custom_line = Some(CustomLine::new(klines, timeframe));
+                            PaneId::CandlestickChartA => {
+                                self.candlestick_chart_a = Some(CandlestickChart::new(klines, timeframe));
                             },
-                            PaneId::CandlestickChart => {
-                                self.candlestick_chart = Some(CustomLine::new(klines, timeframe));
+                            PaneId::CandlestickChartB => {
+                                self.candlestick_chart_b = Some(CandlestickChart::new(klines, timeframe));
                             },
                             PaneId::FootprintChart => {
                                 if let Some(heatmap_chart) = &mut self.heatmap_chart {
@@ -708,7 +708,7 @@ impl State {
 
                                     let tick_size = self.tick_multiply.multiply_with_min_tick_size(self.min_tick_size.unwrap_or(1.0));
 
-                                    self.footprint_chart = Some(Footprint::new(timeframe_u16, tick_size, klines_raw, copied_trades));
+                                    self.footprint_chart = Some(FootprintChart::new(timeframe_u16, tick_size, klines_raw, copied_trades));
                                 }
                             },
                             _ => {}
@@ -716,7 +716,6 @@ impl State {
                     },
                     Err(err) => {
                         eprintln!("Error fetching klines: {err}");
-                        self.candlestick_chart = Some(CustomLine::new(vec![], Timeframe::M1)); 
                     },
                 }
                 Task::none()
@@ -751,14 +750,14 @@ impl State {
                                 if let Some(selected_timeframe) = pane_state.stream.1 {
                                     if selected_timeframe == timeframe {
                                         match pane_state.id {
-                                            PaneId::CandlestickChart => {
-                                                if let Some(candlestick_chart) = &mut self.candlestick_chart {
-                                                    candlestick_chart.insert_datapoint(&kline);
+                                            PaneId::CandlestickChartA => {
+                                                if let Some(chart) = &mut self.candlestick_chart_a {
+                                                    chart.insert_datapoint(&kline);
                                                 }
                                             },
-                                            PaneId::CustomChart => {
-                                                if let Some(custom_line) = &mut self.custom_line {
-                                                    custom_line.insert_datapoint(&kline);
+                                            PaneId::CandlestickChartB => {
+                                                if let Some(chart) = &mut self.candlestick_chart_b {
+                                                    chart.insert_datapoint(&kline);
                                                 }
                                             },
                                             PaneId::FootprintChart => {
@@ -871,14 +870,14 @@ impl State {
                                         };
 
                                         match pane_state.id {
-                                            PaneId::CandlestickChart => {
-                                                if let Some(candlestick_chart) = &mut self.candlestick_chart {
-                                                    candlestick_chart.insert_datapoint(&binance_kline);
+                                            PaneId::CandlestickChartA => {
+                                                if let Some(chart) = &mut self.candlestick_chart_a {
+                                                    chart.insert_datapoint(&binance_kline);
                                                 }
                                             },
-                                            PaneId::CustomChart => {
-                                                if let Some(custom_line) = &mut self.custom_line {
-                                                    custom_line.insert_datapoint(&binance_kline);
+                                            PaneId::CandlestickChartB => {
+                                                if let Some(chart) = &mut self.candlestick_chart_b {
+                                                    chart.insert_datapoint(&binance_kline);
                                                 }
                                             },
                                             PaneId::FootprintChart => {
@@ -1100,8 +1099,8 @@ impl State {
                     &self.footprint_chart,
                     &self.heatmap_chart,
                     &self.time_and_sales,
-                    &self.candlestick_chart, 
-                    &self.custom_line,
+                    &self.candlestick_chart_a, 
+                    &self.candlestick_chart_b,
                 )
             }));
     
@@ -1119,8 +1118,8 @@ impl State {
                 let title = match pane.id {
                     PaneId::HeatmapChart => "Heatmap",
                     PaneId::FootprintChart => "Footprint",
-                    PaneId::CandlestickChart => "Candlesticks",
-                    PaneId::CustomChart => "Candlesticks",
+                    PaneId::CandlestickChartA => "Candlestick",
+                    PaneId::CandlestickChartB => "Candlestick",
                     PaneId::TimeAndSales => "Time&Sales",
                     PaneId::TradePanel => "Trade Panel",
                 };
@@ -1280,8 +1279,8 @@ impl State {
             let pane_info = vec![
                 (PaneId::HeatmapChart, "Heatmap Chart"),
                 (PaneId::FootprintChart, "Footprint Chart"),
-                (PaneId::CandlestickChart, "Candlestick Chart"),
-                (PaneId::CustomChart, "Custom Chart"),
+                (PaneId::CandlestickChartA, "Candlestick Chart 1"),
+                (PaneId::CandlestickChartB, "Candlestick Chart 2"),
                 (PaneId::TimeAndSales, "Time & Sales"),
             ];
 
@@ -1431,11 +1430,11 @@ fn view_content<'a, 'b: 'a>(
     sync_heatmap: bool,
     _total_panes: usize,
     _size: Size,
-    footprint_chart: &'a Option<Footprint>,
-    heatmap_chart: &'a Option<Heatmap>,
+    footprint_chart: &'a Option<FootprintChart>,
+    heatmap_chart: &'a Option<HeatmapChart>,
     time_and_sales: &'a Option<TimeAndSales>,
-    candlestick_chart: &'a Option<CustomLine>,
-    custom_line: &'a Option<CustomLine>,
+    candlestick_chart_a: &'a Option<CandlestickChart>,
+    candlestick_chart_b: &'a Option<CandlestickChart>,
 ) -> Element<'a, Message> {
     let content: Element<Message, Theme, Renderer> = match pane_id {
         PaneId::HeatmapChart => {
@@ -1509,13 +1508,13 @@ fn view_content<'a, 'b: 'a>(
             underlay
         },
         
-        PaneId::CandlestickChart => { 
+        PaneId::CandlestickChartA => { 
             let underlay; 
-            if let Some(candlestick_chart) = candlestick_chart {
+            if let Some(chart) = candlestick_chart_a {
                 underlay =
-                    candlestick_chart
+                    chart
                         .view()
-                        .map(Message::Candlestick);
+                        .map(Message::CandlestickA);
             } else {
                 underlay = Text::new("No data")
                     .width(Length::Fill)
@@ -1525,13 +1524,13 @@ fn view_content<'a, 'b: 'a>(
             underlay
         },
 
-        PaneId::CustomChart => { 
+        PaneId::CandlestickChartB => { 
             let underlay; 
-            if let Some(custom_line) = custom_line {
+            if let Some(chart) = candlestick_chart_b {
                 underlay =
-                    custom_line
+                    chart
                         .view()
-                        .map(Message::CustomLine);
+                        .map(Message::CandlestickB);
             } else {
                 underlay = Text::new("No data")
                     .width(Length::Fill)
@@ -1622,7 +1621,7 @@ fn view_controls<'a>(
         (Icon::ResizeFull, Message::Maximize(pane))
     };
 
-    if pane_id == PaneId::CandlestickChart || pane_id == PaneId::CustomChart || pane_id == PaneId::FootprintChart {
+    if pane_id == PaneId::CandlestickChartA || pane_id == PaneId::CandlestickChartB || pane_id == PaneId::FootprintChart {
         let timeframe_picker = pick_list(
             &Timeframe::ALL[..],
             selected_timeframe,