Skip to main content

shape_viz_core/
utils.rs

1//! Shared utilities for chart calculations
2
3use crate::error::{ChartError, Result};
4
5/// Calculate nice price levels for grid and axis
6pub fn calculate_price_levels(price_min: f64, price_max: f64, content_height: f32) -> Vec<f64> {
7    let price_range = price_max - price_min;
8    let target_label_count = (content_height / 20.0) as i32; // 20 pixels between labels for denser grid
9    let raw_step = price_range / target_label_count as f64;
10    let nice_step = find_nice_number(raw_step);
11
12    // Start from a nice round number
13    let start_price = (price_min / nice_step).floor() * nice_step;
14    let mut levels = Vec::new();
15    let mut current_price = start_price;
16
17    while current_price <= price_max {
18        if current_price >= price_min {
19            levels.push(current_price);
20        }
21        current_price += nice_step;
22    }
23
24    levels
25}
26
27/// Find a "nice" round number for price steps
28pub fn find_nice_number(value: f64) -> f64 {
29    let magnitude = 10.0_f64.powf(value.log10().floor());
30    let normalized = value / magnitude;
31
32    let nice_normalized = if normalized < 1.5 {
33        1.0
34    } else if normalized < 2.5 {
35        2.0
36    } else if normalized < 3.0 {
37        2.5
38    } else if normalized < 7.0 {
39        5.0
40    } else {
41        10.0
42    };
43
44    nice_normalized * magnitude
45}
46
47/// Compute the mean squared error (MSE) between generated RGBA image data and a reference PNG file.
48/// A lower MSE indicates the generated image is more similar to the reference.
49///
50/// * `generated_rgba` - Raw RGBA bytes produced by the renderer.
51/// * `width` / `height`   - Dimensions of the generated image.
52/// * `reference_path`     - Path to the reference image on disk (PNG).
53pub fn compare_image_mse(
54    generated_rgba: &[u8],
55    width: u32,
56    height: u32,
57    reference_path: &str,
58) -> Result<f64> {
59    let reference_img = image::open(reference_path)
60        .map_err(|e| {
61            ChartError::internal(format!(
62                "Failed to open reference image {}: {}",
63                reference_path, e
64            ))
65        })?
66        .to_rgba8();
67
68    // Verify dimensions match.
69    if reference_img.width() != width || reference_img.height() != height {
70        return Err(ChartError::internal(format!(
71            "Reference image dimensions {}x{} do not match generated {}x{}",
72            reference_img.width(),
73            reference_img.height(),
74            width,
75            height
76        )));
77    }
78
79    let ref_pixels = reference_img.as_raw();
80    if ref_pixels.len() != generated_rgba.len() {
81        return Err(ChartError::internal(
82            "Pixel data length mismatch between images",
83        ));
84    }
85
86    let mut mse: f64 = 0.0;
87    for (a, b) in generated_rgba.iter().zip(ref_pixels.iter()) {
88        let diff = (*a as f64) - (*b as f64);
89        mse += diff * diff;
90    }
91
92    mse /= generated_rgba.len() as f64;
93    Ok(mse)
94}