Browse Source

added crosshair for the custom candlestick chart

Berke 1 year ago
parent
commit
b70850c142
1 changed files with 222 additions and 29 deletions
  1. 222 29
      src/charts/custom_line.rs

+ 222 - 29
src/charts/custom_line.rs

@@ -1,12 +1,9 @@
 use std::collections::BTreeMap;
 use std::collections::BTreeMap;
 use chrono::{DateTime, Utc, TimeZone, LocalResult, Duration, NaiveDateTime, Timelike};
 use chrono::{DateTime, Utc, TimeZone, LocalResult, Duration, NaiveDateTime, Timelike};
 use iced::{
 use iced::{
-    mouse, 
-    widget::canvas::{self, event::{self, Event}, 
-    stroke::Stroke, Cache, Geometry, Path, Canvas}, 
-    window, Color, Point, Rectangle, Renderer, Size, Theme, Vector, Element, Length
+    alignment, mouse, widget::{button, canvas::{self, event::{self, Event}, stroke::Stroke, Cache, Canvas, Geometry, Path}}, window, Color, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector
 };
 };
-use iced::widget::{Column, Row, Container, Space};
+use iced::widget::{Column, Row, Container, Text};
 use crate::market_data::Kline;
 use crate::market_data::Kline;
 
 
 #[derive(Debug, Clone)]
 #[derive(Debug, Clone)]
@@ -14,18 +11,24 @@ pub enum Message {
     Translated(Vector),
     Translated(Vector),
     Scaled(f32, Option<Vector>),
     Scaled(f32, Option<Vector>),
     ChartBounds(f32, f32),
     ChartBounds(f32, f32),
+    AutoscaleToggle,
+    CrosshairToggle,
+    CrosshairMoved(Point),
 }
 }
 
 
 #[derive(Debug)]
 #[derive(Debug)]
 pub struct CustomLine {
 pub struct CustomLine {
     mesh_cache: Cache,
     mesh_cache: Cache,
     candles_cache: Cache,
     candles_cache: Cache,
+    crosshair_cache: Cache,
     x_labels_cache: Cache,
     x_labels_cache: Cache,
     y_labels_cache: Cache,
     y_labels_cache: Cache,
     translation: Vector,
     translation: Vector,
     scaling: f32,
     scaling: f32,
     klines_raw: BTreeMap<DateTime<Utc>, (f32, f32, f32, f32, f32, f32)>,
     klines_raw: BTreeMap<DateTime<Utc>, (f32, f32, f32, f32, f32, f32)>,
     autoscale: bool,
     autoscale: bool,
+    crosshair: bool,
+    crosshair_position: Point,
     x_min_time: i64,
     x_min_time: i64,
     x_max_time: i64,
     x_max_time: i64,
     y_min_price: f32,
     y_min_price: f32,
@@ -54,12 +57,15 @@ impl CustomLine {
         CustomLine {
         CustomLine {
             mesh_cache: canvas::Cache::default(),
             mesh_cache: canvas::Cache::default(),
             candles_cache: canvas::Cache::default(),
             candles_cache: canvas::Cache::default(),
+            crosshair_cache: canvas::Cache::default(),
             x_labels_cache: canvas::Cache::default(),
             x_labels_cache: canvas::Cache::default(),
             y_labels_cache: canvas::Cache::default(),
             y_labels_cache: canvas::Cache::default(),
             klines_raw,
             klines_raw,
             translation: Vector::default(),
             translation: Vector::default(),
             scaling: 1.0,
             scaling: 1.0,
             autoscale: true,
             autoscale: true,
+            crosshair: false,
+            crosshair_position: Point::new(0.0, 0.0),
             x_min_time: 0,
             x_min_time: 0,
             x_max_time: 0,
             x_max_time: 0,
             y_min_price: 0.0,
             y_min_price: 0.0,
@@ -127,6 +133,7 @@ impl CustomLine {
         self.y_max_price = highest;
         self.y_max_price = highest;
 
 
         self.y_labels_cache.clear();
         self.y_labels_cache.clear();
+        self.crosshair_cache.clear();
     }
     }
 
 
     pub fn update(&mut self, message: Message) {
     pub fn update(&mut self, message: Message) {
@@ -137,6 +144,7 @@ impl CustomLine {
                 } else {
                 } else {
                     self.translation = translation;
                     self.translation = translation;
                 }
                 }
+                self.crosshair_position = Point::new(0.0, 0.0);
 
 
                 self.render_start();
                 self.render_start();
             }
             }
@@ -150,6 +158,7 @@ impl CustomLine {
                         self.translation = translation;
                         self.translation = translation;
                     }
                     }
                 }
                 }
