1use crate::data::{ChartData, RangeSeries};
7use crate::error::Result;
8use crate::layers::{Layer, LayerStage};
9use crate::renderer::RenderContext;
10use crate::style::ChartStyle;
11use crate::theme::ChartTheme;
12use crate::viewport::{Rect, Viewport};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum RangeBarStyle {
17 #[default]
19 Candlestick,
20 BoxPlot,
22 ErrorBar,
24 RangeArea,
26}
27
28#[derive(Debug, Clone)]
30pub struct RangeBarConfig {
31 pub style: RangeBarStyle,
33 pub body_width_factor: f32,
35 pub line_width: f32,
37 pub min_width: f32,
39 pub max_width: f32,
41}
42
43impl Default for RangeBarConfig {
44 fn default() -> Self {
45 Self {
46 style: RangeBarStyle::default(),
47 body_width_factor: 0.7,
48 line_width: 1.5,
49 min_width: 1.5,
50 max_width: 40.0,
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
57struct RangeBarGeometry {
58 pub body_rect: Rect,
59 pub line_top: (f32, f32, f32, f32), pub line_bottom: (f32, f32, f32, f32),
61 pub is_positive: bool,
63 pub is_neutral: bool,
65}
66
67#[derive(Debug)]
69pub struct RangeBarLayer {
70 enabled: bool,
71 needs_render: bool,
72 config: RangeBarConfig,
73 cached_bars: Vec<RangeBarGeometry>,
74 last_viewport_hash: u64,
75 range_data: Option<RangeData>,
77}
78
79#[derive(Debug, Clone)]
81struct RangeData {
82 timestamps: Vec<f64>,
83 ranges: Vec<(f64, f64, f64, f64)>, _auxiliary: Option<Vec<f64>>,
85}
86
87impl RangeBarLayer {
88 pub fn new() -> Self {
89 Self {
90 enabled: true,
91 needs_render: true,
92 config: RangeBarConfig::default(),
93 cached_bars: Vec::new(),
94 last_viewport_hash: 0,
95 range_data: None,
96 }
97 }
98
99 pub fn with_config(config: RangeBarConfig) -> Self {
100 Self {
101 config,
102 ..Self::new()
103 }
104 }
105
106 pub fn with_style(style: RangeBarStyle) -> Self {
107 Self {
108 config: RangeBarConfig {
109 style,
110 ..Default::default()
111 },
112 ..Self::new()
113 }
114 }
115
116 pub fn set_range_data(
118 &mut self,
119 timestamps: Vec<f64>,
120 ranges: Vec<(f64, f64, f64, f64)>,
121 auxiliary: Option<Vec<f64>>,
122 ) {
123 self.range_data = Some(RangeData {
124 timestamps,
125 ranges,
126 _auxiliary: auxiliary,
127 });
128 self.last_viewport_hash = 0; }
130
131 fn viewport_hash(viewport: &Viewport) -> u64 {
133 use std::collections::hash_map::DefaultHasher;
134 use std::hash::{Hash, Hasher};
135
136 let mut hasher = DefaultHasher::new();
137
138 viewport.screen_rect.x.to_bits().hash(&mut hasher);
139 viewport.screen_rect.y.to_bits().hash(&mut hasher);
140 viewport.screen_rect.width.to_bits().hash(&mut hasher);
141 viewport.screen_rect.height.to_bits().hash(&mut hasher);
142 viewport
143 .chart_bounds
144 .time_start
145 .timestamp()
146 .hash(&mut hasher);
147 viewport.chart_bounds.time_end.timestamp().hash(&mut hasher);
148 viewport.chart_bounds.price_min.to_bits().hash(&mut hasher);
149 viewport.chart_bounds.price_max.to_bits().hash(&mut hasher);
150
151 hasher.finish()
152 }
153
154 fn calculate_geometry_from_data(&mut self, viewport: &Viewport, style: &ChartStyle) {
156 self.cached_bars.clear();
157
158 let data = match &self.range_data {
159 Some(d) => d,
160 None => return,
161 };
162
163 if data.timestamps.is_empty() {
164 return;
165 }
166
167 let count = data.timestamps.len();
168
169 let time_spacing = if count > 1 {
171 let first_time = data.timestamps[0] as f32;
172 let last_time = data.timestamps[count - 1] as f32;
173 (last_time - first_time) / (count - 1) as f32
174 } else {
175 3600.0
176 };
177
178 let content_rect = viewport.layout.main_panel;
179 let time_scale =
180 content_rect.width / (viewport.chart_bounds.time_duration().num_seconds() as f32);
181 let screen_time_width = time_spacing * time_scale;
182
183 let body_width = (screen_time_width * self.config.body_width_factor)
184 .max(self.config.min_width)
185 .min(self.config.max_width);
186
187 let half_body_width = body_width * 0.5;
188
189 for i in 0..count {
190 let timestamp = data.timestamps[i];
191 let (start_val, max_val, min_val, end_val) = data.ranges[i];
192
193 let delta_sec =
195 (timestamp - viewport.chart_bounds.time_start.timestamp() as f64) as f32;
196 let x = content_rect.x + delta_sec * time_scale;
197
198 let y_start = viewport.chart_to_screen_y(start_val as f32);
199 let y_max = viewport.chart_to_screen_y(max_val as f32);
200 let y_min = viewport.chart_to_screen_y(min_val as f32);
201 let y_end = viewport.chart_to_screen_y(end_val as f32);
202
203 let is_positive = end_val >= start_val;
204 let range = (max_val - min_val).abs().max(1e-9);
205 let body_span = (end_val - start_val).abs();
206 let is_neutral = (body_span / range) < 0.05;
207
208 let body_top = y_start.min(y_end);
209 let body_bottom = y_start.max(y_end);
210 let body_height = body_bottom - body_top;
211
212 let min_body_height = style.candles.min_body_height;
213 let adjusted_body_height = body_height.max(min_body_height);
214 let body_y = if body_height < min_body_height {
215 (body_top + body_bottom - adjusted_body_height) * 0.5
216 } else {
217 body_top
218 };
219
220 let body_rect = Rect::new(
221 x - half_body_width,
222 body_y,
223 body_width,
224 adjusted_body_height,
225 );
226
227 let line_top = if y_max < body_top {
228 (x, y_max, x, body_top)
229 } else {
230 (x, y_max, x, y_max)
231 };
232
233 let line_bottom = if y_min > body_bottom {
234 (x, body_bottom, x, y_min)
235 } else {
236 (x, y_min, x, y_min)
237 };
238
239 self.cached_bars.push(RangeBarGeometry {
240 body_rect,
241 line_top,
242 line_bottom,
243 is_positive,
244 is_neutral,
245 });
246 }
247 }
248
249 fn _calculate_geometry_from_series<S: RangeSeries + ?Sized>(
251 &mut self,
252 series: &S,
253 data: &ChartData,
254 viewport: &Viewport,
255 _theme: &ChartTheme,
256 style: &ChartStyle,
257 ) {
258 self.cached_bars.clear();
259
260 let (start_idx, end_idx) = match data.visible_indices() {
261 Some((start, end)) => (start, end),
262 None => (0, series.len()),
263 };
264
265 if start_idx >= end_idx {
266 return;
267 }
268
269 let count = end_idx - start_idx;
270
271 let time_spacing = if count > 1 {
272 let first_time = series.get_x(start_idx) as f32;
273 let last_time = series.get_x(end_idx - 1) as f32;
274 (last_time - first_time) / (count - 1) as f32
275 } else {
276 3600.0
277 };
278
279 let content_rect = viewport.layout.main_panel;
280 let time_scale =
281 content_rect.width / (viewport.chart_bounds.time_duration().num_seconds() as f32);
282 let screen_time_width = time_spacing * time_scale;
283
284 let body_width = (screen_time_width * self.config.body_width_factor)
285 .max(self.config.min_width)
286 .min(self.config.max_width);
287
288 let half_body_width = body_width * 0.5;
289
290 for i in start_idx..end_idx {
291 let (start_val, max_val, min_val, end_val) = series.get_range(i);
292 let timestamp_f64 = series.get_x(i);
293
294 let delta_sec =
295 (timestamp_f64 - viewport.chart_bounds.time_start.timestamp() as f64) as f32;
296 let x = content_rect.x + delta_sec * time_scale;
297
298 let y_start = viewport.chart_to_screen_y(start_val as f32);
299 let y_max = viewport.chart_to_screen_y(max_val as f32);
300 let y_min = viewport.chart_to_screen_y(min_val as f32);
301 let y_end = viewport.chart_to_screen_y(end_val as f32);
302
303 let is_positive = end_val >= start_val;
304 let range = (max_val - min_val).abs().max(1e-9);
305 let body_span = (end_val - start_val).abs();
306 let is_neutral = (body_span / range) < 0.05;
307
308 let body_top = y_start.min(y_end);
309 let body_bottom = y_start.max(y_end);
310 let body_height = body_bottom - body_top;
311
312 let min_body_height = style.candles.min_body_height;
313 let adjusted_body_height = body_height.max(min_body_height);
314 let body_y = if body_height < min_body_height {
315 (body_top + body_bottom - adjusted_body_height) * 0.5
316 } else {
317 body_top
318 };
319
320 let body_rect = Rect::new(
321 x - half_body_width,
322 body_y,
323 body_width,
324 adjusted_body_height,
325 );
326
327 let line_top = if y_max < body_top {
328 (x, y_max, x, body_top)
329 } else {
330 (x, y_max, x, y_max)
331 };
332
333 let line_bottom = if y_min > body_bottom {
334 (x, body_bottom, x, y_min)
335 } else {
336 (x, y_min, x, y_min)
337 };
338
339 self.cached_bars.push(RangeBarGeometry {
340 body_rect,
341 line_top,
342 line_bottom,
343 is_positive,
344 is_neutral,
345 });
346 }
347 }
348
349 fn render_cached_geometry(
351 &self,
352 context: &mut RenderContext,
353 theme: &ChartTheme,
354 ) -> Result<()> {
355 let content_rect = context.viewport().chart_content_rect();
356
357 for bar in &self.cached_bars {
358 if bar.body_rect.x + bar.body_rect.width < content_rect.x
359 || bar.body_rect.x > content_rect.x + content_rect.width
360 || bar.body_rect.y + bar.body_rect.height < content_rect.y
361 || bar.body_rect.y > content_rect.y + content_rect.height
362 {
363 continue;
364 }
365
366 let body_color = if bar.is_neutral {
367 theme.colors.candle_doji
368 } else if bar.is_positive {
369 theme.colors.candle_bullish
370 } else {
371 theme.colors.candle_bearish
372 };
373
374 let line_color = if bar.is_neutral {
375 theme.colors.wick_color
376 } else if bar.is_positive {
377 theme.colors.wick_bullish
378 } else {
379 theme.colors.wick_bearish
380 };
381
382 context.draw_rect(bar.body_rect, body_color);
383
384 let (x1, y1, x2, y2) = bar.line_top;
385 if (y2 - y1).abs() > 0.1 {
386 context.draw_line([x1, y1], [x2, y2], line_color, self.config.line_width);
387 }
388
389 let (x1, y1, x2, y2) = bar.line_bottom;
390 if (y2 - y1).abs() > 0.1 {
391 context.draw_line([x1, y1], [x2, y2], line_color, self.config.line_width);
392 }
393 }
394
395 Ok(())
396 }
397}
398
399impl Default for RangeBarLayer {
400 fn default() -> Self {
401 Self::new()
402 }
403}
404
405impl Layer for RangeBarLayer {
406 fn name(&self) -> &str {
407 "RangeBar"
408 }
409
410 fn stage(&self) -> LayerStage {
411 LayerStage::ChartMain
412 }
413
414 fn update(
415 &mut self,
416 _data: &ChartData,
417 viewport: &Viewport,
418 _theme: &ChartTheme,
419 style: &ChartStyle,
420 ) {
421 self.config.body_width_factor = style.candles.body_width_factor;
422 self.config.line_width = style.candles.wick_width;
423 self.config.min_width = style.candles.min_body_width;
424 self.config.max_width = style.candles.max_body_width;
425
426 let viewport_hash = Self::viewport_hash(viewport);
427
428 if viewport_hash != self.last_viewport_hash {
429 if self.range_data.is_some() {
431 self.calculate_geometry_from_data(viewport, style);
432 }
433 self.last_viewport_hash = viewport_hash;
436 }
437
438 self.needs_render = true;
439 }
440
441 fn render(
442 &self,
443 context: &mut RenderContext,
444 _render_pass: &mut wgpu::RenderPass,
445 ) -> Result<()> {
446 if !self.cached_bars.is_empty() {
447 let theme = context.theme().clone();
448 self.render_cached_geometry(context, &theme)?;
449 }
450 Ok(())
451 }
452
453 fn needs_render(&self) -> bool {
454 self.needs_render
455 }
456
457 fn z_order(&self) -> i32 {
458 1
459 }
460
461 fn is_enabled(&self) -> bool {
462 self.enabled
463 }
464
465 fn set_enabled(&mut self, enabled: bool) {
466 self.enabled = enabled;
467 self.needs_render = true;
468 }
469}
470
471pub type CandlestickLayer = RangeBarLayer;
473pub type CandlestickConfig = RangeBarConfig;