use super::PriceScaleMode;
#[derive(Debug, Clone)]
pub struct PriceMark {
pub price: f64,
pub label: String,
pub y_coord: f32,
pub weight: u8,
}
#[derive(Debug, Clone)]
pub struct PriceMarkGeneratorConfig {
pub min_spacing: f32,
pub max_marks: usize,
pub target_density: f32,
pub min_price_step: Option<f64>,
}
impl Default for PriceMarkGeneratorConfig {
fn default() -> Self {
Self {
min_spacing: 30.0,
max_marks: 20,
target_density: 3.0, min_price_step: None,
}
}
}
pub struct PriceMarkGenerator {
config: PriceMarkGeneratorConfig,
}
impl PriceMarkGenerator {
pub fn new() -> Self {
Self {
config: PriceMarkGeneratorConfig::default(),
}
}
pub fn with_config(config: PriceMarkGeneratorConfig) -> Self {
Self { config }
}
pub fn generate_marks(
&self,
min_price: f64,
max_price: f64,
height_pixels: f32,
scale_mode: PriceScaleMode,
rect_min_y: f32,
rect_max_y: f32,
) -> Vec<PriceMark> {
if min_price >= max_price || height_pixels <= 0.0 {
return Vec::new();
}
match scale_mode {
PriceScaleMode::Normal => self.generate_linear_marks(
min_price,
max_price,
height_pixels,
rect_min_y,
rect_max_y,
),
PriceScaleMode::Logarithmic => {
self.generate_log_marks(min_price, max_price, height_pixels, rect_min_y, rect_max_y)
}
PriceScaleMode::Percentage | PriceScaleMode::IndexedTo100 => {
self.generate_linear_marks(
min_price,
max_price,
height_pixels,
rect_min_y,
rect_max_y,
)
}
}
}
fn generate_linear_marks(
&self,
min_price: f64,
max_price: f64,
height_pixels: f32,
rect_min_y: f32,
rect_max_y: f32,
) -> Vec<PriceMark> {
if !min_price.is_finite()
|| !max_price.is_finite()
|| !height_pixels.is_finite()
|| height_pixels <= 0.0
{
return Vec::new();
}
let price_range = max_price - min_price;
if price_range <= f64::EPSILON * 1000.0 {
return Vec::new();
}
let target_marks = (height_pixels / 100.0 * self.config.target_density)
.max(3.0)
.min(self.config.max_marks as f32);
let raw_step = price_range / target_marks as f64;
let nice_step = self.calculate_nice_step(raw_step);
if nice_step <= 0.0 || !nice_step.is_finite() {
return Vec::new();
}
let start_price = (min_price / nice_step).floor() * nice_step;
if !start_price.is_finite() {
return Vec::new();
}
let mut marks = Vec::new();
let mut curr_price = start_price;
let max_iterations = self.config.max_marks * 10; let mut iterations = 0;
while curr_price <= max_price && iterations < max_iterations {
if curr_price >= min_price {
let ratio = (curr_price - min_price) / price_range;
let y = rect_max_y - (ratio as f32 * (rect_max_y - rect_min_y));
let precision = self.calculate_precision(curr_price, nice_step);
let label = self.format_price(curr_price, precision);
let weight = self.calculate_weight(curr_price, nice_step);
marks.push(PriceMark {
price: curr_price,
label,
y_coord: y,
weight,
});
}
curr_price += nice_step;
iterations += 1;
}
self.apply_spacing_constraints(marks, height_pixels)
}
fn generate_log_marks(
&self,
min_price: f64,
max_price: f64,
height_pixels: f32,
rect_min_y: f32,
rect_max_y: f32,
) -> Vec<PriceMark> {
if min_price <= 0.0
|| max_price <= 0.0
|| !min_price.is_finite()
|| !max_price.is_finite()
|| !height_pixels.is_finite()
|| height_pixels <= 0.0
|| min_price >= max_price
{
return Vec::new();
}
let log_min = min_price.ln();
let log_max = max_price.ln();
let log_range = log_max - log_min;
if !log_min.is_finite() || !log_max.is_finite() || log_range <= f64::EPSILON {
return Vec::new();
}
let min_order = min_price.log10().floor() as i32;
let max_order = max_price.log10().ceil() as i32;
let mut marks = Vec::new();
for order in min_order..=max_order {
let base = 10f64.powi(order);
for multiplier in [1.0, 2.0, 5.0] {
let price = base * multiplier;
if price >= min_price && price <= max_price {
let log_price = price.ln();
let ratio = (log_price - log_min) / log_range;
let y = rect_max_y - (ratio as f32 * (rect_max_y - rect_min_y));
let precision = self.calculate_precision(price, base);
let label = self.format_price(price, precision);
let weight = if multiplier == 1.0 {
100
} else if multiplier == 5.0 {
80
} else {
60
};
marks.push(PriceMark {
price,
label,
y_coord: y,
weight,
});
}
}
}
self.apply_spacing_constraints(marks, height_pixels)
}
fn calculate_nice_step(&self, raw_step: f64) -> f64 {
if raw_step <= 0.0 {
return 1.0;
}
if let Some(min_step) = self.config.min_price_step
&& raw_step < min_step
{
return min_step;
}
let exponent = raw_step.log10().floor();
let fraction = raw_step / 10f64.powf(exponent);
let nice_fraction = if fraction <= 1.0 {
1.0
} else if fraction <= 2.0 {
2.0
} else if fraction <= 5.0 {
5.0
} else {
10.0
};
nice_fraction * 10f64.powf(exponent)
}
fn calculate_precision(&self, price: f64, step: f64) -> usize {
if price == 0.0 {
return 2;
}
if step <= 0.0001 {
8
} else if step <= 0.001 {
6
} else if step <= 0.01 {
4
} else if step <= 0.1 {
3
} else if step <= 1.0 {
2
} else if step <= 10.0 {
1
} else {
0
}
}
fn format_price(&self, price: f64, precision: usize) -> String {
format!("{price:.precision$}")
}
fn calculate_weight(&self, price: f64, step: f64) -> u8 {
let step_10 = step * 10.0;
let step_5 = step * 5.0;
let step_2 = step * 2.0;
let tolerance = step * 0.01;
if (price % step_10).abs() < tolerance {
100
} else if (price % step_5).abs() < tolerance {
80
} else if (price % step_2).abs() < tolerance {
60
} else {
40
}
}
fn apply_spacing_constraints(
&self,
mut marks: Vec<PriceMark>,
height_pixels: f32,
) -> Vec<PriceMark> {
if marks.is_empty() {
return marks;
}
let pixels_per_mark = height_pixels / marks.len() as f32;
if pixels_per_mark < self.config.min_spacing {
marks.sort_by(|a, b| {
b.weight.cmp(&a.weight).then_with(|| {
a.price
.partial_cmp(&b.price)
.unwrap_or(std::cmp::Ordering::Equal)
})
});
let mut filtered = vec![marks[0].clone()];
for mark in marks.iter().skip(1) {
if filtered
.iter()
.all(|m| (m.y_coord - mark.y_coord).abs() >= self.config.min_spacing)
{
filtered.push(mark.clone());
}
}
filtered.sort_by(|a, b| {
a.price
.partial_cmp(&b.price)
.unwrap_or(std::cmp::Ordering::Equal)
});
marks = filtered;
}
if marks.len() > self.config.max_marks {
marks.sort_by(|a, b| {
b.weight.cmp(&a.weight).then_with(|| {
a.price
.partial_cmp(&b.price)
.unwrap_or(std::cmp::Ordering::Equal)
})
});
marks.truncate(self.config.max_marks);
marks.sort_by(|a, b| {
a.price
.partial_cmp(&b.price)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
marks
}
}
impl Default for PriceMarkGenerator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nice_step_calculation() {
let generator = PriceMarkGenerator::new();
assert_eq!(generator.calculate_nice_step(0.7), 1.0);
assert_eq!(generator.calculate_nice_step(1.8), 2.0);
assert_eq!(generator.calculate_nice_step(4.5), 5.0);
assert_eq!(generator.calculate_nice_step(8.0), 10.0);
assert_eq!(generator.calculate_nice_step(15.0), 20.0);
assert_eq!(generator.calculate_nice_step(35.0), 50.0);
}
#[test]
fn test_precision_calculation() {
let generator = PriceMarkGenerator::new();
assert_eq!(generator.calculate_precision(100.0, 0.00001), 8);
assert_eq!(generator.calculate_precision(100.0, 0.001), 6);
assert_eq!(generator.calculate_precision(100.0, 0.01), 4);
assert_eq!(generator.calculate_precision(100.0, 0.1), 3);
assert_eq!(generator.calculate_precision(100.0, 1.0), 2);
assert_eq!(generator.calculate_precision(100.0, 10.0), 1);
assert_eq!(generator.calculate_precision(100.0, 100.0), 0);
}
#[test]
fn test_weight_calculation() {
let generator = PriceMarkGenerator::new();
let step = 1.0;
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); }
#[test]
fn test_linear_marks_generation() {
let generator = PriceMarkGenerator::new();
let marks = generator.generate_linear_marks(100.0, 200.0, 500.0, 0.0, 500.0);
assert!(!marks.is_empty());
assert!(marks.len() <= 20);
for i in 1..marks.len() {
assert!(marks[i].price > marks[i - 1].price);
}
for mark in &marks {
assert!(mark.price >= 100.0 && mark.price <= 200.0);
}
}
#[test]
fn test_log_marks_generation() {
let generator = PriceMarkGenerator::new();
let marks = generator.generate_log_marks(10.0, 1000.0, 500.0, 0.0, 500.0);
assert!(!marks.is_empty());
let has_10 = marks.iter().any(|m| (m.price - 10.0).abs() < 0.01);
let has_100 = marks.iter().any(|m| (m.price - 100.0).abs() < 0.01);
let has_1000 = marks.iter().any(|m| (m.price - 1000.0).abs() < 0.01);
assert!(has_10 || has_100 || has_1000);
}
}