+                self.crosshair_position = Point::new(0.0, 0.0);
 
 
                 self.render_start();
                 self.render_start();
             }
             }
@@ -157,6 +166,16 @@ impl CustomLine {
                 self.chart_width = width;
                 self.chart_width = width;
                 self.chart_height = height;
                 self.chart_height = height;
             }
             }
+            Message::AutoscaleToggle => {
+                self.autoscale = !self.autoscale;
+            }
+            Message::CrosshairToggle => {
+                self.crosshair = !self.crosshair;
+            }
+            Message::CrosshairMoved(position) => {
+                self.crosshair_position = position;
+                self.crosshair_cache.clear();
+            }
         }
         }
     }
     }
 
 
@@ -167,7 +186,7 @@ impl CustomLine {
     
     
         let axis_labels_x = Canvas::new(
         let axis_labels_x = Canvas::new(
             AxisLabelXCanvas { 
             AxisLabelXCanvas { 
-                labels_cache: &self.x_labels_cache, min: self.x_min_time, max: self.x_max_time 
+                labels_cache: &self.x_labels_cache, min: self.x_min_time, max: self.x_max_time, crosshair_cache: &self.crosshair_cache, crosshair_position: self.crosshair_position, crosshair: self.crosshair
             })
             })
             .width(Length::FillPortion(10))
             .width(Length::FillPortion(10))
             .height(Length::Fixed(26.0));
             .height(Length::Fixed(26.0));
@@ -177,24 +196,45 @@ impl CustomLine {
     
     
         let axis_labels_y = Canvas::new(
         let axis_labels_y = Canvas::new(
             AxisLabelYCanvas { 
             AxisLabelYCanvas { 
-                labels_cache: &self.y_labels_cache, min: self.y_min_price, max: self.y_max_price, last_close_price, last_open_price 
+                labels_cache: &self.y_labels_cache, crosshair_cache: &self.crosshair_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(52.0))
+            .width(Length::Fixed(60.0))
             .height(Length::FillPortion(10));
             .height(Length::FillPortion(10));
+
+        let autoscale_button = button(
+            Text::new("A")
+                .size(10)
+                .horizontal_alignment(alignment::Horizontal::Center)
+            )
+            .width(Length::Fill)
+            .height(Length::Fill)
+            .on_press(Message::AutoscaleToggle)
+            .style(MinDarkButtonStyleSheet::new(self.autoscale));
+        let crosshair_button = button(
+            Text::new("+")
+                .size(10)
+                .horizontal_alignment(alignment::Horizontal::Center)
+            ) 
+            .width(Length::Fill)
+            .height(Length::Fill)
+            .on_press(Message::CrosshairToggle)
+            .style(MinDarkButtonStyleSheet::new(self.crosshair));
     
     
-        let empty_space = Container::new(Space::new(Length::Fixed(40.0), Length::Fixed(40.0)))
-            .width(Length::Fixed(52.0))
+        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));
             .height(Length::Fixed(26.0));
-    
+
         let chart_and_y_labels = Row::new()
         let chart_and_y_labels = Row::new()
             .push(chart)
             .push(chart)
-            .push(axis_labels_y)
-            .spacing(0);
+            .push(axis_labels_y);
     
     
         let bottom_row = Row::new()
         let bottom_row = Row::new()
             .push(axis_labels_x)
             .push(axis_labels_x)
-            .push(empty_space)
-            .spacing(0);
+            .push(chart_controls);
     
     
         let content = Column::new()
         let content = Column::new()
             .push(chart_and_y_labels)
             .push(chart_and_y_labels)
@@ -206,6 +246,62 @@ impl CustomLine {
     }
     }
 }
 }
 
 
