1use crate::error::{ChartError, Result};
4use crate::style::LayoutStyle;
5use chrono::{DateTime, Utc};
6use glam::{Mat3, Vec2};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
11pub struct Rect {
12 pub x: f32,
13 pub y: f32,
14 pub width: f32,
15 pub height: f32,
16}
17
18impl Rect {
19 pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
20 Self {
21 x,
22 y,
23 width,
24 height,
25 }
26 }
27
28 pub fn from_size(width: f32, height: f32) -> Self {
29 Self::new(0.0, 0.0, width, height)
30 }
31
32 pub fn right(&self) -> f32 {
33 self.x + self.width
34 }
35
36 pub fn bottom(&self) -> f32 {
37 self.y + self.height
38 }
39
40 pub fn center(&self) -> Vec2 {
41 Vec2::new(self.x + self.width * 0.5, self.y + self.height * 0.5)
42 }
43
44 pub fn contains_point(&self, point: Vec2) -> bool {
45 point.x >= self.x
46 && point.x <= self.right()
47 && point.y >= self.y
48 && point.y <= self.bottom()
49 }
50
51 pub fn intersects(&self, other: &Rect) -> bool {
52 self.x < other.right()
53 && self.right() > other.x
54 && self.y < other.bottom()
55 && self.bottom() > other.y
56 }
57
58 pub fn shrink(&self, margin: f32) -> Rect {
60 Rect::new(
61 self.x + margin,
62 self.y + margin,
63 (self.width - 2.0 * margin).max(0.0),
64 (self.height - 2.0 * margin).max(0.0),
65 )
66 }
67
68 pub fn split_horizontal(&self, ratio: f32) -> (Rect, Rect) {
70 let split_y = self.y + self.height * ratio.clamp(0.0, 1.0);
71 let top_height = split_y - self.y;
72 let bottom_height = self.bottom() - split_y;
73
74 let top = Rect::new(self.x, self.y, self.width, top_height);
75 let bottom = Rect::new(self.x, split_y, self.width, bottom_height);
76
77 (top, bottom)
78 }
79
80 pub fn intersection(&self, other: &Rect) -> Option<Rect> {
82 let x1 = self.x.max(other.x);
83 let y1 = self.y.max(other.y);
84 let x2 = self.right().min(other.right());
85 let y2 = self.bottom().min(other.bottom());
86
87 if x2 > x1 && y2 > y1 {
88 Some(Rect::new(x1, y1, x2 - x1, y2 - y1))
89 } else {
90 None
91 }
92 }
93}
94
95#[derive(Debug, Clone)]
97pub struct ChartLayout {
98 pub main_panel: Rect,
99 pub volume_panel: Rect,
100 pub price_axis_panel: Rect,
101 pub time_axis_panel: Rect,
102 pub full_rect: Rect,
103}
104
105impl ChartLayout {
106 pub fn new(full_rect: Rect, style: &LayoutStyle) -> Self {
108 let price_axis_width = style.price_axis_width.max(40.0);
109 let time_axis_height = style.time_axis_height.max(24.0);
110 let volume_height_ratio = style.volume_height_ratio.clamp(0.05, 0.5);
111 let volume_gap = style.volume_gap.max(0.0);
112 let chart_padding_x = style.chart_padding_x.max(0.0);
113 let chart_padding_y = style.chart_padding_y.max(0.0);
114
115 let chart_area_x = full_rect.x + chart_padding_x;
116 let chart_area_y = full_rect.y + chart_padding_y;
117 let chart_area_width =
118 (full_rect.width - price_axis_width - 2.0 * chart_padding_x).max(1.0);
119 let chart_area_height =
120 (full_rect.height - time_axis_height - 2.0 * chart_padding_y).max(1.0);
121
122 let volume_height = (chart_area_height * volume_height_ratio).max(24.0);
123 let main_height = (chart_area_height - volume_height - volume_gap).max(1.0);
124
125 let main_panel = Rect::new(chart_area_x, chart_area_y, chart_area_width, main_height);
126
127 let volume_panel = Rect::new(
128 chart_area_x,
129 chart_area_y + main_height + volume_gap,
130 chart_area_width,
131 volume_height,
132 );
133
134 let price_axis_panel = Rect::new(
135 chart_area_x + chart_area_width,
136 chart_area_y,
137 price_axis_width,
138 main_height,
139 );
140
141 let time_axis_panel = Rect::new(
142 full_rect.x,
143 volume_panel.y + volume_panel.height,
144 full_rect.width,
145 time_axis_height,
146 );
147
148 Self {
149 main_panel,
150 volume_panel,
151 price_axis_panel,
152 time_axis_panel,
153 full_rect,
154 }
155 }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
160pub struct ChartBounds {
161 pub time_start: DateTime<Utc>,
162 pub time_end: DateTime<Utc>,
163 pub price_min: f64,
164 pub price_max: f64,
165}
166
167impl ChartBounds {
168 pub fn new(
169 time_start: DateTime<Utc>,
170 time_end: DateTime<Utc>,
171 price_min: f64,
172 price_max: f64,
173 ) -> Result<Self> {
174 if time_start >= time_end {
175 return Err(ChartError::data_range("Start time must be before end time"));
176 }
177
178 if price_min >= price_max {
179 return Err(ChartError::data_range(
180 "Min price must be less than max price",
181 ));
182 }
183
184 Ok(Self {
185 time_start,
186 time_end,
187 price_min,
188 price_max,
189 })
190 }
191
192 pub fn time_duration(&self) -> chrono::Duration {
193 self.time_end - self.time_start
194 }
195
196 pub fn price_range(&self) -> f64 {
197 self.price_max - self.price_min
198 }
199
200 pub fn contains_time(&self, time: DateTime<Utc>) -> bool {
201 time >= self.time_start && time <= self.time_end
202 }
203
204 pub fn contains_price(&self, price: f64) -> bool {
205 price >= self.price_min && price <= self.price_max
206 }
207
208 pub fn expand_to_include(&mut self, time: DateTime<Utc>, price: f64) {
210 if time < self.time_start {
211 self.time_start = time;
212 }
213 if time > self.time_end {
214 self.time_end = time;
215 }
216 if price < self.price_min {
217 self.price_min = price;
218 }
219 if price > self.price_max {
220 self.price_max = price;
221 }
222 }
223
224 pub fn with_padding(&self, time_padding: f64, price_padding: f64) -> Result<Self> {
226 let time_range_seconds = self.time_duration().num_seconds() as f64;
227 let time_padding_seconds = (time_range_seconds * time_padding) as i64;
228
229 let price_range = self.price_range();
230 let price_padding_amount = price_range * price_padding;
231
232 ChartBounds::new(
233 self.time_start - chrono::Duration::seconds(time_padding_seconds),
234 self.time_end + chrono::Duration::seconds(time_padding_seconds),
235 self.price_min - price_padding_amount,
236 self.price_max + price_padding_amount,
237 )
238 }
239}
240
241#[derive(Debug, Clone)]
243pub struct Viewport {
244 pub screen_rect: Rect,
246 pub chart_bounds: ChartBounds,
248 pub layout: ChartLayout,
250 layout_style: LayoutStyle,
251 transform: Mat3,
253 inverse_transform: Mat3,
255}
256
257impl Viewport {
258 pub fn new(screen_rect: Rect, chart_bounds: ChartBounds, layout_style: LayoutStyle) -> Self {
259 let layout = ChartLayout::new(screen_rect, &layout_style);
260 let mut viewport = Self {
261 screen_rect,
262 chart_bounds,
263 layout,
264 layout_style,
265 transform: Mat3::IDENTITY,
266 inverse_transform: Mat3::IDENTITY,
267 };
268 viewport.update_transforms();
269 viewport
270 }
271
272 pub fn chart_content_rect(&self) -> Rect {
274 self.layout.main_panel
275 }
276
277 pub fn price_axis_rect(&self) -> Rect {
279 self.layout.price_axis_panel
280 }
281
282 pub fn time_axis_rect(&self) -> Rect {
284 self.layout.time_axis_panel
285 }
286
287 pub fn volume_rect(&self) -> Rect {
289 self.layout.volume_panel
290 }
291
292 pub fn set_screen_rect(&mut self, rect: Rect) {
294 self.screen_rect = rect;
295 self.layout = ChartLayout::new(rect, &self.layout_style);
296 self.update_transforms();
297 }
298
299 pub fn set_layout_style(&mut self, style: LayoutStyle) {
301 self.layout_style = style;
302 self.layout = ChartLayout::new(self.screen_rect, &self.layout_style);
303 self.update_transforms();
304 }
305
306 pub fn set_chart_bounds(&mut self, bounds: ChartBounds) {
308 self.chart_bounds = bounds;
309 self.update_transforms();
310 }
311
312 pub fn pan(&mut self, screen_delta: Vec2) {
314 let chart_delta = self.screen_to_chart_delta(screen_delta);
316
317 let time_delta_seconds = chart_delta.x as i64;
319 let price_delta = chart_delta.y as f64;
320
321 if let Ok(new_bounds) = ChartBounds::new(
322 self.chart_bounds.time_start + chrono::Duration::seconds(time_delta_seconds),
323 self.chart_bounds.time_end + chrono::Duration::seconds(time_delta_seconds),
324 self.chart_bounds.price_min + price_delta,
325 self.chart_bounds.price_max + price_delta,
326 ) {
327 self.chart_bounds = new_bounds;
328 self.update_transforms();
329 }
330 }
331
332 pub fn zoom(&mut self, center_screen: Vec2, zoom_factor: f32) {
334 let center_chart = self.screen_to_chart(center_screen);
336
337 let time_range = self.chart_bounds.time_duration().num_seconds() as f64;
339 let price_range = self.chart_bounds.price_range();
340
341 let new_time_range = time_range / zoom_factor as f64;
342 let new_price_range = price_range / zoom_factor as f64;
343
344 let time_center_offset =
346 (center_chart.x as f64 - self.chart_bounds.time_start.timestamp() as f64) / time_range;
347 let price_center_offset =
348 (center_chart.y as f64 - self.chart_bounds.price_min) / price_range;
349
350 let new_time_start = center_chart.x as i64 - (new_time_range * time_center_offset) as i64;
351 let new_time_end =
352 center_chart.x as i64 + (new_time_range * (1.0 - time_center_offset)) as i64;
353
354 let new_price_min = center_chart.y as f64 - new_price_range * price_center_offset;
355 let new_price_max = center_chart.y as f64 + new_price_range * (1.0 - price_center_offset);
356
357 if let (Some(start_time), Some(end_time)) = (
358 DateTime::from_timestamp(new_time_start, 0),
359 DateTime::from_timestamp(new_time_end, 0),
360 ) {
361 if let Ok(new_bounds) =
362 ChartBounds::new(start_time, end_time, new_price_min, new_price_max)
363 {
364 self.chart_bounds = new_bounds;
365 self.update_transforms();
366 }
367 }
368 }
369
370 pub fn chart_to_screen(&self, chart_pos: Vec2) -> Vec2 {
372 let homogeneous = self.transform * chart_pos.extend(1.0);
373 Vec2::new(homogeneous.x, homogeneous.y)
374 }
375
376 pub fn screen_to_chart(&self, screen_pos: Vec2) -> Vec2 {
378 let homogeneous = self.inverse_transform * screen_pos.extend(1.0);
379 Vec2::new(homogeneous.x, homogeneous.y)
380 }
381
382 pub fn screen_to_chart_delta(&self, screen_delta: Vec2) -> Vec2 {
384 let origin = self.screen_to_chart(Vec2::ZERO);
385 let target = self.screen_to_chart(screen_delta);
386 target - origin
387 }
388
389 pub fn is_chart_pos_visible(&self, chart_pos: Vec2) -> bool {
391 let screen_pos = self.chart_to_screen(chart_pos);
392 self.screen_rect.contains_point(screen_pos)
393 }
394
395 pub fn visible_time_range(&self) -> (i64, i64) {
397 (
398 self.chart_bounds.time_start.timestamp(),
399 self.chart_bounds.time_end.timestamp(),
400 )
401 }
402
403 pub fn visible_price_range(&self) -> (f64, f64) {
405 (self.chart_bounds.price_min, self.chart_bounds.price_max)
406 }
407
408 pub fn chart_to_screen_x(&self, chart_x: f32) -> f32 {
410 let chart_pos = Vec2::new(chart_x, 0.0);
411 let screen_pos = self.chart_to_screen(chart_pos);
412 screen_pos.x
413 }
414
415 pub fn chart_to_screen_y(&self, chart_y: f32) -> f32 {
417 let chart_pos = Vec2::new(0.0, chart_y);
418 let screen_pos = self.chart_to_screen(chart_pos);
419 screen_pos.y
420 }
421
422 pub fn chart_to_screen_distance_x(&self, chart_distance: f32) -> f32 {
424 let origin = self.chart_to_screen(Vec2::ZERO);
425 let target = self.chart_to_screen(Vec2::new(chart_distance, 0.0));
426 (target.x - origin.x).abs()
427 }
428
429 pub fn chart_to_screen_distance_y(&self, chart_distance: f32) -> f32 {
431 let origin = self.chart_to_screen(Vec2::ZERO);
432 let target = self.chart_to_screen(Vec2::new(0.0, chart_distance));
433 (target.y - origin.y).abs()
434 }
435
436 fn update_transforms(&mut self) {
438 let content_rect = self.layout.main_panel;
440
441 let time_scale =
443 content_rect.width / (self.chart_bounds.time_duration().num_seconds() as f32);
444 let price_scale = -content_rect.height / (self.chart_bounds.price_range() as f32); let time_translate =
448 content_rect.x - (self.chart_bounds.time_start.timestamp() as f32 * time_scale);
449 let price_translate =
450 content_rect.bottom() - (self.chart_bounds.price_min as f32 * price_scale);
451
452 self.transform = Mat3::from_translation(Vec2::new(time_translate, price_translate))
454 * Mat3::from_scale(Vec2::new(time_scale, price_scale));
455
456 self.inverse_transform = self.transform.inverse();
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use crate::style::LayoutStyle;
465 use chrono::TimeZone;
466
467 #[test]
468 fn test_rect_operations() {
469 let rect = Rect::new(10.0, 20.0, 100.0, 50.0);
470
471 assert_eq!(rect.right(), 110.0);
472 assert_eq!(rect.bottom(), 70.0);
473 assert_eq!(rect.center(), Vec2::new(60.0, 45.0));
474
475 assert!(rect.contains_point(Vec2::new(50.0, 40.0)));
476 assert!(!rect.contains_point(Vec2::new(5.0, 40.0)));
477 }
478
479 #[test]
480 fn test_chart_bounds() {
481 let start = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
482 let end = Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap();
483
484 let bounds = ChartBounds::new(start, end, 100.0, 200.0).unwrap();
485
486 assert_eq!(bounds.time_duration().num_hours(), 24);
487 assert_eq!(bounds.price_range(), 100.0);
488
489 let mid_time = Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap();
490 assert!(bounds.contains_time(mid_time));
491 assert!(bounds.contains_price(150.0));
492 }
493
494 #[test]
495 fn test_viewport_transforms() {
496 let screen_rect = Rect::new(0.0, 0.0, 800.0, 600.0);
497 let start = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
498 let end = Utc.with_ymd_and_hms(2024, 1, 1, 1, 0, 0).unwrap(); let chart_bounds = ChartBounds::new(start, end, 100.0, 200.0).unwrap();
500
501 let viewport = Viewport::new(screen_rect, chart_bounds, LayoutStyle::default());
502
503 let chart_pos = Vec2::new(start.timestamp() as f32, 150.0);
505 let screen_pos = viewport.chart_to_screen(chart_pos);
506 let back_to_chart = viewport.screen_to_chart(screen_pos);
507
508 assert!((back_to_chart.x - chart_pos.x).abs() < 200.0);
510 assert!((back_to_chart.y - chart_pos.y).abs() < 0.01);
511 }
512}