egui_charts/model/
chartstate.rs1use super::{BarData, TimeScale};
10
11#[derive(Debug, Clone, Copy)]
16pub struct ZoomState {
17 pub bar_spacing: f32,
19 pub right_offset: f32,
21 pub price_range: Option<(f64, f64)>,
23}
24
25#[derive(Debug, Clone)]
42pub struct ChartState {
43 data: BarData,
45 time_scale: TimeScale,
47 price_auto_scale: bool,
49 price_range: Option<(f64, f64)>,
51 zoom_history: Vec<ZoomState>,
53}
54
55impl ChartState {
56 pub fn new(data: BarData) -> Self {
58 let mut time_scale = TimeScale::new();
59 time_scale.set_bar_cnt(data.len());
60
61 Self {
62 data,
63 time_scale,
64 price_auto_scale: true,
65 price_range: None,
66 zoom_history: Vec::new(),
67 }
68 }
69
70 pub fn data(&self) -> &BarData {
72 &self.data
73 }
74
75 pub fn time_scale_mut(&mut self) -> &mut TimeScale {
77 &mut self.time_scale
78 }
79
80 pub fn time_scale(&self) -> &TimeScale {
82 &self.time_scale
83 }
84
85 pub fn set_data(&mut self, data: BarData) {
87 self.time_scale.set_bar_cnt(data.len());
88 self.data = data;
89 }
90
91 pub fn visible_data(&self) -> &[crate::model::Bar] {
93 if self.data.bars.is_empty() {
94 return &[];
95 }
96
97 let logical_range = self.time_scale.visible_logical_range();
98 log::debug!(
99 "[visible_data] logical_range: left={}, right={}",
100 logical_range.left,
101 logical_range.right
102 );
103
104 let (mut start, mut end) = logical_range.to_strict_range();
105 log::debug!("[visible_data] BEFORE clamping: start={start}, end={end}");
106
107 let data_len = self.data.len();
109 end = end.min(data_len);
110 start = start.min(end);
111
112 log::debug!("[visible_data] AFTER clamping: start={start}, end={end}, data_len={data_len}");
113
114 let expected_visible = self.time_scale.visible_candles();
116
117 let actual_visible = end.saturating_sub(start);
119 log::debug!(
120 "[visible_data] actual_visible={actual_visible}, expected_visible={expected_visible}"
121 );
122
123 if actual_visible < expected_visible && start == 0 && data_len > 1 {
124 let old_end = end;
128 end = expected_visible.min(data_len);
129 if end > old_end {
130 log::debug!(
131 "[visible_data] EXTENDING RANGE: old_end={}, new_end={} (gained {} bars)",
132 old_end,
133 end,
134 end - old_end
135 );
136 }
137 }
138
139 log::debug!(
140 "[visible_data] FINAL RESULT: returning bars[{}..{}] ({} bars)",
141 start,
142 end,
143 end - start
144 );
145
146 &self.data.bars[start..end]
147 }
148
149 pub fn visible_range(&self) -> (usize, usize) {
151 if self.data.bars.is_empty() {
152 return (0, 0);
153 }
154
155 let logical_range = self.time_scale.visible_logical_range();
156 let (mut start, mut end) = logical_range.to_strict_range();
157
158 log::debug!("[visible_range] BEFORE clamping: start={start}, end={end}");
159
160 let data_len = self.data.len();
162 end = end.min(data_len);
163 start = start.min(end);
164
165 let expected_visible = self.time_scale.visible_candles();
167
168 let actual_visible = end.saturating_sub(start);
170
171 if actual_visible < expected_visible && start == 0 && data_len > 1 {
172 let old_end = end;
176 end = expected_visible.min(data_len);
177 if end > old_end {
178 log::debug!(
179 "[visible_range] EXTENDING RANGE: old_end={}, new_end={} (gained {} bars)",
180 old_end,
181 end,
182 end - old_end
183 );
184 }
185 }
186
187 log::debug!(
188 "[visible_range] FINAL RESULT: ({}, {}) - {} bars",
189 start,
190 end,
191 end - start
192 );
193
194 (start, end)
195 }
196
197 pub fn set_price_auto_scale(&mut self, enabled: bool) {
199 self.price_auto_scale = enabled;
200 if enabled {
201 self.price_range = None;
202 }
203 }
204
205 pub fn set_price_range(&mut self, min: f64, max: f64) {
207 self.price_auto_scale = false;
208 self.price_range = Some((min, max));
209 }
210
211 pub fn price_range(&self) -> (f64, f64) {
213 if let Some((min, max)) = self.price_range {
214 return (min, max);
215 }
216
217 let visible = self.visible_data();
219 if visible.is_empty() {
220 return (0.0, 100.0);
221 }
222
223 let vmin = visible
224 .iter()
225 .map(|c| c.low)
226 .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
227 .unwrap();
228 let vmax = visible
229 .iter()
230 .map(|c| c.high)
231 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
232 .unwrap();
233
234 let range = (vmax - vmin).max(1e-12);
236 let margin = range * 0.1;
237 (vmin - margin, vmax + margin)
238 }
239
240 pub fn is_price_auto_scale(&self) -> bool {
242 self.price_auto_scale
243 }
244
245 pub fn push_zoom_state(&mut self) {
247 let curr_state = ZoomState {
248 bar_spacing: self.time_scale.bar_spacing(),
249 right_offset: self.time_scale.right_offset(),
250 price_range: self.price_range,
251 };
252 self.zoom_history.push(curr_state);
253 }
254
255 pub fn pop_zoom_state(&mut self) -> bool {
257 if let Some(state) = self.zoom_history.pop() {
258 self.time_scale.set_bar_spacing(state.bar_spacing);
259 self.time_scale.set_right_offset(state.right_offset);
260 self.price_range = state.price_range;
261 self.price_auto_scale = state.price_range.is_none();
262 true
263 } else {
264 false
265 }
266 }
267
268 pub fn clear_zoom_history(&mut self) {
270 self.zoom_history.clear();
271 }
272
273 pub fn has_zoom_history(&self) -> bool {
275 !self.zoom_history.is_empty()
276 }
277}