volume.rs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. use std::collections::BTreeMap;
  2. use iced::widget::{container, row, Canvas};
  3. use iced::{Element, Length};
  4. use iced::{mouse, Point, Rectangle, Renderer, Size, Theme, Vector};
  5. use iced::widget::canvas::{self, Cache, Event, Geometry, LineDash, Path, Stroke};
  6. use crate::charts::{
  7. calc_price_step, convert_to_qty_abbr, round_to_tick, AxisLabel, Caches, CommonChartData, Interaction, Label, Message
  8. };
  9. use crate::data_providers::format_with_commas;
  10. pub fn create_indicator_elem<'a>(
  11. chart_state: &'a CommonChartData,
  12. cache: &'a Caches,
  13. data: &'a BTreeMap<i64, (f32, f32)>,
  14. earliest: i64,
  15. latest: i64,
  16. ) -> Element<'a, Message> {
  17. let indi_chart = Canvas::new(VolumeIndicator {
  18. indicator_cache: &cache.main,
  19. crosshair_cache: &cache.crosshair,
  20. crosshair: chart_state.crosshair,
  21. max: chart_state.latest_x,
  22. scaling: chart_state.scaling,
  23. translation_x: chart_state.translation.x,
  24. timeframe: chart_state.timeframe as u32,
  25. cell_width: chart_state.cell_width,
  26. data_points: data,
  27. chart_bounds: chart_state.bounds,
  28. })
  29. .height(Length::Fill)
  30. .width(Length::Fill);
  31. let max_volume = data
  32. .range(earliest..=latest)
  33. .map(|(_, (buy, sell))| buy.max(*sell))
  34. .max_by(|a, b| a.partial_cmp(b).unwrap())
  35. .unwrap_or(0.0);
  36. let indi_labels = Canvas::new(VolumeLabels {
  37. label_cache: &cache.y_labels,
  38. max: max_volume,
  39. min: 0.0,
  40. crosshair: chart_state.crosshair,
  41. chart_bounds: chart_state.bounds,
  42. })
  43. .height(Length::Fill)
  44. .width(Length::Fixed(60.0 + (chart_state.decimals as f32 * 2.0)));
  45. row![
  46. indi_chart,
  47. container(indi_labels),
  48. ].into()
  49. }
  50. pub struct VolumeIndicator<'a> {
  51. pub indicator_cache: &'a Cache,
  52. pub crosshair_cache: &'a Cache,
  53. pub crosshair: bool,
  54. pub max: i64,
  55. pub scaling: f32,
  56. pub translation_x: f32,
  57. pub timeframe: u32,
  58. pub cell_width: f32,
  59. pub data_points: &'a BTreeMap<i64, (f32, f32)>,
  60. pub chart_bounds: Rectangle,
  61. }
  62. impl VolumeIndicator<'_> {
  63. fn visible_region(&self, size: Size) -> Rectangle {
  64. let width = size.width / self.scaling;
  65. let height = size.height / self.scaling;
  66. Rectangle {
  67. x: -self.translation_x - width / 2.0,
  68. y: 0.0,
  69. width,
  70. height,
  71. }
  72. }
  73. fn x_to_time(&self, x: f32) -> i64 {
  74. let time_per_cell = self.timeframe;
  75. self.max + ((x / self.cell_width) * time_per_cell as f32) as i64
  76. }
  77. fn time_to_x(&self, time: i64) -> f32 {
  78. let time_per_cell = self.timeframe;
  79. let x = (time - self.max) as f32 / time_per_cell as f32;
  80. x * self.cell_width
  81. }
  82. }
  83. impl canvas::Program<Message> for VolumeIndicator<'_> {
  84. type State = Interaction;
  85. fn update(
  86. &self,
  87. interaction: &mut Interaction,
  88. event: Event,
  89. bounds: Rectangle,
  90. cursor: mouse::Cursor,
  91. ) -> Option<canvas::Action<Message>> {
  92. match event {
  93. Event::Mouse(mouse::Event::CursorMoved { .. }) => {
  94. let message = match *interaction {
  95. Interaction::None => {
  96. if self.crosshair && cursor.is_over(bounds) {
  97. Some(Message::CrosshairMoved)
  98. } else {
  99. None
  100. }
  101. }
  102. _ => None,
  103. };
  104. let action =
  105. message.map_or(canvas::Action::request_redraw(), canvas::Action::publish);
  106. Some(match interaction {
  107. Interaction::None => action,
  108. _ => action.and_capture(),
  109. })
  110. }
  111. _ => None,
  112. }
  113. }
  114. fn draw(
  115. &self,
  116. _state: &Self::State,
  117. renderer: &Renderer,
  118. theme: &Theme,
  119. bounds: Rectangle,
  120. cursor: mouse::Cursor,
  121. ) -> Vec<Geometry> {
  122. if self.data_points.is_empty() {
  123. return vec![];
  124. }
  125. let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0);
  126. let palette = theme.extended_palette();
  127. let indicator = self.indicator_cache.draw(renderer, bounds.size(), |frame| {
  128. frame.translate(center);
  129. frame.scale(self.scaling);
  130. frame.translate(Vector::new(
  131. self.translation_x,
  132. (-bounds.height / self.scaling) / 2.0,
  133. ));
  134. let region = self.visible_region(frame.size());
  135. frame.fill_rectangle(
  136. Point::new(region.x, 0.0),
  137. Size::new(region.width, 1.0 / self.scaling),
  138. if palette.is_dark {
  139. palette.background.weak.color.scale_alpha(0.2)
  140. } else {
  141. palette.background.strong.color.scale_alpha(0.2)
  142. },
  143. );
  144. let (earliest, latest) = (
  145. self.x_to_time(region.x) - i64::from(self.timeframe / 2),
  146. self.x_to_time(region.x + region.width) + i64::from(self.timeframe / 2),
  147. );
  148. let mut max_volume: f32 = 0.0;
  149. self.data_points
  150. .range(earliest..=latest)
  151. .for_each(|(_, (buy_volume, sell_volume))| {
  152. max_volume = max_volume.max(buy_volume.max(*sell_volume));
  153. });
  154. self.data_points.range(earliest..=latest).for_each(
  155. |(timestamp, (buy_volume, sell_volume))| {
  156. let x_position = self.time_to_x(*timestamp);
  157. if max_volume > 0.0 {
  158. if *buy_volume != -1.0 {
  159. let buy_bar_height =
  160. (buy_volume / max_volume) * (bounds.height / self.scaling);
  161. let sell_bar_height =
  162. (sell_volume / max_volume) * (bounds.height / self.scaling);
  163. let bar_width = (self.cell_width / 2.0) * 0.9;
  164. frame.fill_rectangle(
  165. Point::new(
  166. x_position - bar_width,
  167. (region.y + region.height) - sell_bar_height,
  168. ),
  169. Size::new(bar_width, sell_bar_height),
  170. palette.danger.base.color,
  171. );
  172. frame.fill_rectangle(
  173. Point::new(x_position, (region.y + region.height) - buy_bar_height),
  174. Size::new(bar_width, buy_bar_height),
  175. palette.success.base.color,
  176. );
  177. } else {
  178. let bar_height =
  179. (sell_volume / max_volume) * (bounds.height / self.scaling);
  180. let bar_width = self.cell_width * 0.9;
  181. frame.fill_rectangle(
  182. Point::new(
  183. x_position - (bar_width / 2.0),
  184. (bounds.height / self.scaling) - bar_height,
  185. ),
  186. Size::new(bar_width, bar_height),
  187. palette.secondary.strong.color,
  188. );
  189. }
  190. }
  191. },
  192. );
  193. });
  194. if self.crosshair {
  195. let crosshair = self.crosshair_cache.draw(renderer, bounds.size(), |frame| {
  196. let dashed_line = Stroke::with_color(
  197. Stroke {
  198. width: 1.0,
  199. line_dash: LineDash {
  200. segments: &[4.0, 4.0],
  201. offset: 8,
  202. },
  203. ..Default::default()
  204. },
  205. palette.secondary.strong.color
  206. .scale_alpha(
  207. if palette.is_dark {
  208. 0.6
  209. } else {
  210. 1.0
  211. },
  212. ),
  213. );
  214. if let Some(cursor_position) = cursor.position_in(self.chart_bounds) {
  215. let region = self.visible_region(frame.size());
  216. // Vertical time line
  217. let earliest = self.x_to_time(region.x) as f64;
  218. let latest = self.x_to_time(region.x + region.width) as f64;
  219. let crosshair_ratio = f64::from(cursor_position.x / bounds.width);
  220. let crosshair_millis = earliest + crosshair_ratio * (latest - earliest);
  221. let rounded_timestamp =
  222. (crosshair_millis / (self.timeframe as f64)).round() as i64 * self.timeframe as i64;
  223. let snap_ratio = ((rounded_timestamp as f64 - earliest) / (latest - earliest)) as f32;
  224. frame.stroke(
  225. &Path::line(
  226. Point::new(snap_ratio * bounds.width, 0.0),
  227. Point::new(snap_ratio * bounds.width, bounds.height),
  228. ),
  229. dashed_line,
  230. );
  231. if let Some((_, (buy_v, sell_v))) = self
  232. .data_points
  233. .iter()
  234. .find(|(time, _)| **time == rounded_timestamp)
  235. {
  236. let mut tooltip_bg_height = 28.0;
  237. let tooltip_text: String = if *buy_v != -1.0 {
  238. format!(
  239. "Buy Volume: {}\nSell Volume: {}",
  240. format_with_commas(*buy_v),
  241. format_with_commas(*sell_v),
  242. )
  243. } else {
  244. tooltip_bg_height = 14.0;
  245. format!(
  246. "Volume: {}",
  247. format_with_commas(*sell_v),
  248. )
  249. };
  250. let text = canvas::Text {
  251. content: tooltip_text,
  252. position: Point::new(8.0, 2.0),
  253. size: iced::Pixels(10.0),
  254. color: palette.background.base.text,
  255. ..canvas::Text::default()
  256. };
  257. frame.fill_text(text);
  258. frame.fill_rectangle(
  259. Point::new(4.0, 0.0),
  260. Size::new(140.0, tooltip_bg_height),
  261. palette.background.base.color,
  262. );
  263. }
  264. } else if let Some(cursor_position) = cursor.position_in(bounds) {
  265. // Horizontal price line
  266. let highest = self.max as f32;
  267. let lowest = 0.0;
  268. let crosshair_ratio = cursor_position.y / bounds.height;
  269. let crosshair_price = highest + crosshair_ratio * (lowest - highest);
  270. let rounded_price = round_to_tick(crosshair_price, 1.0);
  271. let snap_ratio = (rounded_price - highest) / (lowest - highest);
  272. frame.stroke(
  273. &Path::line(
  274. Point::new(0.0, snap_ratio * bounds.height),
  275. Point::new(bounds.width, snap_ratio * bounds.height),
  276. ),
  277. dashed_line,
  278. );
  279. }
  280. });
  281. vec![indicator, crosshair]
  282. } else {
  283. vec![indicator]
  284. }
  285. }
  286. fn mouse_interaction(
  287. &self,
  288. interaction: &Interaction,
  289. bounds: Rectangle,
  290. cursor: mouse::Cursor,
  291. ) -> mouse::Interaction {
  292. match interaction {
  293. Interaction::Panning { .. } => mouse::Interaction::Grabbing,
  294. Interaction::Zoomin { .. } => mouse::Interaction::ZoomIn,
  295. Interaction::None if cursor.is_over(bounds) => {
  296. if self.crosshair {
  297. mouse::Interaction::Crosshair
  298. } else {
  299. mouse::Interaction::default()
  300. }
  301. }
  302. _ => mouse::Interaction::default(),
  303. }
  304. }
  305. }
  306. pub struct VolumeLabels<'a> {
  307. pub label_cache: &'a Cache,
  308. pub crosshair: bool,
  309. pub max: f32,
  310. pub min: f32,
  311. pub chart_bounds: Rectangle,
  312. }
  313. impl canvas::Program<Message> for VolumeLabels<'_> {
  314. type State = Interaction;
  315. fn update(
  316. &self,
  317. _state: &mut Self::State,
  318. _event: Event,
  319. _bounds: Rectangle,
  320. _cursor: mouse::Cursor,
  321. ) -> Option<canvas::Action<Message>> {
  322. None
  323. }
  324. fn draw(
  325. &self,
  326. _state: &Self::State,
  327. renderer: &Renderer,
  328. theme: &Theme,
  329. bounds: Rectangle,
  330. cursor: mouse::Cursor,
  331. ) -> Vec<Geometry> {
  332. let palette = theme.extended_palette();
  333. let highest = self.max;
  334. let lowest = self.min;
  335. let text_size = 12.0;
  336. let labels = self.label_cache.draw(renderer, bounds.size(), |frame| {
  337. frame.fill_rectangle(
  338. Point::new(0.0, 0.0),
  339. Size::new(bounds.width, 1.0),
  340. if palette.is_dark {
  341. palette.background.weak.color.scale_alpha(0.2)
  342. } else {
  343. palette.background.strong.color.scale_alpha(0.2)
  344. },
  345. );
  346. frame.fill_rectangle(
  347. Point::new(0.0, 0.0),
  348. Size::new(1.0, bounds.height),
  349. if palette.is_dark {
  350. palette.background.weak.color.scale_alpha(0.4)
  351. } else {
  352. palette.background.strong.color.scale_alpha(0.4)
  353. },
  354. );
  355. let y_range = highest - lowest;
  356. let y_labels_can_fit: i32 = (bounds.height / (text_size * 2.0)) as i32;
  357. let mut all_labels: Vec<AxisLabel> =
  358. Vec::with_capacity((y_labels_can_fit + 2) as usize); // +2 for last_price and crosshair
  359. let rect = |y_pos: f32, label_amt: i16| {
  360. let label_offset = text_size + (f32::from(label_amt) * (text_size / 2.0) + 2.0);
  361. Rectangle {
  362. x: 6.0,
  363. y: y_pos - label_offset / 2.0,
  364. width: bounds.width - 8.0,
  365. height: label_offset,
  366. }
  367. };
  368. // Regular price labels (priority 1)
  369. let (step, rounded_lowest) = calc_price_step(highest, lowest, y_labels_can_fit, 1.0);
  370. let mut y = rounded_lowest;
  371. while y <= highest {
  372. let y_position = bounds.height - ((y - lowest) / y_range * bounds.height);
  373. if y > 0.0 {
  374. let text_content = convert_to_qty_abbr(y);
  375. let label = Label {
  376. content: text_content,
  377. background_color: None,
  378. marker_color: if palette.is_dark {
  379. palette.background.weak.color.scale_alpha(0.6)
  380. } else {
  381. palette.background.strong.color.scale_alpha(0.6)
  382. },
  383. text_color: palette.background.base.text,
  384. text_size: 12.0,
  385. };
  386. all_labels.push(AxisLabel::Y(rect(y_position, 1), label, None));
  387. }
  388. y += step;
  389. }
  390. // Crosshair price (priority 3)
  391. if self.crosshair {
  392. let common_bounds = Rectangle {
  393. x: self.chart_bounds.x,
  394. y: bounds.y,
  395. width: self.chart_bounds.width,
  396. height: bounds.height,
  397. };
  398. if let Some(crosshair_pos) = cursor.position_in(common_bounds) {
  399. let raw_price =
  400. lowest + (y_range * (bounds.height - crosshair_pos.y) / bounds.height);
  401. let rounded_price = round_to_tick(raw_price, 1.0);
  402. let y_position =
  403. bounds.height - ((rounded_price - lowest) / y_range * bounds.height);
  404. let text_content = convert_to_qty_abbr(rounded_price);
  405. let label = Label {
  406. content: text_content,
  407. background_color: Some(palette.secondary.base.color),
  408. marker_color: palette.background.strong.color,
  409. text_color: palette.secondary.base.text,
  410. text_size: 12.0,
  411. };
  412. all_labels.push(AxisLabel::Y(rect(y_position, 1), label, None));
  413. }
  414. }
  415. AxisLabel::filter_and_draw(&all_labels, frame);
  416. });
  417. vec![labels]
  418. }
  419. fn mouse_interaction(
  420. &self,
  421. interaction: &Interaction,
  422. bounds: Rectangle,
  423. cursor: mouse::Cursor,
  424. ) -> mouse::Interaction {
  425. match interaction {
  426. Interaction::Zoomin { .. } => mouse::Interaction::ResizingVertically,
  427. Interaction::Panning { .. } => mouse::Interaction::None,
  428. Interaction::None if cursor.is_over(bounds) => mouse::Interaction::ResizingVertically,
  429. _ => mouse::Interaction::default(),
  430. }
  431. }
  432. }