use std::f64::consts::E;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PriceScaleMode {
#[default]
Normal,
Logarithmic,
Percentage,
IndexedTo100,
}
impl fmt::Display for PriceScaleMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PriceScaleMode::Normal => write!(f, "Normal"),
PriceScaleMode::Logarithmic => write!(f, "Logarithmic"),
PriceScaleMode::Percentage => write!(f, "Percentage"),
PriceScaleMode::IndexedTo100 => write!(f, "Indexed to 100"),
}
}
}
impl FromStr for PriceScaleMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"normal" => Ok(PriceScaleMode::Normal),
"logarithmic" | "log" => Ok(PriceScaleMode::Logarithmic),
"percentage" | "percent" | "%" => Ok(PriceScaleMode::Percentage),
"indexedto100" | "indexed to 100" | "indexed" => Ok(PriceScaleMode::IndexedTo100),
_ => Err(format!("Invalid price scale mode: {s}")),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PriceRange {
pub min: f64,
pub max: f64,
}
impl PriceRange {
pub fn new(min: f64, max: f64) -> Self {
Self { min, max }
}
pub fn length(&self) -> f64 {
(self.max - self.min).max(1e-12)
}
pub fn contains(&self, price: f64) -> bool {
price >= self.min && price <= self.max
}
}
#[derive(Debug, Clone, Copy)]
pub struct PriceScaleMargins {
pub top: f32,
pub bottom: f32,
}
impl Default for PriceScaleMargins {
fn default() -> Self {
Self {
top: 0.2,
bottom: 0.1,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PriceScaleOptions {
pub auto_scale: bool,
pub mode: PriceScaleMode,
pub invert_scale: bool,
pub scale_margins: PriceScaleMargins,
pub align_labels: bool,
pub border_visible: bool,
pub visible: bool,
pub ticks_visible: bool,
pub entire_text_only: bool,
}
impl Default for PriceScaleOptions {
fn default() -> Self {
Self {
auto_scale: true,
mode: PriceScaleMode::Normal,
invert_scale: false,
scale_margins: PriceScaleMargins::default(),
align_labels: true,
border_visible: true,
visible: true,
ticks_visible: false,
entire_text_only: false,
}
}
}
pub struct PriceScale {
options: PriceScaleOptions,
price_range: PriceRange,
height: f32,
first_val: Option<f64>,
manual_range: Option<PriceRange>,
}
impl PriceScale {
pub fn new(height: f32) -> Self {
Self {
options: PriceScaleOptions::default(),
price_range: PriceRange::new(0.0, 100.0),
height,
first_val: None,
manual_range: None,
}
}
pub fn with_options(height: f32, options: PriceScaleOptions) -> Self {
Self {
options,
price_range: PriceRange::new(0.0, 100.0),
height,
first_val: None,
manual_range: None,
}
}
pub fn set_height(&mut self, height: f32) {
self.height = height;
}
pub fn set_options(&mut self, options: PriceScaleOptions) {
self.options = options;
}
pub fn set_first_val(&mut self, value: f64) {
self.first_val = Some(value);
}
pub fn auto_scale(&mut self, data_min: f64, data_max: f64) {
if !self.options.auto_scale {
if let Some(manual) = self.manual_range {
self.price_range = manual;
return;
}
}
let range = (data_max - data_min).max(1e-12);
let top_margin = range * self.options.scale_margins.top as f64;
let bottom_margin = range * self.options.scale_margins.bottom as f64;
self.price_range = PriceRange::new(data_min - bottom_margin, data_max + top_margin);
}
pub fn set_manual_range(&mut self, min: f64, max: f64) {
self.manual_range = Some(PriceRange::new(min, max));
self.options.auto_scale = false;
}
pub fn reset_auto_scale(&mut self) {
self.manual_range = None;
self.options.auto_scale = true;
}
pub fn price_to_coord(&self, price: f64) -> f32 {
let normalized = self.normalize_price(price);
let ratio = self.price_to_ratio(normalized);
let y = if self.options.invert_scale {
ratio as f32 * self.height
} else {
(1.0 - ratio as f32) * self.height
};
y.clamp(0.0, self.height)
}
pub fn coord_to_price(&self, y: f32) -> f64 {
if self.height.abs() < f32::EPSILON {
return self.denormalize_price(self.ratio_to_price(0.0));
}
let ratio = if self.options.invert_scale {
(y / self.height) as f64
} else {
(1.0 - y / self.height) as f64
};
let normalized = self.ratio_to_price(ratio);
self.denormalize_price(normalized)
}
pub fn price_range(&self) -> PriceRange {
self.price_range
}
pub fn visible_price_range(&self) -> PriceRange {
match self.options.mode {
PriceScaleMode::Normal => self.price_range,
PriceScaleMode::Logarithmic => {
PriceRange::new(
self.price_to_log(self.price_range.min),
self.price_to_log(self.price_range.max),
)
}
PriceScaleMode::Percentage => {
if let Some(first) = self.first_val {
PriceRange::new(
self.price_to_percent(self.price_range.min, first),
self.price_to_percent(self.price_range.max, first),
)
} else {
self.price_range
}
}
PriceScaleMode::IndexedTo100 => {
if let Some(first) = self.first_val {
PriceRange::new(
self.price_to_idxed(self.price_range.min, first),
self.price_to_idxed(self.price_range.max, first),
)
} else {
self.price_range
}
}
}
}
pub(crate) fn normalize_price(&self, price: f64) -> f64 {
match self.options.mode {
PriceScaleMode::Normal => price,
PriceScaleMode::Logarithmic => self.price_to_log(price),
PriceScaleMode::Percentage => {
if let Some(first) = self.first_val {
self.price_to_percent(price, first)
} else {
price
}
}
PriceScaleMode::IndexedTo100 => {
if let Some(first) = self.first_val {
self.price_to_idxed(price, first)
} else {
price
}
}
}
}
fn denormalize_price(&self, normalized: f64) -> f64 {
match self.options.mode {
PriceScaleMode::Normal => normalized,
PriceScaleMode::Logarithmic => self.log_to_price(normalized),
PriceScaleMode::Percentage => {
if let Some(first) = self.first_val {
self.percent_to_price(normalized, first)
} else {
normalized
}
}
PriceScaleMode::IndexedTo100 => {
if let Some(first) = self.first_val {
self.indexed_to_price(normalized, first)
} else {
normalized
}
}
}
}
fn price_to_ratio(&self, normalized_price: f64) -> f64 {
let normalized_range = match self.options.mode {
PriceScaleMode::Normal => self.price_range.length(),
PriceScaleMode::Logarithmic => {
self.price_to_log(self.price_range.max) - self.price_to_log(self.price_range.min)
}
PriceScaleMode::Percentage => {
if let Some(first) = self.first_val {
self.price_to_percent(self.price_range.max, first)
- self.price_to_percent(self.price_range.min, first)
} else {
self.price_range.length()
}
}
PriceScaleMode::IndexedTo100 => {
if let Some(first) = self.first_val {
self.price_to_idxed(self.price_range.max, first)
- self.price_to_idxed(self.price_range.min, first)
} else {
self.price_range.length()
}
}
};
let normalized_min = match self.options.mode {
PriceScaleMode::Normal => self.price_range.min,
PriceScaleMode::Logarithmic => self.price_to_log(self.price_range.min),
PriceScaleMode::Percentage => {
if let Some(first) = self.first_val {
self.price_to_percent(self.price_range.min, first)
} else {
self.price_range.min
}
}
PriceScaleMode::IndexedTo100 => {
if let Some(first) = self.first_val {
self.price_to_idxed(self.price_range.min, first)
} else {
self.price_range.min
}
}
};
((normalized_price - normalized_min) / normalized_range.max(1e-12)).clamp(0.0, 1.0)
}
fn ratio_to_price(&self, ratio: f64) -> f64 {
let ratio_clamped = ratio.clamp(0.0, 1.0);
match self.options.mode {
PriceScaleMode::Normal => {
self.price_range.min + ratio_clamped * self.price_range.length()
}
PriceScaleMode::Logarithmic => {
let log_min = self.price_to_log(self.price_range.min);
let log_max = self.price_to_log(self.price_range.max);
log_min + ratio_clamped * (log_max - log_min)
}
PriceScaleMode::Percentage => {
if let Some(first) = self.first_val {
let percent_min = self.price_to_percent(self.price_range.min, first);
let percent_max = self.price_to_percent(self.price_range.max, first);
percent_min + ratio_clamped * (percent_max - percent_min)
} else {
self.price_range.min + ratio_clamped * self.price_range.length()
}
}
PriceScaleMode::IndexedTo100 => {
if let Some(first) = self.first_val {
let indexed_min = self.price_to_idxed(self.price_range.min, first);
let indexed_max = self.price_to_idxed(self.price_range.max, first);
indexed_min + ratio_clamped * (indexed_max - indexed_min)
} else {
self.price_range.min + ratio_clamped * self.price_range.length()
}
}
}
}
fn price_to_log(&self, price: f64) -> f64 {
if price <= 0.0 {
return 0.0;
}
price.ln() / E.ln()
}
fn log_to_price(&self, log_price: f64) -> f64 {
E.powf(log_price)
}
fn price_to_percent(&self, price: f64, first_val: f64) -> f64 {
if first_val == 0.0 {
return 0.0;
}
((price - first_val) / first_val.abs()) * 100.0
}
fn percent_to_price(&self, percent: f64, first_val: f64) -> f64 {
first_val + (first_val * percent / 100.0)
}
fn price_to_idxed(&self, price: f64, first_val: f64) -> f64 {
if first_val == 0.0 {
return 100.0;
}
(price / first_val) * 100.0
}
fn indexed_to_price(&self, indexed: f64, first_val: f64) -> f64 {
(indexed / 100.0) * first_val
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normal_mode() {
let mut scale = PriceScale::new(100.0);
scale.auto_scale(0.0, 100.0);
let y_min = scale.price_to_coord(0.0);
let y_max = scale.price_to_coord(100.0);
assert!(y_min > y_max, "Lower price should have higher Y coord");
let price_from_y = scale.coord_to_price(y_min);
assert!(
(price_from_y - 0.0).abs() < 1.0,
"Should convert back to ~0"
);
}
#[test]
fn test_logarithmic_mode() {
let mut scale = PriceScale::new(100.0);
scale.set_options(PriceScaleOptions {
mode: PriceScaleMode::Logarithmic,
..Default::default()
});
scale.auto_scale(1.0, 100.0);
let y_10 = scale.price_to_coord(10.0);
let y_100 = scale.price_to_coord(100.0);
assert!(y_10 > y_100);
}
#[test]
fn test_percentage_mode() {
let mut scale = PriceScale::new(100.0);
scale.set_options(PriceScaleOptions {
mode: PriceScaleMode::Percentage,
..Default::default()
});
scale.set_first_val(100.0);
scale.auto_scale(90.0, 110.0);
let percent_90 = scale.normalize_price(90.0);
let percent_100 = scale.normalize_price(100.0);
let percent_110 = scale.normalize_price(110.0);
assert!((percent_90 - (-10.0)).abs() < 0.1, "90 should be -10%");
assert!((percent_100 - 0.0).abs() < 0.1, "100 should be 0%");
assert!((percent_110 - 10.0).abs() < 0.1, "110 should be +10%");
}
#[test]
fn test_idxed_to_100_mode() {
let mut scale = PriceScale::new(100.0);
scale.set_options(PriceScaleOptions {
mode: PriceScaleMode::IndexedTo100,
..Default::default()
});
scale.set_first_val(100.0);
scale.auto_scale(90.0, 110.0);
let indexed_90 = scale.normalize_price(90.0);
let indexed_100 = scale.normalize_price(100.0);
let indexed_110 = scale.normalize_price(110.0);
assert!((indexed_90 - 90.0).abs() < 0.1, "90 should map to 90");
assert!((indexed_100 - 100.0).abs() < 0.1, "100 should map to 100");
assert!((indexed_110 - 110.0).abs() < 0.1, "110 should map to 110");
}
#[test]
fn test_coord_to_price_zero_height_is_finite() {
let mut scale = PriceScale::new(0.0);
scale.auto_scale(100.0, 200.0);
let price = scale.coord_to_price(0.0);
assert!(price.is_finite());
}
#[test]
fn test_invert_scale() {
let mut scale = PriceScale::new(100.0);
scale.set_options(PriceScaleOptions {
invert_scale: true,
..Default::default()
});
scale.auto_scale(0.0, 100.0);
let y_0 = scale.price_to_coord(0.0);
let y_100 = scale.price_to_coord(100.0);
assert!(
y_100 > y_0,
"With inverted scale, higher price should have higher Y"
);
}
}