use crate::core::Rect;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IndexAxis {
pub origin_x: i32,
pub slot_width: i32,
pub count: usize,
}
impl IndexAxis {
pub fn new(origin_x: i32, width: i32, count: usize) -> Self {
let slot_width = if count == 0 { 0 } else { (width.max(0) / count as i32).max(1) };
Self { origin_x, slot_width, count }
}
pub fn x_for(&self, index: usize) -> i32 {
self.origin_x + self.slot_width * index as i32
}
pub fn center_for(&self, index: usize) -> i32 {
self.x_for(index) + self.slot_width / 2
}
pub fn body_width(&self) -> i32 {
if self.count == 0 {
0
} else if self.slot_width <= 2 {
1
} else {
self.slot_width - 2
}
}
pub fn index_at(&self, x: i32) -> Option<usize> {
if self.slot_width <= 0 || self.count == 0 {
return None;
}
let offset = x - self.origin_x;
if offset < 0 || offset >= self.slot_width * self.count as i32 {
return None;
}
Some((offset / self.slot_width) as usize)
}
pub fn total_width(&self) -> i32 {
self.slot_width * self.count as i32
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PriceAxis {
pub top: i32,
pub bottom: i32,
pub low: f64,
pub high: f64,
}
impl PriceAxis {
pub fn new(top: i32, bottom: i32, low: f64, high: f64) -> Self {
let mut adjusted_low = low;
let mut adjusted_high = high;
if !adjusted_low.is_finite() || !adjusted_high.is_finite() {
adjusted_low = 0.0;
adjusted_high = 1.0;
} else if adjusted_high <= adjusted_low {
let magnitude = adjusted_low.abs().max(1.0) * 0.001;
adjusted_low -= magnitude;
adjusted_high += magnitude;
}
Self { top, bottom, low: adjusted_low, high: adjusted_high }
}
pub fn y_for(&self, price: f64) -> i32 {
if !price.is_finite() {
return self.bottom;
}
let span = self.high - self.low;
if span <= 0.0 {
return self.bottom;
}
let fraction = (price - self.low) / span;
let y = self.bottom as f64 - fraction * (self.bottom - self.top) as f64;
(y.round() as i32).clamp(self.top.min(self.bottom), self.top.max(self.bottom))
}
pub fn height(&self) -> i32 {
(self.bottom - self.top).abs()
}
pub fn price_at(&self, y: i32) -> f64 {
let height = (self.bottom - self.top) as f64;
if height == 0.0 {
return self.low;
}
let fraction = (self.bottom - y) as f64 / height;
self.low + fraction * (self.high - self.low)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn index_at_honours_both_edges() {
let axis = IndexAxis::new(10, 100, 10);
assert_eq!(axis.index_at(9), None, "one pixel left of the plot is outside");
assert_eq!(axis.index_at(10), Some(0), "the plot's own left edge is bar 0");
assert_eq!(axis.index_at(109), Some(9), "the last bar's last pixel");
assert_eq!(axis.index_at(110), None, "the first pixel past the plot is outside");
}
#[test]
fn index_at_rejects_points_outside_a_negative_origin_plot() {
let axis = IndexAxis::new(-30, 100, 10);
assert_eq!(axis.index_at(-30), Some(0), "the plot starts at -30");
assert_eq!(axis.index_at(-31), None, "left of the plot's own left edge");
assert_eq!(axis.index_at(69), Some(9), "the last bar inside the plot");
assert_eq!(axis.index_at(70), None, "the first pixel past the plot");
}
#[test]
fn an_empty_axis_has_no_body_width() {
let axis = IndexAxis::new(0, 100, 0);
assert_eq!(axis.slot_width, 0);
assert_eq!(axis.body_width(), 0);
assert_eq!(axis.total_width(), 0);
}
#[test]
fn a_one_pixel_slot_still_has_a_visible_body() {
let axis = IndexAxis::new(0, 10, 10);
assert_eq!(axis.slot_width, 1);
assert_eq!(axis.body_width(), 1);
}
#[test]
fn a_degenerate_price_range_is_widened() {
let axis = PriceAxis::new(0, 100, 50.0, 50.0);
assert!(axis.high > axis.low, "the range must be usable");
let middle = axis.y_for(50.0);
assert!((0..=100).contains(&middle));
}
#[test]
fn a_higher_price_maps_to_a_smaller_y() {
let axis = PriceAxis::new(0, 100, 90.0, 110.0);
assert!(axis.y_for(110.0) < axis.y_for(90.0));
assert_eq!(axis.y_for(110.0), 0);
assert_eq!(axis.y_for(90.0), 100);
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PlotArea {
pub rect: Rect,
pub left_margin: i32,
pub right_margin: i32,
pub top_margin: i32,
pub bottom_margin: i32,
}
impl PlotArea {
pub fn of(rect: Rect) -> Self {
Self::with_margins(rect, 48, 8, 8, 20)
}
pub fn with_margins(rect: Rect, left: i32, right: i32, top: i32, bottom: i32) -> Self {
let max_horizontal = (rect.width as i32).saturating_sub(4).max(0) / 2;
let left_margin = left.min(max_horizontal).max(0);
let right_margin = right.min(max_horizontal).max(0);
let max_vertical = (rect.height as i32).saturating_sub(2).max(0) / 2;
let top_margin = top.min(max_vertical).max(0);
let bottom_margin = bottom.min(max_vertical).max(0);
let width = (rect.width as i32 - left_margin - right_margin).max(0) as u32;
let height = (rect.height as i32 - top_margin - bottom_margin).max(0) as u32;
Self {
rect: Rect::new(rect.x + left_margin, rect.y + top_margin, width, height),
left_margin,
right_margin,
top_margin,
bottom_margin,
}
}
pub fn index_axis(&self, count: usize) -> IndexAxis {
IndexAxis::new(self.rect.x, self.rect.width as i32, count)
}
pub fn price_axis(&self, low: f64, high: f64) -> PriceAxis {
PriceAxis::new(self.rect.y, self.rect.y + self.rect.height as i32, low, high)
}
pub fn right(&self) -> i32 {
self.rect.x + self.rect.width as i32
}
pub fn bottom(&self) -> i32 {
self.rect.y + self.rect.height as i32
}
pub fn label_x(&self) -> i32 {
self.rect.x - 4
}
}