use super::MAX_GRID_LINES;
use crate::util::abbr_large_numbers;
use exchange::unit::{MinTicksize, Price, PriceStep};
#[derive(Debug, Clone, Copy)]
pub enum YAxisScale {
Price { row_step: PriceStep },
Linear,
Percent,
}
#[derive(Debug, Clone)]
enum Grid {
Row(RowGrid),
Float(FloatGrid),
}
impl Grid {
fn ticks(&self) -> Vec<(TickValue, f64)> {
match self {
Grid::Row(g) => {
let mut out: Vec<_> = g
.values()
.map(|units| (TickValue::PriceUnits(units), g.ratio_of(units)))
.collect();
out.reverse();
out
}
Grid::Float(g) => g
.ticks
.iter()
.map(|(v, r)| (TickValue::Float(*v), *r))
.collect(),
}
}
}
#[derive(Debug, Clone)]
struct RowGrid {
step: i64,
low: i64,
high: i64,
range: i64,
}
impl RowGrid {
fn new(lowest: f64, highest: f64, row_step: PriceStep, labels_can_fit: i32) -> Option<Self> {
let row = row_step.units.max(1);
let low = Price::from_f64(lowest).units;
let high = Price::from_f64(highest).units;
if high <= low {
return None;
}
let range_rows = (high as f64 - low as f64) / row as f64;
let budget = labels_can_fit.max(1) as f64;
let span_rows = tick_span_min(0.0, range_rows, range_rows / budget, 1.0);
let step = (span_rows.ceil() as i64).max(1).saturating_mul(row);
Self::from_units(low, high, step)
}
fn from_row(lowest: f64, highest: f64, row_step: PriceStep) -> Option<Self> {
let row = row_step.units.max(1);
let low = Price::from_f64(lowest).units;
let high = Price::from_f64(highest).units;
if high <= low {
return None;
}
Self::from_units(low, high, row)
}
fn from_units(low: i64, high: i64, step: i64) -> Option<Self> {
let range = high.saturating_sub(low);
if step > range {
return None;
}
Some(Self {
step,
low,
high,
range,
})
}
fn top(&self) -> i64 {
self.high.saturating_sub(self.high.rem_euclid(self.step))
}
fn values(&self) -> impl Iterator<Item = i64> + '_ {
std::iter::successors(Some(self.top()), move |&v| {
v.checked_sub(self.step).filter(|&n| n > self.low)
})
.take(MAX_GRID_LINES)
}
fn ratio_of(&self, units: i64) -> f64 {
units.saturating_sub(self.low) as f64 / self.range as f64
}
}
#[derive(Debug, Clone)]
pub struct FloatGrid {
step: f64,
ticks: Vec<(f64, f64)>,
}
impl FloatGrid {
fn new(lowest: f64, highest: f64, labels_can_fit: i32) -> Self {
let span = highest - lowest;
if !span.is_finite() || span <= 0.0 {
return FloatGrid {
step: 0.0,
ticks: Vec::new(),
};
}
let budget = labels_can_fit.max(1) as f64;
let step = tick_span_min(0.0, span, span / budget, 0.0);
if !step.is_finite() || step <= 0.0 || step > span {
return FloatGrid {
step,
ticks: Vec::new(),
};
}
let mut value = (highest / step).floor() * step;
let mut ticks = Vec::with_capacity((budget + 2.0) as usize);
let mut safety_counter = 0;
while value > lowest && safety_counter < MAX_GRID_LINES {
let ratio = (value - lowest) / span;
ticks.push((value, ratio));
value -= step;
safety_counter += 1;
}
ticks.reverse();
FloatGrid { step, ticks }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TickValue {
PriceUnits(i64),
Float(f64),
}
#[derive(Debug, Clone, Copy)]
pub struct YTick {
pub value: TickValue,
pub ratio: f64,
}
#[derive(Debug, Clone, Default)]
pub struct YTicks {
pub step: Option<f64>,
pub ticks: Vec<YTick>,
}
impl YTicks {
fn from_grid(grid: Grid) -> YTicks {
let ticks = grid
.ticks()
.into_iter()
.map(|(value, ratio)| YTick { value, ratio })
.collect();
let step = match grid {
Grid::Float(g) => Some(g.step),
Grid::Row(_) => None,
};
YTicks { step, ticks }
}
}
impl YAxisScale {
pub fn ticks(&self, lowest: f64, highest: f64, labels_can_fit: i32) -> YTicks {
match self {
YAxisScale::Price { row_step } => {
let row = RowGrid::new(lowest, highest, *row_step, labels_can_fit).map(Grid::Row);
row.map(YTicks::from_grid).unwrap_or_default()
}
YAxisScale::Linear | YAxisScale::Percent => {
let grid = Grid::Float(FloatGrid::new(lowest, highest, labels_can_fit));
YTicks::from_grid(grid)
}
}
}
fn ticks_from_row(&self, lowest: f64, highest: f64) -> YTicks {
let YAxisScale::Price { row_step } = self else {
return YTicks::default();
};
let row = RowGrid::from_row(lowest, highest, *row_step).map(Grid::Row);
row.map(YTicks::from_grid).unwrap_or_default()
}
}
#[derive(Debug, Clone, Copy)]
pub struct PriceAxis {
pub row_step: PriceStep,
pub precision: MinTicksize,
}
impl PriceAxis {
pub fn new(row_step: PriceStep, precision: Option<MinTicksize>) -> Self {
Self {
row_step,
precision: precision
.unwrap_or_else(|| MinTicksize::new(-(row_step.decimal_places() as i8))),
}
}
pub fn ticks(&self, lowest: f64, highest: f64, labels_can_fit: i32) -> YTicks {
self.scale().ticks(lowest, highest, labels_can_fit)
}
pub fn ticks_from_row(&self, lowest: f64, highest: f64) -> YTicks {
self.scale().ticks_from_row(lowest, highest)
}
pub fn format_units(&self, units: i64) -> String {
Price::from_units(units).to_string(self.precision)
}
pub fn format(&self, price: Price) -> String {
price.to_string(self.precision)
}
fn scale(&self) -> YAxisScale {
YAxisScale::Price {
row_step: self.row_step,
}
}
pub fn y_to_price(&self, y: f32, cell_height: f32, min: Price) -> Price {
let ticks = f64::from(y) / f64::from(cell_height);
let price = min.to_f64() - ticks * self.row_step.to_f64_lossy();
Price::from_f64(price)
}
fn floor_to_row(&self, value: f64) -> i64 {
let row = self.row_step.units.max(1);
let units = Price::from_f64(value).units;
units.div_euclid(row) * row
}
}
fn tick_span(low: f64, high: f64, max_span: f64, min_span: f64, dividers: [f64; 3]) -> f64 {
const EPS: f64 = 1e-14;
let mut span = 10f64.powf((high - low).log10().ceil().clamp(0.0, 308.0));
let mut i = 0;
loop {
let c = dividers[i % dividers.len()];
let above_min = span >= min_span - EPS && span > min_span + EPS;
let above_budget = span >= max_span * c - EPS;
if !(above_min && above_budget) {
break;
}
span /= c;
i += 1;
}
span.max(min_span)
}
fn tick_span_min(low: f64, high: f64, max_span: f64, min_span: f64) -> f64 {
[
tick_span(low, high, max_span, min_span, [2.0, 2.5, 2.0]),
tick_span(low, high, max_span, min_span, [2.0, 2.0, 2.5]),
tick_span(low, high, max_span, min_span, [2.5, 2.0, 2.0]),
]
.into_iter()
.fold(f64::INFINITY, f64::min)
}
#[derive(Debug, Clone, PartialEq)]
pub struct YTickLabel {
pub y_pos: f32,
pub content: String,
}
impl YTickLabel {
pub fn for_range(
lowest: f64,
highest: f64,
height: f32,
labels_can_fit: i32,
axis: Option<PriceAxis>,
) -> Vec<Self> {
if !lowest.is_finite() || !highest.is_finite() || highest - lowest <= 0.0 {
return Vec::new();
}
if labels_can_fit <= 1 {
return match axis {
Some(axis) => Self::single_row(highest, axis),
None => Self::single_y(highest),
};
}
match axis {
Some(axis) => Self::row_grid(lowest, highest, height, labels_can_fit, axis),
None => Self::float_grid(lowest, highest, height, labels_can_fit),
}
}
fn single(content: String) -> Vec<Self> {
vec![Self {
y_pos: 0.0,
content,
}]
}
fn single_y(value: f64) -> Vec<Self> {
Self::single(abbr_large_numbers(value))
}
fn single_row(value: f64, axis: PriceAxis) -> Vec<Self> {
let aligned = axis.floor_to_row(value);
Self::single(axis.format_units(aligned))
}
fn row_grid(
lowest: f64,
highest: f64,
height: f32,
labels_can_fit: i32,
axis: PriceAxis,
) -> Vec<Self> {
let ticks = axis.ticks(lowest, highest, labels_can_fit);
if ticks.ticks.is_empty() {
return Self::single_row(highest, axis);
}
let mut labels = Vec::with_capacity((labels_can_fit + 2) as usize);
for tick in ticks.ticks {
let TickValue::PriceUnits(units) = tick.value else {
continue;
};
labels.push(Self {
y_pos: height - tick.ratio as f32 * height,
content: axis.format_units(units),
});
}
labels
}
fn float_grid(lowest: f64, highest: f64, height: f32, labels_can_fit: i32) -> Vec<Self> {
let ticks = YAxisScale::Linear.ticks(lowest, highest, labels_can_fit);
if ticks.ticks.is_empty() {
return Self::single_y(highest);
}
let mut labels = Vec::with_capacity((labels_can_fit + 2) as usize);
for tick in ticks.ticks {
let TickValue::Float(value) = tick.value else {
continue;
};
labels.push(Self {
y_pos: height - tick.ratio as f32 * height,
content: abbr_large_numbers(value),
});
}
labels
}
}