use super::{BarData, TimeScale};
#[derive(Debug, Clone, Copy)]
pub struct ZoomState {
pub bar_spacing: f32,
pub right_offset: f32,
pub price_range: Option<(f64, f64)>,
}
#[derive(Debug, Clone)]
pub struct ChartState {
data: BarData,
time_scale: TimeScale,
price_auto_scale: bool,
price_range: Option<(f64, f64)>,
zoom_history: Vec<ZoomState>,
}
impl ChartState {
pub fn new(data: BarData) -> Self {
let mut time_scale = TimeScale::new();
time_scale.set_bar_cnt(data.len());
Self {
data,
time_scale,
price_auto_scale: true,
price_range: None,
zoom_history: Vec::new(),
}
}
pub fn data(&self) -> &BarData {
&self.data
}
pub fn time_scale_mut(&mut self) -> &mut TimeScale {
&mut self.time_scale
}
pub fn time_scale(&self) -> &TimeScale {
&self.time_scale
}
pub fn set_data(&mut self, data: BarData) {
self.time_scale.set_bar_cnt(data.len());
self.data = data;
}
pub fn visible_data(&self) -> &[crate::model::Bar] {
if self.data.bars.is_empty() {
return &[];
}
let logical_range = self.time_scale.visible_logical_range();
log::debug!(
"[visible_data] logical_range: left={}, right={}",
logical_range.left,
logical_range.right
);
let (mut start, mut end) = logical_range.to_strict_range();
log::debug!("[visible_data] BEFORE clamping: start={start}, end={end}");
let data_len = self.data.len();
end = end.min(data_len);
start = start.min(end);
log::debug!("[visible_data] AFTER clamping: start={start}, end={end}, data_len={data_len}");
let expected_visible = self.time_scale.visible_candles();
let actual_visible = end.saturating_sub(start);
log::debug!(
"[visible_data] actual_visible={actual_visible}, expected_visible={expected_visible}"
);
if actual_visible < expected_visible && start == 0 && data_len > 1 {
let old_end = end;
end = expected_visible.min(data_len);
if end > old_end {
log::debug!(
"[visible_data] EXTENDING RANGE: old_end={}, new_end={} (gained {} bars)",
old_end,
end,
end - old_end
);
}
}
log::debug!(
"[visible_data] FINAL RESULT: returning bars[{}..{}] ({} bars)",
start,
end,
end - start
);
&self.data.bars[start..end]
}
pub fn visible_range(&self) -> (usize, usize) {
if self.data.bars.is_empty() {
return (0, 0);
}
let logical_range = self.time_scale.visible_logical_range();
let (mut start, mut end) = logical_range.to_strict_range();
log::debug!("[visible_range] BEFORE clamping: start={start}, end={end}");
let data_len = self.data.len();
end = end.min(data_len);
start = start.min(end);
let expected_visible = self.time_scale.visible_candles();
let actual_visible = end.saturating_sub(start);
if actual_visible < expected_visible && start == 0 && data_len > 1 {
let old_end = end;
end = expected_visible.min(data_len);
if end > old_end {
log::debug!(
"[visible_range] EXTENDING RANGE: old_end={}, new_end={} (gained {} bars)",
old_end,
end,
end - old_end
);
}
}
log::debug!(
"[visible_range] FINAL RESULT: ({}, {}) - {} bars",
start,
end,
end - start
);
(start, end)
}
pub fn set_price_auto_scale(&mut self, enabled: bool) {
self.price_auto_scale = enabled;
if enabled {
self.price_range = None;
}
}
pub fn set_price_range(&mut self, min: f64, max: f64) {
self.price_auto_scale = false;
self.price_range = Some((min, max));
}
pub fn price_range(&self) -> (f64, f64) {
if let Some((min, max)) = self.price_range {
return (min, max);
}
let visible = self.visible_data();
if visible.is_empty() {
return (0.0, 100.0);
}
let vmin = visible
.iter()
.map(|c| c.low)
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap();
let vmax = visible
.iter()
.map(|c| c.high)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap();
let range = (vmax - vmin).max(1e-12);
let margin = range * 0.1;
(vmin - margin, vmax + margin)
}
pub fn is_price_auto_scale(&self) -> bool {
self.price_auto_scale
}
pub fn push_zoom_state(&mut self) {
let curr_state = ZoomState {
bar_spacing: self.time_scale.bar_spacing(),
right_offset: self.time_scale.right_offset(),
price_range: self.price_range,
};
self.zoom_history.push(curr_state);
}
pub fn pop_zoom_state(&mut self) -> bool {
if let Some(state) = self.zoom_history.pop() {
self.time_scale.set_bar_spacing(state.bar_spacing);
self.time_scale.set_right_offset(state.right_offset);
self.price_range = state.price_range;
self.price_auto_scale = state.price_range.is_none();
true
} else {
false
}
}
pub fn clear_zoom_history(&mut self) {
self.zoom_history.clear();
}
pub fn has_zoom_history(&self) -> bool {
!self.zoom_history.is_empty()
}
}