heatmap.rs 34 KB

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