custom_line.rs 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950
  1. use std::{collections::BTreeMap, vec};
  2. use chrono::{DateTime, Utc, TimeZone, LocalResult, Duration, NaiveDateTime, Timelike};
  3. use iced::{
  4. 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
  5. };
  6. use iced::widget::{Column, Row, Container, Text};
  7. use crate::{market_data::Kline, Timeframe};
  8. #[derive(Debug, Clone)]
  9. pub enum Message {
  10. Translated(Vector),
  11. Scaled(f32, Option<Vector>),
  12. ChartBounds(Rectangle),
  13. AutoscaleToggle,
  14. CrosshairToggle,
  15. CrosshairMoved(Point),
  16. }
  17. #[derive(Debug)]
  18. pub struct CustomLine {
  19. mesh_cache: Cache,
  20. candles_cache: Cache,
  21. crosshair_cache: Cache,
  22. x_labels_cache: Cache,
  23. y_labels_cache: Cache,
  24. y_croshair_cache: Cache,
  25. x_crosshair_cache: Cache,
  26. translation: Vector,
  27. scaling: f32,
  28. klines_raw: BTreeMap<DateTime<Utc>, (f32, f32, f32, f32, f32, f32)>,
  29. timeframe: i16,
  30. autoscale: bool,
  31. crosshair: bool,
  32. crosshair_position: Point,
  33. x_min_time: i64,
  34. x_max_time: i64,
  35. y_min_price: f32,
  36. y_max_price: f32,
  37. bounds: Rectangle,
  38. }
  39. impl CustomLine {
  40. const MIN_SCALING: f32 = 0.1;
  41. const MAX_SCALING: f32 = 2.0;
  42. pub fn new(klines: Vec<Kline>, timeframe: Timeframe) -> CustomLine {
  43. let _size = window::Settings::default().size;
  44. let mut klines_raw = BTreeMap::new();
  45. for kline in klines {
  46. let time = match Utc.timestamp_opt(kline.time as i64 / 1000, 0) {
  47. LocalResult::Single(dt) => dt,
  48. _ => continue,
  49. };
  50. let buy_volume = kline.taker_buy_base_asset_volume;
  51. let sell_volume = kline.volume - buy_volume;
  52. klines_raw.insert(time, (kline.open, kline.high, kline.low, kline.close, buy_volume, sell_volume));
  53. }
  54. let timeframe = match timeframe {
  55. Timeframe::M1 => 1,
  56. Timeframe::M3 => 3,
  57. Timeframe::M5 => 5,
  58. Timeframe::M15 => 15,
  59. Timeframe::M30 => 30,
  60. };
  61. CustomLine {
  62. mesh_cache: canvas::Cache::default(),
  63. candles_cache: canvas::Cache::default(),
  64. crosshair_cache: canvas::Cache::default(),
  65. x_labels_cache: canvas::Cache::default(),
  66. y_labels_cache: canvas::Cache::default(),
  67. y_croshair_cache: canvas::Cache::default(),
  68. x_crosshair_cache: canvas::Cache::default(),
  69. timeframe,
  70. klines_raw,
  71. translation: Vector::default(),
  72. scaling: 1.0,
  73. autoscale: true,
  74. crosshair: false,
  75. crosshair_position: Point::new(0.0, 0.0),
  76. x_min_time: 0,
  77. x_max_time: 0,
  78. y_min_price: 0.0,
  79. y_max_price: 0.0,
  80. bounds: Rectangle::default(),
  81. }
  82. }
  83. pub fn insert_datapoint(&mut self, kline: Kline) {
  84. let time = match Utc.timestamp_opt(kline.time as i64 / 1000, 0) {
  85. LocalResult::Single(dt) => dt,
  86. _ => return,
  87. };
  88. let buy_volume = kline.taker_buy_base_asset_volume;
  89. let sell_volume = kline.volume - buy_volume;
  90. self.klines_raw.insert(time, (kline.open, kline.high, kline.low, kline.close, buy_volume, sell_volume));
  91. self.render_start();
  92. }
  93. pub fn render_start(&mut self) {
  94. self.candles_cache.clear();
  95. 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);
  96. let earliest: i64 = latest - ((6400.0*self.timeframe as f32) / (self.scaling / (self.bounds.width/800.0))) as i64;
  97. let (visible_klines, highest, lowest, avg_body_height, _, _) = self.klines_raw.iter()
  98. .filter(|(time, _)| {
  99. let timestamp = time.timestamp();
  100. timestamp >= earliest && timestamp <= latest
  101. })
  102. .fold((vec![], f32::MIN, f32::MAX, 0.0f32, 0.0f32, None), |(mut klines, highest, lowest, total_body_height, max_vol, latest_kline), (time, kline)| {
  103. let body_height = (kline.0 - kline.3).abs();
  104. klines.push((*time, *kline));
  105. let total_body_height = match latest_kline {
  106. Some(_) => total_body_height + body_height,
  107. None => total_body_height,
  108. };
  109. (
  110. klines,
  111. highest.max(kline.1),
  112. lowest.min(kline.2),
  113. total_body_height,
  114. max_vol.max(kline.4.max(kline.5)),
  115. Some(kline)
  116. )
  117. });
  118. if visible_klines.is_empty() || visible_klines.len() == 1 {
  119. return;
  120. }
  121. let avg_body_height = avg_body_height / (visible_klines.len() - 1) as f32;
  122. let (highest, lowest) = (highest + avg_body_height, lowest - avg_body_height);
  123. if earliest != self.x_min_time || latest != self.x_max_time || lowest != self.y_min_price || highest != self.y_max_price {
  124. self.x_labels_cache.clear();
  125. self.mesh_cache.clear();
  126. }
  127. self.x_min_time = earliest;
  128. self.x_max_time = latest;
  129. self.y_min_price = lowest;
  130. self.y_max_price = highest;
  131. self.y_labels_cache.clear();
  132. self.crosshair_cache.clear();
  133. }
  134. pub fn update(&mut self, message: Message) {
  135. match message {
  136. Message::Translated(translation) => {
  137. if self.autoscale {
  138. self.translation.x = translation.x;
  139. } else {
  140. self.translation = translation;
  141. }
  142. self.crosshair_position = Point::new(0.0, 0.0);
  143. self.render_start();
  144. }
  145. Message::Scaled(scaling, translation) => {
  146. self.scaling = scaling;
  147. if let Some(translation) = translation {
  148. if self.autoscale {
  149. self.translation.x = translation.x;
  150. } else {
  151. self.translation = translation;
  152. }
  153. }
  154. self.crosshair_position = Point::new(0.0, 0.0);
  155. self.render_start();
  156. }
  157. Message::ChartBounds(bounds) => {
  158. self.bounds = bounds;
  159. }
  160. Message::AutoscaleToggle => {
  161. self.autoscale = !self.autoscale;
  162. }
  163. Message::CrosshairToggle => {
  164. self.crosshair = !self.crosshair;
  165. }
  166. Message::CrosshairMoved(position) => {
  167. self.crosshair_position = position;
  168. if self.crosshair {
  169. self.crosshair_cache.clear();
  170. self.y_croshair_cache.clear();
  171. self.x_crosshair_cache.clear();
  172. }
  173. }
  174. }
  175. }
  176. pub fn view(&self) -> Element<Message> {
  177. let chart = Canvas::new(self)
  178. .width(Length::FillPortion(10))
  179. .height(Length::FillPortion(10));
  180. let axis_labels_x = Canvas::new(
  181. AxisLabelXCanvas {
  182. labels_cache: &self.x_labels_cache,
  183. min: self.x_min_time,
  184. max: self.x_max_time,
  185. crosshair_cache: &self.x_crosshair_cache,
  186. crosshair_position: self.crosshair_position,
  187. crosshair: self.crosshair,
  188. timeframe: self.timeframe
  189. })
  190. .width(Length::FillPortion(10))
  191. .height(Length::Fixed(26.0));
  192. let last_close_price = self.klines_raw.values().last().map_or(0.0, |kline| kline.3);
  193. let last_open_price = self.klines_raw.values().last().map_or(0.0, |kline| kline.0);
  194. let axis_labels_y = Canvas::new(
  195. AxisLabelYCanvas {
  196. labels_cache: &self.y_labels_cache,
  197. y_croshair_cache: &self.y_croshair_cache,
  198. min: self.y_min_price,
  199. max: self.y_max_price,
  200. last_close_price,
  201. last_open_price,
  202. crosshair_position: self.crosshair_position,
  203. crosshair: self.crosshair
  204. })
  205. .width(Length::Fixed(60.0))
  206. .height(Length::FillPortion(10));
  207. let autoscale_button = button(
  208. Text::new("A")
  209. .size(12)
  210. .horizontal_alignment(alignment::Horizontal::Center)
  211. )
  212. .width(Length::Fill)
  213. .height(Length::Fill)
  214. .on_press(Message::AutoscaleToggle)
  215. .style(|_theme: &Theme, _status: iced::widget::button::Status| chart_button(_theme, &_status, self.autoscale));
  216. let crosshair_button = button(
  217. Text::new("+")
  218. .size(12)
  219. .horizontal_alignment(alignment::Horizontal::Center)
  220. )
  221. .width(Length::Fill)
  222. .height(Length::Fill)
  223. .on_press(Message::CrosshairToggle)
  224. .style(|_theme: &Theme, _status: iced::widget::button::Status| chart_button(_theme, &_status, self.crosshair));
  225. let chart_controls = Container::new(
  226. Row::new()
  227. .push(autoscale_button)
  228. .push(crosshair_button).spacing(2)
  229. ).padding([0, 2, 0, 2])
  230. .width(Length::Fixed(60.0))
  231. .height(Length::Fixed(26.0));
  232. let chart_and_y_labels = Row::new()
  233. .push(chart)
  234. .push(axis_labels_y);
  235. let bottom_row = Row::new()
  236. .push(axis_labels_x)
  237. .push(chart_controls);
  238. let content = Column::new()
  239. .push(chart_and_y_labels)
  240. .push(bottom_row)
  241. .spacing(0)
  242. .padding(5);
  243. content.into()
  244. }
  245. }
  246. fn chart_button(_theme: &Theme, _status: &button::Status, is_active: bool) -> button::Style {
  247. button::Style {
  248. background: Some(Color::from_rgba8(20, 20, 20, 1.0).into()),
  249. border: Border {
  250. color: {
  251. if is_active {
  252. Color::from_rgba8(50, 50, 50, 1.0)
  253. } else {
  254. Color::from_rgba8(20, 20, 20, 1.0)
  255. }
  256. },
  257. width: 1.0,
  258. radius: 2.0.into(),
  259. },
  260. text_color: Color::WHITE,
  261. ..button::Style::default()
  262. }
  263. }
  264. #[derive(Debug, Clone, Copy)]
  265. pub enum Interaction {
  266. None,
  267. Drawing,
  268. Erasing,
  269. Panning { translation: Vector, start: Point },
  270. }
  271. impl Default for Interaction {
  272. fn default() -> Self {
  273. Self::None
  274. }
  275. }
  276. impl canvas::Program<Message> for CustomLine {
  277. type State = Interaction;
  278. fn update(
  279. &self,
  280. interaction: &mut Interaction,
  281. event: Event,
  282. bounds: Rectangle,
  283. cursor: mouse::Cursor,
  284. ) -> (event::Status, Option<Message>) {
  285. if bounds != self.bounds {
  286. return (event::Status::Ignored, Some(Message::ChartBounds(bounds)));
  287. }
  288. if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event {
  289. *interaction = Interaction::None;
  290. }
  291. let Some(cursor_position) = cursor.position_in(bounds) else {
  292. return (event::Status::Ignored,
  293. if self.crosshair {
  294. Some(Message::CrosshairMoved(Point::new(0.0, 0.0)))
  295. } else {
  296. None
  297. }
  298. );
  299. };
  300. match event {
  301. Event::Mouse(mouse_event) => match mouse_event {
  302. mouse::Event::ButtonPressed(button) => {
  303. let message = match button {
  304. mouse::Button::Right => {
  305. *interaction = Interaction::Drawing;
  306. None
  307. }
  308. mouse::Button::Left => {
  309. *interaction = Interaction::Panning {
  310. translation: self.translation,
  311. start: cursor_position,
  312. };
  313. None
  314. }
  315. _ => None,
  316. };
  317. (event::Status::Captured, message)
  318. }
  319. mouse::Event::CursorMoved { .. } => {
  320. let message = match *interaction {
  321. Interaction::Drawing => None,
  322. Interaction::Erasing => None,
  323. Interaction::Panning { translation, start } => {
  324. Some(Message::Translated(
  325. translation
  326. + (cursor_position - start)
  327. * (1.0 / self.scaling),
  328. ))
  329. }
  330. Interaction::None =>
  331. if self.crosshair && cursor.is_over(bounds) {
  332. Some(Message::CrosshairMoved(cursor_position))
  333. } else {
  334. None
  335. },
  336. };
  337. let event_status = match interaction {
  338. Interaction::None => event::Status::Ignored,
  339. _ => event::Status::Captured,
  340. };
  341. (event_status, message)
  342. }
  343. mouse::Event::WheelScrolled { delta } => match delta {
  344. mouse::ScrollDelta::Lines { y, .. }
  345. | mouse::ScrollDelta::Pixels { y, .. } => {
  346. if y < 0.0 && self.scaling > Self::MIN_SCALING
  347. || y > 0.0 && self.scaling < Self::MAX_SCALING
  348. {
  349. //let old_scaling = self.scaling;
  350. let scaling = (self.scaling * (1.0 + y / 30.0))
  351. .clamp(
  352. Self::MIN_SCALING, // 0.1
  353. Self::MAX_SCALING, // 2.0
  354. );
  355. //let translation =
  356. // if let Some(cursor_to_center) =
  357. // cursor.position_from(bounds.center())
  358. // {
  359. // let factor = scaling - old_scaling;
  360. // Some(
  361. // self.translation
  362. // - Vector::new(
  363. // cursor_to_center.x * factor
  364. // / (old_scaling
  365. // * old_scaling),
  366. // cursor_to_center.y * factor
  367. // / (old_scaling
  368. // * old_scaling),
  369. // ),
  370. // )
  371. // } else {
  372. // None
  373. // };
  374. (
  375. event::Status::Captured,
  376. Some(Message::Scaled(scaling, None)),
  377. )
  378. } else {
  379. (event::Status::Captured, None)
  380. }
  381. }
  382. },
  383. _ => (event::Status::Ignored, None),
  384. },
  385. _ => (event::Status::Ignored, None),
  386. }
  387. }
  388. fn draw(
  389. &self,
  390. _state: &Self::State,
  391. renderer: &Renderer,
  392. _theme: &Theme,
  393. bounds: Rectangle,
  394. cursor: mouse::Cursor,
  395. ) -> Vec<Geometry> {
  396. 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);
  397. let earliest: i64 = latest - ((6400.0*self.timeframe as f32) / (self.scaling / (bounds.width/800.0))) as i64;
  398. let (visible_klines, highest, lowest, avg_body_height, max_volume, _) = self.klines_raw.iter()
  399. .filter(|(time, _)| {
  400. let timestamp = time.timestamp();
  401. timestamp >= earliest && timestamp <= latest
  402. })
  403. .fold((vec![], f32::MIN, f32::MAX, 0.0f32, 0.0f32, None), |(mut klines, highest, lowest, total_body_height, max_vol, latest_kline), (time, kline)| {
  404. let body_height = (kline.0 - kline.3).abs();
  405. klines.push((*time, *kline));
  406. let total_body_height = match latest_kline {
  407. Some(_) => total_body_height + body_height,
  408. None => total_body_height,
  409. };
  410. (
  411. klines,
  412. highest.max(kline.1),
  413. lowest.min(kline.2),
  414. total_body_height,
  415. max_vol.max(kline.4.max(kline.5)),
  416. Some(kline)
  417. )
  418. });
  419. if visible_klines.is_empty() || visible_klines.len() == 1 {
  420. return vec![];
  421. }
  422. let avg_body_height = avg_body_height / (visible_klines.len() - 1) as f32;
  423. let (highest, lowest) = (highest + avg_body_height, lowest - avg_body_height);
  424. let y_range = highest - lowest;
  425. let volume_area_height = bounds.height / 8.0;
  426. let candlesticks_area_height = bounds.height - volume_area_height;
  427. let y_labels_can_fit = (bounds.height / 32.0) as i32;
  428. let (step, rounded_lowest) = calculate_price_step(highest, lowest, y_labels_can_fit);
  429. let x_labels_can_fit = (bounds.width / 90.0) as i32;
  430. let (time_step, rounded_earliest) = calculate_time_step(earliest, latest, x_labels_can_fit);
  431. let background = self.mesh_cache.draw(renderer, bounds.size(), |frame| {
  432. frame.with_save(|frame| {
  433. let latest_in_millis = latest * 1000;
  434. let earliest_in_millis = earliest * 1000;
  435. let mut time = rounded_earliest;
  436. let latest_time = NaiveDateTime::from_timestamp(latest, 0);
  437. while time <= latest_time {
  438. let time_in_millis = time.timestamp_millis();
  439. let x_position = ((time_in_millis - earliest_in_millis) as f64 / (latest_in_millis - earliest_in_millis) as f64) * bounds.width as f64;
  440. if x_position >= 0.0 && x_position <= bounds.width as f64 {
  441. let line = Path::line(
  442. Point::new(x_position as f32, 0.0),
  443. Point::new(x_position as f32, bounds.height as f32)
  444. );
  445. frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(27, 27, 27, 1.0)).with_width(1.0))
  446. }
  447. time = time + time_step;
  448. }
  449. });
  450. frame.with_save(|frame| {
  451. let mut y = rounded_lowest;
  452. while y <= highest {
  453. let y_position = candlesticks_area_height - ((y - lowest) / y_range * candlesticks_area_height);
  454. let line = Path::line(
  455. Point::new(0.0, y_position),
  456. Point::new(bounds.width as f32, y_position)
  457. );
  458. frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(27, 27, 27, 1.0)).with_width(1.0));
  459. y += step;
  460. }
  461. });
  462. });
  463. let candlesticks = self.candles_cache.draw(renderer, bounds.size(), |frame| {
  464. for (time, (open, high, low, close, buy_volume, sell_volume)) in visible_klines {
  465. let x_position: f64 = ((time.timestamp() - earliest) as f64 / (latest - earliest) as f64) * bounds.width as f64;
  466. let y_open = candlesticks_area_height - ((open - lowest) / y_range * candlesticks_area_height);
  467. let y_high = candlesticks_area_height - ((high - lowest) / y_range * candlesticks_area_height);
  468. let y_low = candlesticks_area_height - ((low - lowest) / y_range * candlesticks_area_height);
  469. let y_close = candlesticks_area_height - ((close - lowest) / y_range * candlesticks_area_height);
  470. let color = if close >= open { Color::from_rgb8(81, 205, 160) } else { Color::from_rgb8(192, 80, 77) };
  471. let body = Path::rectangle(
  472. Point::new(x_position as f32 - (2.0 * self.scaling), y_open.min(y_close)),
  473. Size::new(4.0 * self.scaling, (y_open - y_close).abs())
  474. );
  475. frame.fill(&body, color);
  476. let wick = Path::line(
  477. Point::new(x_position as f32, y_high),
  478. Point::new(x_position as f32, y_low)
  479. );
  480. frame.stroke(&wick, Stroke::default().with_color(color).with_width(1.0));
  481. let buy_bar_height = (buy_volume / max_volume) * volume_area_height;
  482. let sell_bar_height = (sell_volume / max_volume) * volume_area_height;
  483. let buy_bar = Path::rectangle(
  484. Point::new(x_position as f32, (bounds.height - buy_bar_height) as f32),
  485. Size::new(2.0 * self.scaling, buy_bar_height as f32)
  486. );
  487. frame.fill(&buy_bar, Color::from_rgb8(81, 205, 160));
  488. let sell_bar = Path::rectangle(
  489. Point::new(x_position as f32 - (2.0 * self.scaling), (bounds.height - sell_bar_height) as f32),
  490. Size::new(2.0 * self.scaling, sell_bar_height as f32)
  491. );
  492. frame.fill(&sell_bar, Color::from_rgb8(192, 80, 77));
  493. }
  494. });
  495. if self.crosshair {
  496. let crosshair = self.crosshair_cache.draw(renderer, bounds.size(), |frame| {
  497. if let Some(cursor_position) = cursor.position_in(bounds) {
  498. let line = Path::line(
  499. Point::new(0.0, cursor_position.y),
  500. Point::new(bounds.width as f32, cursor_position.y)
  501. );
  502. frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(200, 200, 200, 0.6)).with_width(1.0));
  503. let crosshair_ratio = cursor_position.x as f64 / bounds.width as f64;
  504. let crosshair_millis = earliest as f64 * 1000.0 + crosshair_ratio * (latest as f64 * 1000.0 - earliest as f64 * 1000.0);
  505. let crosshair_time = NaiveDateTime::from_timestamp((crosshair_millis / 1000.0) as i64, 0);
  506. let crosshair_timestamp = crosshair_time.timestamp();
  507. let rounded_timestamp = (crosshair_timestamp as f64 / (self.timeframe as f64 * 60.0)).round() as i64 * self.timeframe as i64 * 60;
  508. let snap_ratio = (rounded_timestamp as f64 - earliest as f64) / ((latest as f64) - (earliest as f64));
  509. let snap_x = snap_ratio * bounds.width as f64;
  510. let line = Path::line(
  511. Point::new(snap_x as f32, 0.0),
  512. Point::new(snap_x as f32, bounds.height as f32)
  513. );
  514. frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(200, 200, 200, 0.6)).with_width(1.0));
  515. if let Some((_, kline)) = self.klines_raw.iter()
  516. .find(|(time, _)| time.timestamp() == rounded_timestamp) {
  517. let tooltip_text = format!(
  518. "O: {} H: {} L: {} C: {}\nBuyV: {:.0} SellV: {:.0}",
  519. kline.0, kline.1, kline.2, kline.3, kline.4, kline.5
  520. );
  521. let text = canvas::Text {
  522. content: tooltip_text,
  523. position: Point::new(10.0, 10.0),
  524. size: iced::Pixels(12.0),
  525. color: Color::from_rgba8(120, 120, 120, 1.0),
  526. ..canvas::Text::default()
  527. };
  528. frame.fill_text(text);
  529. }
  530. }
  531. });
  532. return vec![background, crosshair, candlesticks];
  533. } else {
  534. return vec![background, candlesticks];
  535. }
  536. }
  537. fn mouse_interaction(
  538. &self,
  539. interaction: &Interaction,
  540. bounds: Rectangle,
  541. cursor: mouse::Cursor,
  542. ) -> mouse::Interaction {
  543. match interaction {
  544. Interaction::Drawing => mouse::Interaction::Crosshair,
  545. Interaction::Erasing => mouse::Interaction::Crosshair,
  546. Interaction::Panning { .. } => mouse::Interaction::Grabbing,
  547. Interaction::None if cursor.is_over(bounds) => {
  548. if self.crosshair {
  549. mouse::Interaction::Crosshair
  550. } else {
  551. mouse::Interaction::default()
  552. }
  553. }
  554. Interaction::None => { mouse::Interaction::default() }
  555. }
  556. }
  557. }
  558. fn calculate_price_step(highest: f32, lowest: f32, labels_can_fit: i32) -> (f32, f32) {
  559. let range = highest - lowest;
  560. let mut step = 1000.0;
  561. 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];
  562. for &s in steps.iter().rev() {
  563. if range / s <= labels_can_fit as f32 {
  564. step = s;
  565. break;
  566. }
  567. }
  568. let rounded_lowest = (lowest / step).floor() * step;
  569. (step, rounded_lowest)
  570. }
  571. fn calculate_time_step(earliest: i64, latest: i64, labels_can_fit: i32) -> (Duration, NaiveDateTime) {
  572. let duration = latest - earliest;
  573. let duration_in_millis = duration * 1000;
  574. let steps = [
  575. Duration::days(1),
  576. Duration::hours(12),
  577. Duration::hours(8),
  578. Duration::hours(4),
  579. Duration::hours(2),
  580. Duration::minutes(60),
  581. Duration::minutes(30),
  582. Duration::minutes(15),
  583. Duration::minutes(10),
  584. Duration::minutes(5),
  585. Duration::minutes(1),
  586. ];
  587. let mut selected_step = steps[0];
  588. for &step in steps.iter() {
  589. if duration_in_millis / step.num_milliseconds() >= labels_can_fit as i64 {
  590. selected_step = step;
  591. break;
  592. }
  593. }
  594. let mut rounded_earliest = NaiveDateTime::from_timestamp(earliest, 0)
  595. .with_second(0).unwrap()
  596. .with_nanosecond(0).unwrap();
  597. let minutes = rounded_earliest.minute();
  598. let step_minutes = selected_step.num_minutes() as u32;
  599. let remainder = minutes % step_minutes;
  600. if remainder > 0 {
  601. rounded_earliest = rounded_earliest + Duration::minutes((step_minutes - remainder) as i64);
  602. }
  603. (selected_step, rounded_earliest)
  604. }
  605. pub struct AxisLabelXCanvas<'a> {
  606. labels_cache: &'a Cache,
  607. crosshair_cache: &'a Cache,
  608. crosshair_position: Point,
  609. crosshair: bool,
  610. min: i64,
  611. max: i64,
  612. timeframe: i16,
  613. }
  614. impl canvas::Program<Message> for AxisLabelXCanvas<'_> {
  615. type State = Interaction;
  616. fn update(
  617. &self,
  618. _interaction: &mut Interaction,
  619. _event: Event,
  620. _bounds: Rectangle,
  621. _cursor: mouse::Cursor,
  622. ) -> (event::Status, Option<Message>) {
  623. (event::Status::Ignored, None)
  624. }
  625. fn draw(
  626. &self,
  627. _state: &Self::State,
  628. renderer: &Renderer,
  629. _theme: &Theme,
  630. bounds: Rectangle,
  631. _cursor: mouse::Cursor,
  632. ) -> Vec<Geometry> {
  633. if self.max == 0 {
  634. return vec![];
  635. }
  636. let latest_in_millis = self.max * 1000;
  637. let earliest_in_millis = self.min * 1000;
  638. let x_labels_can_fit = (bounds.width / 90.0) as i32;
  639. let (time_step, rounded_earliest) = calculate_time_step(self.min, self.max, x_labels_can_fit);
  640. let labels = self.labels_cache.draw(renderer, bounds.size(), |frame| {
  641. frame.with_save(|frame| {
  642. let mut time = rounded_earliest;
  643. let latest_time = NaiveDateTime::from_timestamp(self.max, 0);
  644. while time <= latest_time {
  645. let time_in_millis = time.timestamp_millis();
  646. let x_position = ((time_in_millis - earliest_in_millis) as f64 / (latest_in_millis - earliest_in_millis) as f64) * bounds.width as f64;
  647. if x_position >= 0.0 && x_position <= bounds.width as f64 {
  648. let text_size = 12.0;
  649. let label = canvas::Text {
  650. content: time.format("%H:%M").to_string(),
  651. position: Point::new(x_position as f32 - text_size, bounds.height as f32 - 20.0),
  652. size: iced::Pixels(text_size),
  653. color: Color::from_rgba8(200, 200, 200, 1.0),
  654. ..canvas::Text::default()
  655. };
  656. label.draw_with(|path, color| {
  657. frame.fill(&path, color);
  658. });
  659. }
  660. time = time + time_step;
  661. }
  662. });
  663. });
  664. let crosshair = self.crosshair_cache.draw(renderer, bounds.size(), |frame| {
  665. if self.crosshair && self.crosshair_position.x > 0.0 {
  666. let crosshair_ratio = self.crosshair_position.x as f64 / bounds.width as f64;
  667. let crosshair_millis = earliest_in_millis as f64 + crosshair_ratio * (latest_in_millis - earliest_in_millis) as f64;
  668. let crosshair_time = NaiveDateTime::from_timestamp((crosshair_millis / 1000.0) as i64, 0);
  669. let crosshair_timestamp = crosshair_time.timestamp();
  670. let rounded_timestamp = (crosshair_timestamp as f64 / (self.timeframe as f64 * 60.0)).round() as i64 * self.timeframe as i64 * 60;
  671. let rounded_time = NaiveDateTime::from_timestamp(rounded_timestamp, 0);
  672. let snap_ratio = (rounded_timestamp as f64 * 1000.0 - earliest_in_millis as f64) / (latest_in_millis as f64 - earliest_in_millis as f64);
  673. let snap_x = snap_ratio * bounds.width as f64;
  674. let text_size = 12.0;
  675. let text_content = rounded_time.format("%H:%M").to_string();
  676. let growth_amount = 6.0;
  677. let rectangle_position = Point::new(snap_x as f32 - 14.0 - growth_amount, bounds.height as f32 - 20.0);
  678. let text_position = Point::new(snap_x as f32 - 14.0, bounds.height as f32 - 20.0);
  679. 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));
  680. frame.fill(&text_background, Color::from_rgba8(200, 200, 200, 1.0));
  681. let crosshair_label = canvas::Text {
  682. content: text_content,
  683. position: text_position,
  684. size: iced::Pixels(text_size),
  685. color: Color::from_rgba8(0, 0, 0, 1.0),
  686. ..canvas::Text::default()
  687. };
  688. crosshair_label.draw_with(|path, color| {
  689. frame.fill(&path, color);
  690. });
  691. }
  692. });
  693. vec![labels, crosshair]
  694. }
  695. fn mouse_interaction(
  696. &self,
  697. interaction: &Interaction,
  698. bounds: Rectangle,
  699. cursor: mouse::Cursor,
  700. ) -> mouse::Interaction {
  701. match interaction {
  702. Interaction::Drawing => mouse::Interaction::Crosshair,
  703. Interaction::Erasing => mouse::Interaction::Crosshair,
  704. Interaction::Panning { .. } => mouse::Interaction::ResizingHorizontally,
  705. Interaction::None if cursor.is_over(bounds) => {
  706. mouse::Interaction::ResizingHorizontally
  707. }
  708. Interaction::None => mouse::Interaction::default(),
  709. }
  710. }
  711. }
  712. pub struct AxisLabelYCanvas<'a> {
  713. labels_cache: &'a Cache,
  714. y_croshair_cache: &'a Cache,
  715. min: f32,
  716. max: f32,
  717. last_close_price: f32,
  718. last_open_price: f32,
  719. crosshair_position: Point,
  720. crosshair: bool,
  721. }
  722. impl canvas::Program<Message> for AxisLabelYCanvas<'_> {
  723. type State = Interaction;
  724. fn update(
  725. &self,
  726. _interaction: &mut Interaction,
  727. _event: Event,
  728. _bounds: Rectangle,
  729. _cursor: mouse::Cursor,
  730. ) -> (event::Status, Option<Message>) {
  731. (event::Status::Ignored, None)
  732. }
  733. fn draw(
  734. &self,
  735. _state: &Self::State,
  736. renderer: &Renderer,
  737. _theme: &Theme,
  738. bounds: Rectangle,
  739. _cursor: mouse::Cursor,
  740. ) -> Vec<Geometry> {
  741. if self.max == 0.0 {
  742. return vec![];
  743. }
  744. let y_labels_can_fit = (bounds.height / 32.0) as i32;
  745. let (step, rounded_lowest) = calculate_price_step(self.max, self.min, y_labels_can_fit);
  746. let volume_area_height = bounds.height / 8.0;
  747. let candlesticks_area_height = bounds.height - volume_area_height;
  748. let labels = self.labels_cache.draw(renderer, bounds.size(), |frame| {
  749. frame.with_save(|frame| {
  750. let y_range = self.max - self.min;
  751. let mut y = rounded_lowest;
  752. while y <= self.max {
  753. let y_position = candlesticks_area_height - ((y - self.min) / y_range * candlesticks_area_height);
  754. let text_size = 12.0;
  755. let decimal_places = if step.fract() == 0.0 { 0 } else { 1 };
  756. let label_content = match decimal_places {
  757. 0 => format!("{:.0}", y),
  758. _ => format!("{:.1}", y),
  759. };
  760. let label = canvas::Text {
  761. content: label_content,
  762. position: Point::new(10.0, y_position - text_size / 2.0),
  763. size: iced::Pixels(text_size),
  764. color: Color::from_rgba8(200, 200, 200, 1.0),
  765. ..canvas::Text::default()
  766. };
  767. label.draw_with(|path, color| {
  768. frame.fill(&path, color);
  769. });
  770. y += step;
  771. }
  772. let last_close_y_position = candlesticks_area_height - ((self.last_close_price - self.min) / y_range * candlesticks_area_height);
  773. let triangle_color = if self.last_close_price >= self.last_open_price {
  774. Color::from_rgba8(81, 205, 160, 0.9)
  775. } else {
  776. Color::from_rgba8(192, 80, 77, 0.9)
  777. };
  778. let triangle = Path::new(|path| {
  779. path.move_to(Point::new(5.0, last_close_y_position));
  780. path.line_to(Point::new(0.0, last_close_y_position - 5.0));
  781. path.line_to(Point::new(0.0, last_close_y_position + 5.0));
  782. path.close();
  783. });
  784. frame.fill(&triangle, triangle_color);
  785. });
  786. });
  787. let crosshair = self.y_croshair_cache.draw(renderer, bounds.size(), |frame| {
  788. if self.crosshair && self.crosshair_position.y > 0.0 {
  789. let text_size = 12.0;
  790. let y_range = self.max - self.min;
  791. let label_content = format!("{:.1}", self.min + (y_range * (candlesticks_area_height - self.crosshair_position.y) / candlesticks_area_height));
  792. let growth_amount = 3.0;
  793. let rectangle_position = Point::new(8.0 - growth_amount, self.crosshair_position.y - text_size / 2.0 - 3.0);
  794. let text_position = Point::new(8.0, self.crosshair_position.y - text_size / 2.0 - 3.0);
  795. 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));
  796. frame.fill(&text_background, Color::from_rgba8(200, 200, 200, 1.0));
  797. let label = canvas::Text {
  798. content: label_content,
  799. position: text_position,
  800. size: iced::Pixels(text_size),
  801. color: Color::from_rgba8(0, 0, 0, 1.0),
  802. ..canvas::Text::default()
  803. };
  804. label.draw_with(|path, color| {
  805. frame.fill(&path, color);
  806. });
  807. }
  808. });
  809. vec![labels, crosshair]
  810. }
  811. fn mouse_interaction(
  812. &self,
  813. interaction: &Interaction,
  814. bounds: Rectangle,
  815. cursor: mouse::Cursor,
  816. ) -> mouse::Interaction {
  817. match interaction {
  818. Interaction::Drawing => mouse::Interaction::Crosshair,
  819. Interaction::Erasing => mouse::Interaction::Crosshair,
  820. Interaction::Panning { .. } => mouse::Interaction::ResizingVertically,
  821. Interaction::None if cursor.is_over(bounds) => {
  822. mouse::Interaction::ResizingVertically
  823. }
  824. Interaction::None => mouse::Interaction::default(),
  825. }
  826. }
  827. }