Skip to main content

shape_viz_core/layers/
time_axis.rs

1//! Time axis rendering layer
2
3use 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 chrono::{DateTime, Utc};
11
12/// Layer for rendering time axis
13#[derive(Debug)]
14pub struct TimeAxisLayer {
15    enabled: bool,
16    needs_render: bool,
17    axis_height: f32,
18    _tick_length: f32,
19    _label_offset: f32,
20    show_grid_lines: bool,
21}
22
23impl TimeAxisLayer {
24    pub fn new() -> Self {
25        Self {
26            enabled: true,
27            needs_render: true,
28            axis_height: 25.0, // Reduced from 30.0
29            _tick_length: 4.0,
30            _label_offset: 2.0, // Reduced from 3.0
31            show_grid_lines: false,
32        }
33    }
34
35    /// Set the height of the time axis
36    pub fn set_axis_height(&mut self, height: f32) {
37        if (self.axis_height - height).abs() > 0.1 {
38            self.axis_height = height;
39            self.needs_render = true;
40        }
41    }
42
43    /// Find a nice time interval in seconds
44    fn find_nice_time_interval(&self, seconds: f64) -> f64 {
45        // Common time intervals in seconds
46        let intervals = [
47            1.0,       // 1 second
48            5.0,       // 5 seconds
49            10.0,      // 10 seconds
50            30.0,      // 30 seconds
51            60.0,      // 1 minute
52            300.0,     // 5 minutes
53            600.0,     // 10 minutes
54            900.0,     // 15 minutes
55            1800.0,    // 30 minutes
56            3600.0,    // 1 hour
57            7200.0,    // 2 hours
58            14400.0,   // 4 hours
59            21600.0,   // 6 hours
60            43200.0,   // 12 hours
61            86400.0,   // 1 day
62            604800.0,  // 1 week
63            2629746.0, // 1 month (approximate)
64        ];
65
66        // Find the closest interval
67        intervals
68            .iter()
69            .min_by(|&&a, &&b| {
70                let diff_a = (a - seconds).abs();
71                let diff_b = (b - seconds).abs();
72                diff_a
73                    .partial_cmp(&diff_b)
74                    .unwrap_or(std::cmp::Ordering::Equal)
75            })
76            .copied()
77            .unwrap_or(seconds)
78    }
79
80    /// Format timestamp based on the interval
81    fn format_time(&self, timestamp: i64, prev_timestamp: Option<i64>, interval: f64) -> String {
82        let dt = DateTime::<Utc>::from_timestamp(timestamp, 0).unwrap();
83        let prev_dt = prev_timestamp.map(|ts| DateTime::<Utc>::from_timestamp(ts, 0).unwrap());
84
85        let show_date = match prev_dt {
86            Some(prev) => dt.date_naive() != prev.date_naive(),
87            None => true, // Always show date for the first label
88        };
89
90        if show_date {
91            return dt.format("%d %b").to_string();
92        }
93
94        match interval {
95            i if i < 60.0 => dt.format("%H:%M:%S").to_string(),
96            _ => dt.format("%H:%M").to_string(),
97        }
98    }
99}
100
101impl Default for TimeAxisLayer {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107impl Layer for TimeAxisLayer {
108    fn name(&self) -> &str {
109        "TimeAxis"
110    }
111
112    fn stage(&self) -> LayerStage {
113        LayerStage::TimeAxis
114    }
115
116    fn update(
117        &mut self,
118        _data: &ChartData,
119        _viewport: &Viewport,
120        _theme: &ChartTheme,
121        _style: &ChartStyle,
122    ) {
123        self.needs_render = true;
124    }
125
126    fn render(
127        &self,
128        context: &mut RenderContext,
129        _render_pass: &mut wgpu::RenderPass,
130    ) -> Result<()> {
131        if !self.enabled {
132            return Ok(());
133        }
134
135        let viewport = context.viewport().clone();
136        let theme = context.theme().clone();
137        let content_rect = viewport.chart_content_rect();
138        let axis_rect = viewport.time_axis_rect();
139        let chart_bounds = &viewport.chart_bounds;
140
141        // The axis line is at the top edge of the axis area (boundary between content and axis)
142        let _axis_line_y = content_rect.y + content_rect.height;
143
144        // Draw axis background
145        context.draw_rect(axis_rect, theme.colors.axis_background);
146
147        // Don't draw axis line - reference chart has no visible axis lines
148
149        // Calculate time range and steps
150        let time_range_seconds = chart_bounds.time_duration().num_seconds() as f64;
151        let target_label_count = (content_rect.width / 70.0) as i32; // Roughly 70 pixels between labels
152        let raw_step = time_range_seconds / target_label_count as f64;
153        let nice_step = self.find_nice_time_interval(raw_step);
154
155        // Start from a nice round timestamp
156        let start_timestamp =
157            ((chart_bounds.time_start.timestamp() as f64 / nice_step).floor() * nice_step) as i64;
158        let mut current_timestamp = start_timestamp;
159
160        // Draw time labels and ticks
161        let mut prev_timestamp = None;
162        while current_timestamp <= chart_bounds.time_end.timestamp() {
163            // Convert timestamp to screen coordinates
164            let chart_pos = glam::Vec2::new(current_timestamp as f32, 0.0);
165            let screen_pos = viewport.chart_to_screen(chart_pos);
166
167            if screen_pos.x >= content_rect.x && screen_pos.x <= content_rect.x + content_rect.width
168            {
169                // Don't draw tick marks - reference chart has no visible ticks
170
171                // Draw time label centered on the tick mark
172                let time_text = self.format_time(current_timestamp, prev_timestamp, nice_step);
173
174                #[cfg(feature = "text-rendering")]
175                {
176                    use crate::text::{TextAnchor, TextBaseline};
177                    context.draw_text_anchored(
178                        &time_text,
179                        screen_pos.x,                         // Center on the grid line
180                        axis_rect.y + axis_rect.height / 2.0, // Center vertically in axis
181                        theme.colors.axis_label,
182                        Some(theme.typography.secondary_font_size),
183                        TextAnchor::Middle,
184                        TextBaseline::Middle,
185                    );
186                }
187
188                // Optionally draw grid line
189                if self.show_grid_lines {
190                    context.draw_line(
191                        [screen_pos.x, content_rect.y],
192                        [screen_pos.x, content_rect.y + content_rect.height],
193                        theme.colors.grid_minor,
194                        0.5,
195                    );
196                }
197            }
198
199            prev_timestamp = Some(current_timestamp);
200            current_timestamp += nice_step as i64;
201        }
202
203        Ok(())
204    }
205
206    fn needs_render(&self) -> bool {
207        self.needs_render
208    }
209
210    fn z_order(&self) -> i32 {
211        60 // Above grid and candles
212    }
213
214    fn is_enabled(&self) -> bool {
215        self.enabled
216    }
217
218    fn set_enabled(&mut self, enabled: bool) {
219        self.enabled = enabled;
220        self.needs_render = true;
221    }
222}