| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950 |
- use std::{collections::BTreeMap, vec};
- use chrono::{DateTime, Utc, TimeZone, LocalResult, Duration, NaiveDateTime, Timelike};
- use iced::{
- alignment, color, mouse, widget::{button, canvas::{self, event::{self, Event}, path, 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)]
- 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<DateTime<Utc>, (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 time = match Utc.timestamp_opt(kline.time as i64 / 1000, 0) {
- LocalResult::Single(dt) => dt,
- _ => continue,
- };
- let buy_volume = kline.taker_buy_base_asset_volume;
- let sell_volume = kline.volume - buy_volume;
- klines_raw.insert(time, (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 time = match Utc.timestamp_opt(kline.time as i64 / 1000, 0) {
- LocalResult::Single(dt) => dt,
- _ => return,
- };
- let buy_volume = kline.taker_buy_base_asset_volume;
- let sell_volume = kline.volume - buy_volume;
- self.klines_raw.insert(time, (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.timestamp() - ((self.translation.x*10.0)*(self.timeframe as f32)) as i64);
- let earliest: i64 = latest - ((6400.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, _)| {
- let timestamp = time.timestamp();
- timestamp >= earliest && timestamp <= 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.timestamp() - ((self.translation.x*10.0)*(self.timeframe as f32)) as i64);
- let earliest: i64 = latest - ((6400.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, _)| {
- let timestamp = time.timestamp();
- timestamp >= earliest && timestamp <= 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);
- let background = self.mesh_cache.draw(renderer, bounds.size(), |frame| {
- frame.with_save(|frame| {
- let latest_in_millis = latest * 1000;
- let earliest_in_millis = earliest * 1000;
- let mut time = rounded_earliest;
- let latest_time = NaiveDateTime::from_timestamp(latest, 0);
- while time <= latest_time {
- let time_in_millis = time.timestamp_millis();
-
- let x_position = ((time_in_millis - 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 line = Path::line(
- Point::new(x_position as f32, 0.0),
- Point::new(x_position as f32, bounds.height as f32)
- );
- frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(27, 27, 27, 1.0)).with_width(1.0))
- }
-
- time = 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 as f32, 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.timestamp() - 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));
- 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) as f32),
- Size::new(2.0 * self.scaling, buy_bar_height as f32)
- );
- 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) as f32),
- Size::new(2.0 * self.scaling, sell_bar_height as f32)
- );
- frame.fill(&sell_bar, Color::from_rgb8(192, 80, 77));
- }
- });
- 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 as f32, 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 * 1000.0 + crosshair_ratio * (latest as f64 * 1000.0 - earliest as f64 * 1000.0);
- 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 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 as f32)
- );
- 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.timestamp() == rounded_timestamp) {
- let tooltip_text = format!(
- "O: {} H: {} L: {} C: {}\nBuyV: {:.0} SellV: {:.0}",
- kline.0, kline.1, kline.2, kline.3, kline.4, 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);
- }
- }
- });
- return vec![background, crosshair, candlesticks];
- } else {
- return 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)
- }
- fn calculate_time_step(earliest: i64, latest: i64, labels_can_fit: i32) -> (Duration, NaiveDateTime) {
- let duration = latest - earliest;
- let duration_in_millis = duration * 1000;
- let steps = [
- Duration::days(1),
- Duration::hours(12),
- Duration::hours(8),
- Duration::hours(4),
- Duration::hours(2),
- Duration::minutes(60),
- Duration::minutes(30),
- Duration::minutes(15),
- Duration::minutes(10),
- Duration::minutes(5),
- Duration::minutes(1),
- ];
- let mut selected_step = steps[0];
- for &step in steps.iter() {
- if duration_in_millis / step.num_milliseconds() >= labels_can_fit as i64 {
- selected_step = step;
- break;
- }
- }
- let mut rounded_earliest = NaiveDateTime::from_timestamp(earliest, 0)
- .with_second(0).unwrap()
- .with_nanosecond(0).unwrap();
- let minutes = rounded_earliest.minute();
- let step_minutes = selected_step.num_minutes() as u32;
- let remainder = minutes % step_minutes;
- if remainder > 0 {
- rounded_earliest = rounded_earliest + Duration::minutes((step_minutes - remainder) as i64);
- }
- (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 latest_in_millis = self.max * 1000;
- let earliest_in_millis = self.min * 1000;
- 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);
- let labels = self.labels_cache.draw(renderer, bounds.size(), |frame| {
- frame.with_save(|frame| {
- let mut time = rounded_earliest;
- let latest_time = NaiveDateTime::from_timestamp(self.max, 0);
- while time <= latest_time {
- let time_in_millis = time.timestamp_millis();
-
- let x_position = ((time_in_millis - 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 label = canvas::Text {
- content: time.format("%H:%M").to_string(),
- position: Point::new(x_position as f32 - text_size, bounds.height as f32 - 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 + 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 = 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 as f32 - 20.0);
- let text_position = Point::new(snap_x as f32 - 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 + 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 = if step.fract() == 0.0 { 0 } else { 1 };
- let label_content = match decimal_places {
- 0 => format!("{:.0}", y),
- _ => format!("{:.1}", y),
- };
- 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(),
- }
- }
- }
|