shape_viz_core/layers/
time_axis.rs1use 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#[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, _tick_length: 4.0,
30 _label_offset: 2.0, show_grid_lines: false,
32 }
33 }
34
35 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 fn find_nice_time_interval(&self, seconds: f64) -> f64 {
45 let intervals = [
47 1.0, 5.0, 10.0, 30.0, 60.0, 300.0, 600.0, 900.0, 1800.0, 3600.0, 7200.0, 14400.0, 21600.0, 43200.0, 86400.0, 604800.0, 2629746.0, ];
65
66 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 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, };
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 let _axis_line_y = content_rect.y + content_rect.height;
143
144 context.draw_rect(axis_rect, theme.colors.axis_background);
146
147 let time_range_seconds = chart_bounds.time_duration().num_seconds() as f64;
151 let target_label_count = (content_rect.width / 70.0) as i32; let raw_step = time_range_seconds / target_label_count as f64;
153 let nice_step = self.find_nice_time_interval(raw_step);
154
155 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 let mut prev_timestamp = None;
162 while current_timestamp <= chart_bounds.time_end.timestamp() {
163 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 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, axis_rect.y + axis_rect.height / 2.0, theme.colors.axis_label,
182 Some(theme.typography.secondary_font_size),
183 TextAnchor::Middle,
184 TextBaseline::Middle,
185 );
186 }
187
188 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 }
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}