use crate::data::ChartData;
use crate::error::Result;
use crate::layers::{Layer, LayerStage};
use crate::renderer::RenderContext;
use crate::style::ChartStyle;
use crate::theme::ChartTheme;
use crate::viewport::Viewport;
use chrono::{DateTime, Utc};
#[derive(Debug)]
pub struct TimeAxisLayer {
enabled: bool,
needs_render: bool,
axis_height: f32,
_tick_length: f32,
_label_offset: f32,
show_grid_lines: bool,
}
impl TimeAxisLayer {
pub fn new() -> Self {
Self {
enabled: true,
needs_render: true,
axis_height: 25.0, _tick_length: 4.0,
_label_offset: 2.0, show_grid_lines: false,
}
}
pub fn set_axis_height(&mut self, height: f32) {
if (self.axis_height - height).abs() > 0.1 {
self.axis_height = height;
self.needs_render = true;
}
}
fn find_nice_time_interval(&self, seconds: f64) -> f64 {
let intervals = [
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, ];
intervals
.iter()
.min_by(|&&a, &&b| {
let diff_a = (a - seconds).abs();
let diff_b = (b - seconds).abs();
diff_a
.partial_cmp(&diff_b)
.unwrap_or(std::cmp::Ordering::Equal)
})
.copied()
.unwrap_or(seconds)
}
fn format_time(&self, timestamp: i64, prev_timestamp: Option<i64>, interval: f64) -> String {
let dt = DateTime::<Utc>::from_timestamp(timestamp, 0).unwrap();
let prev_dt = prev_timestamp.map(|ts| DateTime::<Utc>::from_timestamp(ts, 0).unwrap());
let show_date = match prev_dt {
Some(prev) => dt.date_naive() != prev.date_naive(),
None => true, };
if show_date {
return dt.format("%d %b").to_string();
}
match interval {
i if i < 60.0 => dt.format("%H:%M:%S").to_string(),
_ => dt.format("%H:%M").to_string(),
}
}
}
impl Default for TimeAxisLayer {
fn default() -> Self {
Self::new()
}
}
impl Layer for TimeAxisLayer {
fn name(&self) -> &str {
"TimeAxis"
}
fn stage(&self) -> LayerStage {
LayerStage::TimeAxis
}
fn update(
&mut self,
_data: &ChartData,
_viewport: &Viewport,
_theme: &ChartTheme,
_style: &ChartStyle,
) {
self.needs_render = true;
}
fn render(
&self,
context: &mut RenderContext,
_render_pass: &mut wgpu::RenderPass,
) -> Result<()> {
if !self.enabled {
return Ok(());
}
let viewport = context.viewport().clone();
let theme = context.theme().clone();
let content_rect = viewport.chart_content_rect();
let axis_rect = viewport.time_axis_rect();
let chart_bounds = &viewport.chart_bounds;
let _axis_line_y = content_rect.y + content_rect.height;
context.draw_rect(axis_rect, theme.colors.axis_background);
let time_range_seconds = chart_bounds.time_duration().num_seconds() as f64;
let target_label_count = (content_rect.width / 70.0) as i32; let raw_step = time_range_seconds / target_label_count as f64;
let nice_step = self.find_nice_time_interval(raw_step);
let start_timestamp =
((chart_bounds.time_start.timestamp() as f64 / nice_step).floor() * nice_step) as i64;
let mut current_timestamp = start_timestamp;
let mut prev_timestamp = None;
while current_timestamp <= chart_bounds.time_end.timestamp() {
let chart_pos = glam::Vec2::new(current_timestamp as f32, 0.0);
let screen_pos = viewport.chart_to_screen(chart_pos);
if screen_pos.x >= content_rect.x && screen_pos.x <= content_rect.x + content_rect.width
{
let time_text = self.format_time(current_timestamp, prev_timestamp, nice_step);
#[cfg(feature = "text-rendering")]
{
use crate::text::{TextAnchor, TextBaseline};
context.draw_text_anchored(
&time_text,
screen_pos.x, axis_rect.y + axis_rect.height / 2.0, theme.colors.axis_label,
Some(theme.typography.secondary_font_size),
TextAnchor::Middle,
TextBaseline::Middle,
);
}
if self.show_grid_lines {
context.draw_line(
[screen_pos.x, content_rect.y],
[screen_pos.x, content_rect.y + content_rect.height],
theme.colors.grid_minor,
0.5,
);
}
}
prev_timestamp = Some(current_timestamp);
current_timestamp += nice_step as i64;
}
Ok(())
}
fn needs_render(&self) -> bool {
self.needs_render
}
fn z_order(&self) -> i32 {
60 }
fn is_enabled(&self) -> bool {
self.enabled
}
fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
self.needs_render = true;
}
}