+pub struct MinDarkButtonStyleSheet {
+    is_active: bool,
+}
+
+impl MinDarkButtonStyleSheet {
+    pub fn new(is_active: bool) -> iced::theme::Button {
+        iced::theme::Button::Custom(Box::new(Self { is_active }))
+    }
+}
+
+impl button::StyleSheet for MinDarkButtonStyleSheet {
+    type Style = iced::Theme;
+
+    fn active(&self, _style: &Self::Style) -> button::Appearance {
+        let background = if self.is_active {
+            iced::Background::Color(Color::from_rgba8(20, 20, 20, 255.0)) 
+        } else {
+            iced::Background::Color(Color::from_rgba8(0, 0, 0, 255.0)) 
+        };
+        let border_color = if self.is_active {
+            Color::from_rgba8(50, 50, 50, 255.0)
+        } else {
+            Color::from_rgba8(20, 20, 20, 255.0)
+        };
+
+        button::Appearance {
+            background: Some(background),
+            text_color: Color::WHITE,
+            border: iced::Border {
+                color: border_color,
+                width: 1.0,
+                radius: 4.0.into(),
+            },
+            ..Default::default()
+        }
+    }
+    fn hovered(&self, _style: &Self::Style) -> button::Appearance {    
+        let background = iced::Background::Color(Color::from_rgba8(40, 40, 40,255.0));
+        let border_color = if self.is_active {
+            Color::from_rgba8(50, 50, 50, 255.0)
+        } else {
+            Color::from_rgba8(20, 20, 20, 255.0)
+        };
+
+        button::Appearance {
+            background: Some(background),
+            text_color: Color::WHITE,
+            border: iced::Border {
+                color: border_color,
+                width: 1.0,
+                radius: 4.0.into(),
+            },
+            ..Default::default()
+        }
+    }
+}
 
 
 #[derive(Debug, Clone, Copy)]
 #[derive(Debug, Clone, Copy)]
 pub enum Interaction {
 pub enum Interaction {
@@ -239,7 +335,13 @@ impl canvas::Program<Message> for CustomLine {
         }
         }
 
 
         let Some(cursor_position) = cursor.position_in(bounds) else {
         let Some(cursor_position) = cursor.position_in(bounds) else {
-            return (event::Status::Ignored, None);
+            return (event::Status::Ignored, 
+                if self.crosshair {
+                    Some(Message::CrosshairMoved(Point::new(0.0, 0.0)))
+                } else {
+                    None
+                }
+                );
         };
         };
 
 
         match event {
         match event {
@@ -273,7 +375,12 @@ impl canvas::Program<Message> for CustomLine {
                                         * (1.0 / self.scaling),
                                         * (1.0 / self.scaling),
                             ))
                             ))
                         }
                         }
-                        Interaction::None => None,
+                        Interaction::None => 
+                            if self.crosshair && cursor.is_over(bounds) {
+                                Some(Message::CrosshairMoved(cursor_position))
+                            } else {
+                                None
+                            },
                     };
                     };
 
 
                     let event_status = match interaction {
                     let event_status = match interaction {
@@ -339,7 +446,7 @@ impl canvas::Program<Message> for CustomLine {
         renderer: &Renderer,
         renderer: &Renderer,
         _theme: &Theme,
         _theme: &Theme,
         bounds: Rectangle,
         bounds: Rectangle,
-        _cursor: mouse::Cursor,
+        cursor: mouse::Cursor,
     ) -> Vec<Geometry> {    
     ) -> Vec<Geometry> {    
         let latest: i64 = self.klines_raw.keys().last().map_or(0, |time| time.timestamp() - (self.translation.x*10.0) as i64);
         let latest: i64 = self.klines_raw.keys().last().map_or(0, |time| time.timestamp() - (self.translation.x*10.0) as i64);
         let earliest: i64 = latest - (6400.0 / (self.scaling / (bounds.width/800.0))) as i64;
         let earliest: i64 = latest - (6400.0 / (self.scaling / (bounds.width/800.0))) as i64;
@@ -464,7 +571,30 @@ impl canvas::Program<Message> for CustomLine {
                 }
                 }
             });
             });
 
 
