1use super::{PriceMap, PriceScaleMode};
9
10#[derive(Debug, Clone)]
12pub struct PriceMark {
13 pub price: f64,
16
17 pub label: String,
19
20 pub y_coord: f32,
22
23 pub weight: u8,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
30pub struct PriceMarkGeneratorConfig {
31 pub min_spacing: f32,
33
34 pub max_marks: usize,
36
37 pub target_density: f32,
41
42 pub min_price_step: Option<f64>,
45}
46
47impl PriceMarkGeneratorConfig {
48 pub(crate) const MAX_MARKS: usize = 1_000;
49}
50
51impl Default for PriceMarkGeneratorConfig {
52 fn default() -> Self {
53 Self {
54 min_spacing: 20.0,
55 max_marks: 100,
56 target_density: 2.5,
57 min_price_step: None,
58 }
59 }
60}
61
62pub struct PriceMarkGenerator {
65 config: PriceMarkGeneratorConfig,
66}
67
68impl PriceMarkGenerator {
69 pub fn new() -> Self {
71 Self {
72 config: PriceMarkGeneratorConfig::default(),
73 }
74 }
75
76 pub fn with_config(mut config: PriceMarkGeneratorConfig) -> Self {
78 config.max_marks = config.max_marks.min(PriceMarkGeneratorConfig::MAX_MARKS);
79 Self { config }
80 }
81
82 pub fn generate_marks(&self, map: &PriceMap) -> Vec<PriceMark> {
88 let range = map.range();
89 let (top, bottom) = map.extent();
90 let height_pixels = bottom - top;
91 if range.min >= range.max || height_pixels <= 0.0 {
92 return Vec::new();
93 }
94
95 let transform = map.transform();
96 if transform.mode != PriceScaleMode::Logarithmic {
97 return self.generate_linear_marks(range.min, range.max, height_pixels, map, false);
98 }
99 let (min_price, max_price) = (transform.to_price(range.min), transform.to_price(range.max));
100 let marks = self.generate_log_marks(min_price, max_price, height_pixels, map);
101 if marks.len() >= 3 {
102 return marks;
103 }
104 self.generate_linear_marks(min_price, max_price, height_pixels, map, true)
106 }
107
108 fn generate_linear_marks(
111 &self,
112 min_price: f64,
113 max_price: f64,
114 height_pixels: f32,
115 map: &PriceMap,
116 prices: bool,
117 ) -> Vec<PriceMark> {
118 if !min_price.is_finite()
120 || !max_price.is_finite()
121 || !height_pixels.is_finite()
122 || height_pixels <= 0.0
123 {
124 return Vec::new();
125 }
126
127 let price_range = max_price - min_price;
128
129 if price_range <= 0.0 {
131 return Vec::new();
132 }
133
134 let target_marks = (height_pixels / 100.0 * self.config.target_density)
136 .max(3.0)
137 .min(self.config.max_marks as f32);
138
139 let raw_step = price_range / target_marks as f64;
141
142 let mut nice_step = self.calculate_nice_step(raw_step);
144 if let Some(decimals) = map.price_precision() {
145 let unit = 10f64.powi(-(decimals as i32));
146 nice_step = Self::round_up_to_multiple(nice_step, unit);
147 }
148 let min_step = self
149 .min_step()
150 .filter(|_| prices || map.transform().mode == PriceScaleMode::Normal);
151 if let Some(min_step) = min_step {
152 nice_step = Self::round_up_to_multiple(nice_step, min_step);
153 }
154
155 if nice_step <= 0.0 || !nice_step.is_finite() {
157 return Vec::new();
158 }
159
160 let start_index = (min_price / nice_step).floor();
162 let start_price = start_index * nice_step;
163
164 if !start_price.is_finite() {
166 return Vec::new();
167 }
168
169 let mut marks = Vec::new();
171 let mut curr_price = start_price;
172 let max_iterations = self.config.max_marks * 10; let mut iterations = 0;
174
175 while curr_price <= max_price && iterations < max_iterations {
176 if curr_price >= min_price {
177 let y = if prices {
178 map.price_to_y(curr_price)
179 } else {
180 map.scale_to_y(curr_price)
181 };
182
183 let precision = map.price_precision().unwrap_or_else(|| {
185 let mut decimals = self.calculate_precision(nice_step);
186 if min_step.is_some() {
187 while decimals < 8 {
188 let scaled = nice_step * 10f64.powi(decimals as i32);
189 if (scaled - scaled.round()).abs() <= scaled * f64::EPSILON * 4.0 {
190 break;
191 }
192 decimals += 1;
193 }
194 }
195 if matches!(
196 map.transform().mode,
197 PriceScaleMode::Normal | PriceScaleMode::Logarithmic
198 ) {
199 decimals.max(2)
200 } else {
201 decimals
202 }
203 });
204 let label = if map.price_precision().is_none() && precision > 8 {
205 let mut label = self.format_price(curr_price, precision, true);
206 if map.transform().mode == PriceScaleMode::Percentage {
207 if !label.starts_with('-') {
208 label.insert(0, '+');
209 }
210 label.push('%');
211 }
212 label
213 } else if prices {
214 self.format_price(curr_price, precision, false)
215 } else {
216 map.transform().label(curr_price, precision, None)
217 };
218 let label = super::price_formatter::group_thousands(label, map.thousands_sep());
219
220 let weight = self.calculate_weight(curr_price, nice_step);
222
223 marks.push(PriceMark {
224 price: curr_price,
225 label,
226 y_coord: y,
227 weight,
228 });
229 }
230
231 iterations += 1;
232 curr_price = if min_step.is_some() {
233 (start_index + iterations as f64) * nice_step
234 } else {
235 curr_price + nice_step
236 };
237 }
238
239 self.apply_spacing_constraints(marks, self.config.min_spacing)
240 }
241
242 fn generate_log_marks(
244 &self,
245 min_price: f64,
246 max_price: f64,
247 height_pixels: f32,
248 map: &PriceMap,
249 ) -> Vec<PriceMark> {
250 if min_price <= 0.0
252 || max_price <= 0.0
253 || !min_price.is_finite()
254 || !max_price.is_finite()
255 || !height_pixels.is_finite()
256 || height_pixels <= 0.0
257 || min_price >= max_price
258 {
259 return Vec::new();
260 }
261
262 let log_min = min_price.ln();
263 let log_max = max_price.ln();
264 let log_range = log_max - log_min;
265
266 if !log_min.is_finite() || !log_max.is_finite() || log_range <= f64::EPSILON {
268 return Vec::new();
269 }
270
271 let mut min_order = min_price.log10().floor() as i32;
274 if let Some(decimals) = map.price_precision() {
275 min_order = min_order.max(-(decimals as i32));
276 }
277 let max_order = max_price.log10().ceil() as i32;
278
279 let mut marks = Vec::new();
280
281 for order in min_order..=max_order {
283 let base = 10f64.powi(order);
284
285 for multiplier in [1.0, 2.0, 5.0] {
287 let price = base * multiplier;
288
289 if let Some(step) = self.min_step()
290 && (price - (price / step).round() * step).abs()
291 > price.abs().max(step) * f64::EPSILON * 4.0
292 {
293 continue;
294 }
295
296 if price >= min_price && price <= max_price {
297 let y = map.price_to_y(price);
298
299 let precision = map
300 .price_precision()
301 .unwrap_or_else(|| self.calculate_precision(base).max(2));
302 let label = super::price_formatter::group_thousands(
303 self.format_price(price, precision, map.price_precision().is_none()),
304 map.thousands_sep(),
305 );
306
307 let weight = if multiplier == 1.0 {
308 100
309 } else if multiplier == 5.0 {
310 80
311 } else {
312 60
313 };
314
315 marks.push(PriceMark {
316 price,
317 label,
318 y_coord: y,
319 weight,
320 });
321 }
322 }
323 }
324
325 self.apply_spacing_constraints(
327 marks,
328 self.config
329 .min_spacing
330 .max(100.0 / self.config.target_density),
331 )
332 }
333
334 fn round_up_to_multiple(step: f64, unit: f64) -> f64 {
335 let ratio = step / unit;
336 let rounded = ratio.round();
337 let multiple = if (ratio - rounded).abs() <= ratio * 1e-9 {
338 rounded
339 } else {
340 ratio.ceil()
341 };
342 multiple.max(1.0) * unit
343 }
344
345 fn calculate_nice_step(&self, raw_step: f64) -> f64 {
348 if raw_step <= 0.0 {
349 return 1.0;
350 }
351
352 if let Some(min_step) = self.min_step()
353 && raw_step < min_step
354 {
355 return min_step;
356 }
357
358 let exponent = raw_step.log10().floor();
360 let fraction = raw_step / 10f64.powf(exponent);
361
362 let distance = |nice: f64| (fraction - nice).abs();
363 let nice_fraction = [1.0, 2.0, 2.5, 4.0, 5.0, 10.0]
364 .into_iter()
365 .min_by(|a, b| distance(*a).total_cmp(&distance(*b)))
366 .unwrap_or(1.0);
367
368 nice_fraction * 10f64.powf(exponent)
369 }
370
371 fn calculate_precision(&self, step: f64) -> usize {
374 if step <= 0.0001 {
376 (1.0 - step.log10().floor()).max(8.0) as usize
377 } else if step <= 0.001 {
378 6
379 } else if step <= 0.01 {
380 4
381 } else if step <= 0.1 {
382 3
383 } else if step <= 1.0 {
384 2
385 } else if step <= 10.0 {
386 1
387 } else {
388 0
389 }
390 }
391
392 fn min_step(&self) -> Option<f64> {
394 self.config
395 .min_price_step
396 .filter(|s| s.is_finite() && *s > 0.0)
397 }
398
399 fn format_price(&self, price: f64, precision: usize, automatic: bool) -> String {
401 if automatic && precision > 8 {
402 let price = if price == 0.0 { 0.0 } else { price };
403 let digits = (price.abs().log10().floor() + precision as f64).clamp(0.0, 17.0) as usize;
404 format!("{price:.digits$e}")
405 } else {
406 format!("{price:.precision$}")
407 }
408 }
409
410 fn calculate_weight(&self, price: f64, step: f64) -> u8 {
412 let step_10 = step * 10.0;
414 let step_5 = step * 5.0;
415 let step_2 = step * 2.0;
416
417 let tolerance = step * 0.01;
419
420 if (price % step_10).abs() < tolerance {
421 100
422 } else if (price % step_5).abs() < tolerance {
423 80
424 } else if (price % step_2).abs() < tolerance {
425 60
426 } else {
427 40
428 }
429 }
430
431 fn apply_spacing_constraints(&self, mut marks: Vec<PriceMark>, spacing: f32) -> Vec<PriceMark> {
433 if marks.is_empty() {
434 return marks;
435 }
436
437 marks.sort_by(|a, b| {
439 b.weight
440 .cmp(&a.weight)
441 .then_with(|| a.price.total_cmp(&b.price))
442 });
443
444 let mut filtered = vec![marks[0].clone()];
446
447 for mark in marks.iter().skip(1) {
448 if filtered
449 .iter()
450 .all(|m| (m.y_coord - mark.y_coord).abs() >= spacing)
451 {
452 filtered.push(mark.clone());
453 }
454 }
455
456 filtered.sort_by(|a, b| a.price.total_cmp(&b.price));
458 marks = filtered;
459
460 if marks.len() > self.config.max_marks {
462 marks.sort_by(|a, b| {
464 b.weight
465 .cmp(&a.weight)
466 .then_with(|| a.price.total_cmp(&b.price))
467 });
468 marks.truncate(self.config.max_marks);
469 marks.sort_by(|a, b| a.price.total_cmp(&b.price));
470 }
471
472 marks
473 }
474}
475
476impl Default for PriceMarkGenerator {
477 fn default() -> Self {
478 Self::new()
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485
486 #[test]
487 fn test_nice_step_calculation() {
488 let generator = PriceMarkGenerator::new();
489
490 assert_eq!(generator.calculate_nice_step(0.7), 0.5);
492 assert_eq!(generator.calculate_nice_step(1.8), 2.0);
493 assert_eq!(generator.calculate_nice_step(2.2), 2.0);
494 assert_eq!(generator.calculate_nice_step(3.6), 4.0);
495 assert_eq!(generator.calculate_nice_step(4.6), 5.0);
496 assert_eq!(generator.calculate_nice_step(8.0), 10.0);
497 assert_eq!(generator.calculate_nice_step(16.0), 20.0);
498 assert_eq!(generator.calculate_nice_step(35.0), 40.0);
499 }
500
501 #[test]
502 fn test_precision_calculation() {
503 let generator = PriceMarkGenerator::new();
504
505 assert_eq!(generator.calculate_precision(0.00001), 8);
506 assert_eq!(generator.calculate_precision(0.001), 6);
507 assert_eq!(generator.calculate_precision(0.01), 4);
508 assert_eq!(generator.calculate_precision(0.1), 3);
509 assert_eq!(generator.calculate_precision(1.0), 2);
510 assert_eq!(generator.calculate_precision(10.0), 1);
511 assert_eq!(generator.calculate_precision(100.0), 0);
512 }
513
514 #[test]
515 fn ticks_stay_on_the_minimum_price_grid() {
516 use PriceScaleMode::{Logarithmic, Normal};
517 for (mode, min, max, step, precision, expected) in [
518 (Normal, 4500.0, 4503.75, 0.25, None, None),
519 (Normal, -3.75, 0.5, 0.25, Some(2), None),
520 (Normal, 4500.0, 4503.75, 0.05, None, None),
521 (Normal, 0.01, 0.9, 0.03, Some(2), None),
522 (Logarithmic, 4500.0, 4503.75, 0.25, Some(2), None),
523 (Logarithmic, 0.01, 1.0, 0.03, None, None),
524 (Normal, 100.0, 101.0, 1.0 / 32.0, None, Some("100.125")),
525 (Normal, 0.012, 0.013, 1e-6, None, Some("0.01210000")),
526 ] {
527 let (min, max) = if mode == Logarithmic {
528 (f64::ln(min), f64::ln(max))
529 } else {
530 (min, max)
531 };
532 let marks = map(mode, min, max)
533 .with_price_precision(precision)
534 .with_tick_marks(PriceMarkGeneratorConfig {
535 min_price_step: Some(step),
536 ..Default::default()
537 })
538 .marks();
539 assert!(marks.len() >= 3, "{mode:?}: {marks:?}");
540 if let Some(expected) = expected {
541 assert!(
542 marks.iter().any(|mark| mark.label == expected),
543 "{mode:?}: expected {expected}, got {marks:?}"
544 );
545 }
546 for mark in marks {
547 let ticks = mark.price / step;
548 assert!(
549 (ticks - ticks.round()).abs() < 1e-9,
550 "{mode:?}: {} is off the {step} grid",
551 mark.label
552 );
553 }
554 }
555 }
556
557 #[test]
558 fn percentage_and_indexed_ticks_keep_their_own_units() {
559 use PriceScaleMode::{IndexedTo100, Percentage};
560 for (mode, min, max, expected) in [
561 (Percentage, -2.0, 2.375, "-1.60%"),
562 (IndexedTo100, 98.0, 102.375, "98.40"),
563 ] {
564 let marks = map(mode, min, max)
565 .with_tick_marks(PriceMarkGeneratorConfig {
566 min_price_step: Some(0.25),
567 ..Default::default()
568 })
569 .marks();
570 assert!(
571 marks.iter().any(|mark| mark.label == expected),
572 "{mode:?}: expected {expected}, got {marks:?}"
573 );
574 }
575 }
576
577 #[test]
578 fn test_weight_calculation() {
579 let generator = PriceMarkGenerator::new();
580 let step = 1.0;
581
582 assert_eq!(generator.calculate_weight(10.0, step), 100); assert_eq!(generator.calculate_weight(5.0, step), 80); assert_eq!(generator.calculate_weight(2.0, step), 60); assert_eq!(generator.calculate_weight(1.0, step), 40); }
587
588 fn map(mode: PriceScaleMode, min: f64, max: f64) -> PriceMap {
589 let transform = crate::scales::PriceTransform { mode, base: 0.0 };
590 PriceMap::new(
591 transform,
592 crate::scales::PriceRange::new(min, max),
593 0.0,
594 500.0,
595 false,
596 )
597 }
598
599 #[test]
600 fn test_linear_marks_generation() {
601 let generator = PriceMarkGenerator::new();
602 let marks = generator.generate_marks(&map(PriceScaleMode::Normal, 100.0, 200.0));
603 for max_marks in [1_000, 1_001, usize::MAX] {
604 let bounded = PriceMarkGenerator::with_config(PriceMarkGeneratorConfig {
605 max_marks,
606 min_spacing: 0.0,
607 target_density: 100.0,
608 ..Default::default()
609 });
610 assert_eq!(
611 bounded
612 .generate_marks(
613 &map(PriceScaleMode::Normal, 0.0, 2_000.0).with_bounds(0.0, 2_000.0)
614 )
615 .len(),
616 1_000
617 );
618 }
619
620 assert!(!marks.is_empty());
621 assert!(marks.len() <= 20); for i in 1..marks.len() {
625 assert!(marks[i].price > marks[i - 1].price);
626 }
627
628 for mark in &marks {
630 assert!(mark.price >= 100.0 && mark.price <= 200.0);
631 }
632
633 for (mode, min, max, precision, expected) in [
634 (PriceScaleMode::Normal, 18_200.0, 22_800.0, None, "20000.00"),
635 (PriceScaleMode::Normal, 700.0, 730.0, Some(3), "710.000"),
636 (PriceScaleMode::Normal, 0.012, 0.013, Some(6), "0.012100"),
637 (
638 PriceScaleMode::Normal,
639 1e-18,
640 9e-18,
641 Some(18),
642 "0.000000000000000005",
643 ),
644 (
645 PriceScaleMode::Logarithmic,
646 700f64.ln(),
647 730f64.ln(),
648 Some(3),
649 "710.000",
650 ),
651 ] {
652 let map = map(mode, min, max).with_price_precision(precision);
653 let marks = generator.generate_marks(&map);
654 assert!(
655 marks.iter().any(|mark| mark.label == expected),
656 "{mode:?}: {marks:?}"
657 );
658 }
659 for (mode, min, max, decimals) in [
660 (PriceScaleMode::Normal, 700.0, 700.04, 2),
661 (PriceScaleMode::Normal, 0.0, 30.0, 0),
662 (PriceScaleMode::Logarithmic, 0.001_f64.ln(), 0.1_f64.ln(), 2),
663 ] {
664 let marks =
665 generator.generate_marks(&map(mode, min, max).with_price_precision(Some(decimals)));
666 assert!(!marks.is_empty());
667 assert!(marks.windows(2).all(|pair| pair[0].label != pair[1].label));
668 assert!(
669 marks
670 .iter()
671 .all(|mark| (mark.label.parse::<f64>().unwrap() - mark.price).abs() < 1e-8)
672 );
673 }
674 let marks = generator.generate_marks(&map(PriceScaleMode::Normal, 0.0, 70.0));
677 assert_eq!(marks.len(), 15);
678 for (mode, min, max) in [
679 (PriceScaleMode::Normal, 1e-10, 2e-10),
680 (PriceScaleMode::Normal, -2e-18, 2e-18),
681 (PriceScaleMode::Normal, 1e-100, 2e-100),
682 (PriceScaleMode::Logarithmic, 1e-12_f64.ln(), 1e-9_f64.ln()),
683 (
684 PriceScaleMode::Logarithmic,
685 1e-10_f64.ln(),
686 1.1e-10_f64.ln(),
687 ),
688 ] {
689 let marks = generator.generate_marks(&map(mode, min, max));
690 assert!(marks.len() >= 3, "{mode:?}, {min}..{max}: {marks:?}");
691 assert!(marks.windows(2).all(|pair| pair[0].label != pair[1].label));
692 for mark in marks {
693 let labelled = mark.label.parse::<f64>().unwrap();
694 let tolerance = if mode == PriceScaleMode::Logarithmic {
695 mark.price.abs() * 1e-9
696 } else {
697 (max - min) * 1e-9
698 };
699 assert!(
700 (labelled - mark.price).abs() <= tolerance,
701 "{mode:?}: {} labelled {}",
702 mark.price,
703 mark.label
704 );
705 }
706 }
707 }
708
709 #[test]
712 fn target_density_sets_the_number_of_marks() {
713 use crate::scales::{PriceScale, PriceScaleOptions, PriceTransform};
714 for (density, count) in [(1.0, 6), (2.5, 11), (5.0, 26)] {
715 let mut options = PriceScaleOptions::default();
716 options.tick_marks.target_density = density;
717 let mut scale = PriceScale::new(options);
718 scale.set_range(0.0, 100.0);
719 let marks = scale.map(PriceTransform::default(), 0.0, 500.0).marks();
720 assert_eq!(marks.len(), count, "{density}: {marks:?}");
721 }
722 }
723
724 #[test]
725 fn test_log_marks_generation() {
726 let generator = PriceMarkGenerator::new();
727 let (lo, hi) = (10f64.ln(), 1000f64.ln());
728 let marks = generator.generate_marks(&map(PriceScaleMode::Logarithmic, lo, hi));
729
730 let prices: Vec<_> = marks.iter().map(|mark| mark.price).collect();
732 assert_eq!(prices, [20.0, 50.0, 100.0, 200.0, 500.0]);
733
734 let (lo, hi) = (18_200f64.ln(), 22_800f64.ln());
737 let marks = generator.generate_marks(&map(PriceScaleMode::Logarithmic, lo, hi));
738 let labels: Vec<&str> = marks.iter().map(|m| m.label.as_str()).collect();
739 let expected = [
740 "18400.00", "18800.00", "19200.00", "19600.00", "20000.00", "20400.00", "20800.00",
741 "21200.00", "21600.00", "22000.00", "22400.00",
742 ];
743 assert_eq!(labels, expected);
744 }
745}