1use crate::data::ChartData;
4use crate::error::Result;
5use crate::layers::{Layer, LayerStage};
6use crate::renderer::RenderContext;
7use crate::style::ChartStyle;
8use crate::theme::ChartTheme;
9use crate::viewport::Viewport;
10use glam::Vec2;
11use serde_json::Value;
12
13#[derive(Debug)]
15pub struct GridLayer {
16 enabled: bool,
17 show_major_lines: bool,
18 show_minor_lines: bool,
19 auto_spacing: bool,
20 major_spacing_pixels: f32,
21 minor_divisions: u32,
22 needs_render: bool,
23}
24
25impl GridLayer {
26 pub fn new() -> Self {
28 Self {
29 enabled: true,
30 show_major_lines: true,
31 show_minor_lines: false, auto_spacing: true,
33 major_spacing_pixels: 50.0,
34 minor_divisions: 5,
35 needs_render: true,
36 }
37 }
38
39 pub fn set_show_major_lines(&mut self, show: bool) {
41 if self.show_major_lines != show {
42 self.show_major_lines = show;
43 self.needs_render = true;
44 }
45 }
46
47 pub fn set_show_minor_lines(&mut self, show: bool) {
49 if self.show_minor_lines != show {
50 self.show_minor_lines = show;
51 self.needs_render = true;
52 }
53 }
54
55 pub fn set_auto_spacing(&mut self, auto: bool) {
57 if self.auto_spacing != auto {
58 self.auto_spacing = auto;
59 self.needs_render = true;
60 }
61 }
62
63 pub fn set_major_spacing_pixels(&mut self, spacing: f32) {
65 if (self.major_spacing_pixels - spacing).abs() > 0.1 {
66 self.major_spacing_pixels = spacing;
67 self.needs_render = true;
68 }
69 }
70
71 fn render_horizontal_lines(
73 &self,
74 context: &mut RenderContext,
75 viewport: &Viewport,
76 theme: &ChartTheme,
77 ) {
78 let content_rect = viewport.chart_content_rect();
79 let chart_bounds = &viewport.chart_bounds;
80
81 let price_levels = crate::utils::calculate_price_levels(
83 chart_bounds.price_min,
84 chart_bounds.price_max,
85 content_rect.height,
86 );
87
88 for &price_level in &price_levels {
90 let chart_pos = glam::Vec2::new(0.0, price_level as f32);
92 let screen_pos = viewport.chart_to_screen(chart_pos);
93
94 if screen_pos.y >= content_rect.y
95 && screen_pos.y <= content_rect.y + content_rect.height
96 {
97 let color = theme.colors.grid_major.with_alpha(0.35);
98
99 context.draw_line(
100 [content_rect.x, screen_pos.y],
101 [content_rect.x + content_rect.width, screen_pos.y],
102 color,
103 1.0,
104 );
105 }
106 }
107 }
108
109 fn render_vertical_lines(
111 &self,
112 context: &mut RenderContext,
113 viewport: &Viewport,
114 theme: &ChartTheme,
115 ) {
116 let content_rect = viewport.chart_content_rect();
117 let chart_bounds = &viewport.chart_bounds;
118
119 let time_range_seconds = chart_bounds.time_duration().num_seconds() as f64;
121 let target_label_count = (content_rect.width / theme.spacing.grid_spacing_min) as i32;
122 let time_step_seconds = time_range_seconds / target_label_count as f64;
123
124 let nice_time_step = self.find_nice_time_interval(time_step_seconds);
126 let start_timestamp = ((chart_bounds.time_start.timestamp() as f64 / nice_time_step)
127 .floor()
128 * nice_time_step) as i64;
129
130 let mut current_timestamp = start_timestamp;
131
132 while current_timestamp <= chart_bounds.time_end.timestamp() {
133 let chart_pos = Vec2::new(current_timestamp as f32, 0.0);
135 let screen_pos = viewport.chart_to_screen(chart_pos);
136
137 if screen_pos.x >= content_rect.x && screen_pos.x <= content_rect.x + content_rect.width
138 {
139 let color = theme.colors.grid_major.with_alpha(0.35);
140 context.draw_line(
141 [screen_pos.x, content_rect.y],
142 [screen_pos.x, content_rect.y + content_rect.height],
143 color,
144 1.0,
145 );
146 }
147
148 current_timestamp += nice_time_step as i64;
149 }
150 }
151
152 fn find_nice_time_interval(&self, seconds: f64) -> f64 {
154 let intervals = [
156 1.0, 5.0, 10.0, 15.0, 30.0, 60.0, 300.0, 600.0, 900.0, 1800.0, 3600.0, 14400.0, 28800.0, 43200.0, 86400.0, 604800.0, 2592000.0, 7776000.0, 31536000.0, ];
176
177 intervals
179 .iter()
180 .min_by(|&&a, &&b| {
181 let diff_a = (a - seconds).abs();
182 let diff_b = (b - seconds).abs();
183 diff_a
184 .partial_cmp(&diff_b)
185 .unwrap_or(std::cmp::Ordering::Equal)
186 })
187 .copied()
188 .unwrap_or(seconds)
189 }
190}
191
192impl Default for GridLayer {
193 fn default() -> Self {
194 Self::new()
195 }
196}
197
198impl Layer for GridLayer {
199 fn name(&self) -> &str {
200 "Grid"
201 }
202
203 fn stage(&self) -> LayerStage {
204 LayerStage::ChartUnderlay
205 }
206
207 fn update(
208 &mut self,
209 _data: &ChartData,
210 _viewport: &Viewport,
211 _theme: &ChartTheme,
212 _style: &ChartStyle,
213 ) {
214 self.needs_render = true;
216 }
217
218 fn render(
219 &self,
220 context: &mut RenderContext,
221 _render_pass: &mut wgpu::RenderPass,
222 ) -> Result<()> {
223 if !self.enabled {
224 return Ok(());
225 }
226
227 let viewport = context.viewport().clone();
228 let theme = context.theme().clone();
229
230 if self.show_major_lines || self.show_minor_lines {
232 self.render_vertical_lines(context, &viewport, &theme);
233 self.render_horizontal_lines(context, &viewport, &theme);
234 }
235
236 Ok(())
237 }
238
239 fn needs_render(&self) -> bool {
240 self.needs_render
241 }
242
243 fn z_order(&self) -> i32 {
244 -100 }
246
247 fn is_enabled(&self) -> bool {
248 self.enabled
249 }
250
251 fn set_enabled(&mut self, enabled: bool) {
252 if self.enabled != enabled {
253 self.enabled = enabled;
254 self.needs_render = true;
255 }
256 }
257
258 fn get_config(&self) -> Value {
259 serde_json::json!({
260 "show_major_lines": self.show_major_lines,
261 "show_minor_lines": self.show_minor_lines,
262 "auto_spacing": self.auto_spacing,
263 "major_spacing_pixels": self.major_spacing_pixels,
264 "minor_divisions": self.minor_divisions
265 })
266 }
267
268 fn set_config(&mut self, config: Value) -> Result<()> {
269 if let Some(show_major) = config.get("show_major_lines").and_then(|v| v.as_bool()) {
270 self.set_show_major_lines(show_major);
271 }
272 if let Some(show_minor) = config.get("show_minor_lines").and_then(|v| v.as_bool()) {
273 self.set_show_minor_lines(show_minor);
274 }
275 if let Some(auto_spacing) = config.get("auto_spacing").and_then(|v| v.as_bool()) {
276 self.set_auto_spacing(auto_spacing);
277 }
278 if let Some(spacing) = config.get("major_spacing_pixels").and_then(|v| v.as_f64()) {
279 self.set_major_spacing_pixels(spacing as f32);
280 }
281 if let Some(divisions) = config.get("minor_divisions").and_then(|v| v.as_u64()) {
282 self.minor_divisions = divisions as u32;
283 self.needs_render = true;
284 }
285
286 Ok(())
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn test_grid_layer_creation() {
296 let layer = GridLayer::new();
297 assert_eq!(layer.name(), "Grid");
298 assert!(layer.is_enabled());
299 assert!(layer.needs_render());
300 assert_eq!(layer.z_order(), -100);
301 }
302
303 #[test]
304 fn test_grid_layer_configuration() {
305 let mut layer = GridLayer::new();
306
307 layer.set_show_major_lines(false);
308 assert!(!layer.show_major_lines);
309 assert!(layer.needs_render());
310
311 layer.set_auto_spacing(false);
312 assert!(!layer.auto_spacing);
313
314 layer.set_major_spacing_pixels(100.0);
315 assert_eq!(layer.major_spacing_pixels, 100.0);
316 }
317
318 #[test]
319 fn test_time_interval_calculation() {
320 let layer = GridLayer::new();
321
322 assert_eq!(layer.find_nice_time_interval(3.0), 1.0); assert_eq!(layer.find_nice_time_interval(50.0), 60.0); assert_eq!(layer.find_nice_time_interval(400.0), 300.0); assert_eq!(layer.find_nice_time_interval(3000.0), 3600.0); }
327}