-        vec![background, candlesticks]
+        if self.crosshair {
+            let crosshair = self.crosshair_cache.draw(renderer, bounds.size(), |frame| {
+                if let Some(cursor_position) = cursor.position_in(bounds) {
+                    let x = cursor_position.x;
+                    let y = cursor_position.y;
+
+                    let line = Path::line(
+                        Point::new(x, 0.0), 
+                        Point::new(x, bounds.height as f32)
+                    );
+                    frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(200, 200, 200, 1.0)).with_width(1.0));
+
+                    let line = Path::line(
+                        Point::new(0.0, y), 
+                        Point::new(bounds.width as f32, y)
+                    );
+                    frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(200, 200, 200, 1.0)).with_width(1.0));
+                }
+            });
+
+            return vec![background, crosshair, candlesticks];
+        }   else {
+            return vec![background, candlesticks];
+        }
     }
     }
 
 
     fn mouse_interaction(
     fn mouse_interaction(
@@ -543,6 +673,9 @@ impl Default for CustomLine {
 }
 }
 pub struct AxisLabelXCanvas<'a> {
 pub struct AxisLabelXCanvas<'a> {
     labels_cache: &'a Cache,
     labels_cache: &'a Cache,
+    crosshair_cache: &'a Cache,
+    crosshair_position: Point,
+    crosshair: bool,
     min: i64,
     min: i64,
     max: i64,
     max: i64,
 }
 }
@@ -551,10 +684,10 @@ impl canvas::Program<Message> for AxisLabelXCanvas<'_> {
 
 
     fn update(
     fn update(
         &self,
         &self,
-        interaction: &mut Interaction,
-        event: Event,
-        bounds: Rectangle,
-        cursor: mouse::Cursor,
+        _interaction: &mut Interaction,
+        _event: Event,
+        _bounds: Rectangle,
+        _cursor: mouse::Cursor,
     ) -> (event::Status, Option<Message>) {
     ) -> (event::Status, Option<Message>) {
         (event::Status::Ignored, None)
         (event::Status::Ignored, None)
     }
     }
@@ -606,8 +739,39 @@ impl canvas::Program<Message> for AxisLabelXCanvas<'_> {
                 }
                 }
             });
             });
         });
         });
+        let crosshair = self.crosshair_cache.draw(renderer, bounds.size(), |frame| {
+            if self.crosshair && self.crosshair_position.x > 0.0 {
+                let latest_in_millis = self.max * 1000;
+                let earliest_in_millis = self.min * 1000;
+
+                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 text_size = 12.0;
+                let text_content = crosshair_time.format("%H:%M").to_string();
+                let growth_amount = 6.0; 
+                let rectangle_position = Point::new(self.crosshair_position.x - 14.0 - growth_amount, bounds.height as f32 - 20.0);
+                let text_position = Point::new(self.crosshair_position.x - 14.0, bounds.height as f32 - 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]
+        vec![labels, crosshair]
     }
     }
 
 
     fn mouse_interaction(
     fn mouse_interaction(
@@ -629,20 +793,23 @@ impl canvas::Program<Message> for AxisLabelXCanvas<'_> {
 }
 }
 pub struct AxisLabelYCanvas<'a> {
 pub struct AxisLabelYCanvas<'a> {
     labels_cache: &'a Cache,
     labels_cache: &'a Cache,
+    crosshair_cache: &'a Cache,
     min: f32,
     min: f32,
     max: f32,
     max: f32,
     last_close_price: f32,
     last_close_price: f32,
     last_open_price: f32,
     last_open_price: f32,
+    crosshair_position: Point,
+    crosshair: bool,
 }
 }
 impl canvas::Program<Message> for AxisLabelYCanvas<'_> {
 impl canvas::Program<Message> for AxisLabelYCanvas<'_> {
     type State = Interaction;
     type State = Interaction;
 
 
     fn update(
     fn update(
         &self,
         &self,
-        interaction: &mut Interaction,
-        event: Event,
-        bounds: Rectangle,
-        cursor: mouse::Cursor,
+        _interaction: &mut Interaction,
+        _event: Event,
+        _bounds: Rectangle,
+        _cursor: mouse::Cursor,
     ) -> (event::Status, Option<Message>) {
     ) -> (event::Status, Option<Message>) {
         (event::Status::Ignored, None)
         (event::Status::Ignored, None)
     }
     }
@@ -712,8 +879,34 @@ impl canvas::Program<Message> for AxisLabelYCanvas<'_> {
                 frame.fill(&triangle, triangle_color);
                 frame.fill(&triangle, triangle_color);
             });
             });
         });
         });
+        let crosshair = self.crosshair_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, 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]
+        vec![labels, crosshair]
     }
     }
 
 
     fn mouse_interaction(
     fn mouse_interaction(