1use super::series::KlineSeries;
2use crate::chart::Basis;
3pub use crate::chart::axis::{
4 PanelYValueRange, PanelYViewportLimits, ZOOM_SENSITIVITY, autoscale_span_limits,
5};
6pub use crate::chart::value::{PanelValueScale, Y_UNIT_STEP_FALLBACK, YUnit, YUnitRange};
7use exchange::{Timeframe, UnixMs};
8
9use std::ops::Range;
10
11pub const DEFAULT_HORIZONTAL_SPACING_PX: f32 = 5.0;
12pub const MIN_HORIZONTAL_SPACING_PX: f32 = 2.0;
13pub const MAX_HORIZONTAL_SPACING_PX: f32 = 360.0;
14
15const BAR_CENTER_OFFSET_UNITS: f32 = 0.5;
16const RIGHT_EDGE_INSET_PX: f32 = 1.0;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
19pub struct ChartStepMs(i64);
20
21impl ChartStepMs {
22 pub fn from_u64(step: u64) -> Self {
23 let clamped = i64::try_from(step).unwrap_or(i64::MAX);
24 Self(clamped.max(1))
25 }
26
27 pub fn get(self) -> i64 {
28 self.0
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
33pub struct ChartCoord(i64);
34
35impl ChartCoord {
36 fn from_u64_clamped(value: u64) -> Self {
37 Self(i64::try_from(value).unwrap_or(i64::MAX))
38 }
39
40 pub fn from_unix_ms(value: UnixMs) -> Self {
41 Self::from_u64_clamped(value.as_u64())
42 }
43
44 pub fn get(self) -> i64 {
45 self.0
46 }
47
48 pub fn saturating_add_i64(self, rhs: i64) -> Self {
49 Self(self.0.saturating_add(rhs))
50 }
51
52 pub fn saturating_sub_i64(self, rhs: i64) -> Self {
53 Self(self.0.saturating_sub(rhs))
54 }
55
56 pub fn to_unix_ms_non_negative(self) -> UnixMs {
57 UnixMs::new(u64::try_from(self.0.max(0)).unwrap_or(u64::MAX))
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq)]
62pub struct HorizontalWorld {
63 spacing: f32,
64 offset: f32,
65}
66
67impl HorizontalWorld {
68 pub fn visible_window(self, data_bounds: (i64, i64), plot_width: f32) -> (i64, i64, f32) {
69 let (_, data_max_x) = data_bounds;
70
71 let span = self.spacing();
72 let visible_units = (plot_width.max(1.0) / span).max(1.0);
73 let right = data_max_x as f32 + self.offset();
74 let left = right - visible_units + 1.0;
75 let left_unit = left.floor() as i64;
76 let right_unit = right.ceil() as i64;
77
78 if right_unit <= left_unit {
79 return (left_unit, left_unit.saturating_add(1), right);
80 }
81
82 (left_unit, right_unit, right)
83 }
84
85 pub fn new(spacing: f32, offset: f32) -> Self {
86 let spacing = Self::normalized_spacing(spacing);
87
88 Self { spacing, offset }
89 }
90
91 fn normalized_spacing(spacing: f32) -> f32 {
92 if spacing.is_finite() {
93 spacing.clamp(MIN_HORIZONTAL_SPACING_PX, MAX_HORIZONTAL_SPACING_PX)
94 } else {
95 DEFAULT_HORIZONTAL_SPACING_PX
96 }
97 }
98
99 pub fn spacing(self) -> f32 {
100 self.spacing
101 }
102
103 pub fn offset(self) -> f32 {
104 self.offset
105 }
106
107 pub fn with_offset(self, offset: f32) -> Self {
108 Self { offset, ..self }
109 }
110
111 pub fn axis_at_right_unit(self, right_unit: f32) -> WorldAxis {
112 WorldAxis::new(
113 self.spacing,
114 -(right_unit + BAR_CENTER_OFFSET_UNITS) * self.spacing - RIGHT_EDGE_INSET_PX,
115 )
116 }
117
118 pub fn zoom_around_screen(
119 self,
120 current_right_unit: f32,
121 cursor_screen: f32,
122 origin: f32,
123 delta: f32,
124 ) -> Self {
125 let axis = self.axis_at_right_unit(current_right_unit);
126 let spacing = (self.spacing * (delta / ZOOM_SENSITIVITY).exp())
127 .clamp(MIN_HORIZONTAL_SPACING_PX, MAX_HORIZONTAL_SPACING_PX);
128 let cursor_world = axis.screen_to_world(cursor_screen, origin);
129 let translation = cursor_screen - origin - cursor_world * spacing;
130 let next_right_unit =
131 (-translation - RIGHT_EDGE_INSET_PX) / spacing - BAR_CENTER_OFFSET_UNITS;
132
133 Self {
134 spacing,
135 offset: self.offset + next_right_unit - current_right_unit,
136 }
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq)]
141pub struct WorldAxis {
142 spacing: f32,
143 translation: f32,
144}
145
146impl WorldAxis {
147 fn new(spacing: f32, translation: f32) -> Self {
148 Self {
149 spacing: spacing.max(f32::EPSILON),
150 translation,
151 }
152 }
153
154 pub fn spacing(self) -> f32 {
155 self.spacing
156 }
157
158 pub fn world_to_screen(self, world: f32, origin: f32) -> f32 {
159 origin + world * self.spacing + self.translation
160 }
161
162 pub fn screen_to_world(self, screen: f32, origin: f32) -> f32 {
163 (screen - origin - self.translation) / self.spacing
164 }
165
166 pub fn screen_to_logical_index(self, screen: f32, origin: f32) -> f32 {
167 self.screen_to_world(screen, origin) - BAR_CENTER_OFFSET_UNITS
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
172pub struct TickIndex(u64);
173
174impl TickIndex {
175 pub const ZERO: Self = Self(0);
176
177 #[inline]
178 pub fn as_u64(self) -> u64 {
179 self.0
180 }
181
182 pub fn from_u64(value: u64) -> Self {
183 Self(value)
184 }
185}
186
187#[derive(Debug, Clone, Copy)]
188pub enum XAxis {
189 Time {
190 timeframe: Timeframe,
191 anchor: UnixMs,
192 },
193 Tick {
194 anchor: TickIndex,
195 },
196}
197
198impl XAxis {
199 pub fn for_series(basis: Basis, series: &[KlineSeries]) -> Option<Self> {
200 match basis {
201 Basis::Time(timeframe) => {
202 let anchor = series
203 .iter()
204 .filter_map(KlineSeries::latest_point)
205 .map(|point| point.kline.time)
206 .max()?;
207 Some(Self::Time { timeframe, anchor })
208 }
209 Basis::Tick(_) => series
210 .iter()
211 .any(|series| series.point_count() > 0)
212 .then_some(Self::Tick {
213 anchor: TickIndex::ZERO,
214 }),
215 }
216 }
217
218 pub fn data_bounds(self, series: &[KlineSeries]) -> Option<(i64, i64)> {
219 match self {
220 Self::Time { .. } => series
221 .iter()
222 .filter_map(|series| {
223 let first = series.first_point()?.kline.time;
224 let last = series.latest_point()?.kline.time;
225 Some((self.unit_from_time(first), self.unit_from_time(last)))
226 })
227 .reduce(|(min, max), (first, last)| (min.min(first), max.max(last))),
228 Self::Tick { .. } => {
229 let point_count = series.iter().map(KlineSeries::point_count).max()?;
230 let last = self.unit_from_tick(TickIndex::ZERO);
231 let first =
232 self.unit_from_tick(TickIndex::from_u64(point_count.saturating_sub(1) as u64));
233 Some((first, last))
234 }
235 }
236 }
237
238 pub fn visible_window(
239 self,
240 data_bounds: (i64, i64),
241 output_offset: i64,
242 world: HorizontalWorld,
243 plot_width: f32,
244 ) -> (i64, i64, f32) {
245 let (min_extension, max_extension) = self.output_domain_expansion(output_offset);
246 world.visible_window(
247 (
248 data_bounds.0.saturating_add(min_extension),
249 data_bounds.1.saturating_add(max_extension),
250 ),
251 plot_width,
252 )
253 }
254
255 pub fn output_domain_expansion(self, offset: i64) -> (i64, i64) {
256 match self {
257 Self::Time { .. } => (0, offset),
258 Self::Tick { .. } => (offset.saturating_neg(), 0),
259 }
260 }
261
262 #[inline]
263 pub fn unit_from_time(self, value: UnixMs) -> i64 {
264 match self {
265 Self::Time { timeframe, anchor } => {
266 let aligned = ChartCoord::from_unix_ms(value.floor_to(timeframe)).get();
267 let anchor = ChartCoord::from_unix_ms(anchor).get();
268 let step = ChartStepMs::from_u64(timeframe.to_milliseconds().max(1)).get();
269
270 aligned.saturating_sub(anchor) / step
271 }
272 Self::Tick { .. } => 0,
273 }
274 }
275
276 pub fn unit_from_time_f32(self, value: UnixMs) -> Option<f32> {
277 match self {
278 Self::Time { timeframe, anchor } => {
279 let delta_ms = value.as_u64() as f64 - anchor.as_u64() as f64;
280 Some((delta_ms / timeframe.to_milliseconds().max(1) as f64) as f32)
281 }
282 Self::Tick { .. } => None,
283 }
284 }
285
286 #[inline]
287 pub fn unit_from_tick(self, value: TickIndex) -> i64 {
288 match self {
289 Self::Tick { anchor } => {
290 let anchor = ChartCoord::from_u64_clamped(anchor.as_u64()).get();
291 let index = ChartCoord::from_u64_clamped(value.as_u64()).get();
292
293 anchor.saturating_sub(index)
294 }
295 Self::Time { .. } => 0,
296 }
297 }
298
299 #[inline]
300 pub fn time_from_unit(self, unit: i64) -> Option<UnixMs> {
301 match self {
302 Self::Time { timeframe, anchor } => {
303 let step = i64::try_from(timeframe.to_milliseconds()).unwrap_or(i64::MAX);
304 Some(anchor.saturating_add_signed(unit.saturating_mul(step)))
305 }
306 Self::Tick { .. } => None,
307 }
308 }
309
310 pub fn time_from_unit_f32(self, unit: f32) -> Option<UnixMs> {
311 if !unit.is_finite() {
312 return None;
313 }
314
315 match self {
316 Self::Time { timeframe, anchor } => {
317 let offset = (f64::from(unit) * timeframe.to_milliseconds() as f64).round();
318 let offset = offset.clamp(i64::MIN as f64, i64::MAX as f64) as i64;
319 Some(anchor.saturating_add_signed(offset))
320 }
321 Self::Tick { .. } => None,
322 }
323 }
324
325 #[inline]
326 pub fn tick_from_unit(self, unit: i64) -> Option<TickIndex> {
327 match self {
328 Self::Tick { anchor } => {
329 let anchor = anchor.as_u64();
330 if unit >= 0 {
331 anchor.checked_sub(unit as u64).map(TickIndex)
332 } else {
333 anchor.checked_add(unit.unsigned_abs()).map(TickIndex)
334 }
335 }
336 Self::Time { .. } => None,
337 }
338 }
339
340 #[inline]
341 pub fn step_ms(self, step_units: i64) -> u64 {
342 match self {
343 Self::Time { timeframe, .. } => {
344 let step = step_units.max(1) as u64;
345 timeframe.to_milliseconds().max(1).saturating_mul(step)
346 }
347 Self::Tick { .. } => 1,
348 }
349 }
350
351 pub fn bucket_start(self, unit: i64) -> Option<UnixMs> {
352 let Self::Time { timeframe, .. } = self else {
353 return None;
354 };
355 Some(self.time_from_unit(unit)?.floor_to(timeframe))
356 }
357
358 pub fn bucket_end(self, unit: i64) -> Option<UnixMs> {
359 let Self::Time { timeframe, .. } = self else {
360 return None;
361 };
362 Some(
363 self.bucket_start(unit)?
364 .saturating_add(timeframe.to_milliseconds().max(1).saturating_sub(1)),
365 )
366 }
367
368 pub fn output_unit(self, source_unit: i64, offset_bars: i64) -> i64 {
369 match self {
370 Self::Time { .. } => source_unit.saturating_add(offset_bars),
371 Self::Tick { .. } => source_unit.saturating_sub(offset_bars),
372 }
373 }
374
375 pub fn source_unit(self, output_unit: i64, offset_bars: i64) -> i64 {
376 match self {
377 Self::Time { .. } => output_unit.saturating_sub(offset_bars),
378 Self::Tick { .. } => output_unit.saturating_add(offset_bars),
379 }
380 }
381
382 pub fn source_window(self, min: i64, max: i64, offsets: &[i64]) -> (i64, i64) {
383 let mut source_min = min;
384 let mut source_max = max;
385 for &offset in offsets {
386 source_min = source_min.min(self.source_unit(min, offset));
387 source_max = source_max.max(self.source_unit(max, offset));
388 }
389 (source_min.saturating_sub(1), source_max.saturating_add(1))
390 }
391}
392
393#[derive(Debug, Clone)]
394pub struct BarUnitWindow {
395 values: Vec<i64>,
396 start_index: usize,
397}
398
399impl BarUnitWindow {
400 pub(crate) fn from_units(values: Vec<i64>, start_index: usize) -> Self {
401 Self {
402 values,
403 start_index,
404 }
405 }
406
407 pub fn values(&self) -> &[i64] {
408 &self.values
409 }
410
411 pub fn start_index(&self) -> usize {
412 self.start_index
413 }
414
415 pub fn global_index(&self, local_index: usize) -> Option<usize> {
416 self.start_index.checked_add(local_index)
417 }
418
419 pub fn global_range(&self, local_range: Range<usize>) -> Range<usize> {
420 self.start_index.saturating_add(local_range.start)
421 ..self.start_index.saturating_add(local_range.end)
422 }
423
424 pub fn unit_at_global_index(&self, index: usize) -> Option<i64> {
425 index
426 .checked_sub(self.start_index)
427 .and_then(|local_index| self.values.get(local_index).copied())
428 }
429
430 pub fn index_range(&self, min: i64, max: i64) -> Range<usize> {
431 self.values.partition_point(|&unit| unit < min)
432 ..self.values.partition_point(|&unit| unit <= max)
433 }
434
435 pub fn output_range(
436 &self,
437 axis: XAxis,
438 min: i64,
439 max: i64,
440 offset: i64,
441 end: usize,
442 ) -> Range<usize> {
443 let values = &self.values[..end];
444 values.partition_point(|&unit| axis.output_unit(unit, offset) < min)
445 ..values.partition_point(|&unit| axis.output_unit(unit, offset) <= max)
446 }
447}
448
449#[derive(Debug, Clone, Copy, PartialEq, Eq)]
450pub enum PrimaryViewportState {
451 Auto,
452 Manual { range: Option<YUnitRange> },
453}
454
455impl PrimaryViewportState {
456 pub fn is_auto(self) -> bool {
457 matches!(self, Self::Auto)
458 }
459
460 pub fn range(self) -> Option<YUnitRange> {
461 match self {
462 Self::Auto => None,
463 Self::Manual { range } => range,
464 }
465 }
466
467 pub fn disable(&mut self) -> bool {
468 if !self.is_auto() {
469 return false;
470 }
471
472 *self = Self::Manual { range: None };
473 true
474 }
475
476 pub fn set_range(&mut self, range: YUnitRange) -> bool {
477 let next = Self::Manual { range: Some(range) };
478 if *self == next {
479 return false;
480 }
481
482 *self = next;
483 true
484 }
485
486 pub fn reset(&mut self) -> bool {
487 if matches!(self, Self::Auto) {
488 return false;
489 }
490
491 *self = Self::Auto;
492 true
493 }
494
495 pub fn toggle(&mut self) -> bool {
496 if self.is_auto() {
497 *self = Self::Manual { range: None };
498 } else {
499 *self = Self::Auto;
500 }
501 true
502 }
503}