sql_cli/chart/renderers/
line.rs

1use crate::chart::types::{ChartConfig, ChartViewport, DataSeries};
2use chrono::{DateTime, Utc};
3use ratatui::{
4    layout::Rect,
5    style::{Color, Style},
6    symbols,
7    widgets::{Axis, Chart, Dataset, GraphType},
8    Frame,
9};
10
11pub struct LineRenderer {
12    viewport: ChartViewport,
13}
14
15impl LineRenderer {
16    #[must_use]
17    pub fn new(viewport: ChartViewport) -> Self {
18        Self { viewport }
19    }
20
21    pub fn render(&self, frame: &mut Frame, area: Rect, data: &DataSeries, config: &ChartConfig) {
22        // Convert data points to ratatui format
23        let chart_data: Vec<(f64, f64)> = data
24            .points
25            .iter()
26            .filter(|p| {
27                p.x >= self.viewport.x_min
28                    && p.x <= self.viewport.x_max
29                    && p.y >= self.viewport.y_min
30                    && p.y <= self.viewport.y_max
31            })
32            .map(|p| (p.x, p.y))
33            .collect();
34
35        let datasets = self.create_datasets(data, &chart_data);
36        let (x_axis, y_axis) = self.create_axes(data, config);
37
38        let chart = Chart::new(datasets).x_axis(x_axis).y_axis(y_axis);
39
40        frame.render_widget(chart, area);
41    }
42
43    fn create_datasets<'a>(
44        &self,
45        data: &DataSeries,
46        chart_data: &'a [(f64, f64)],
47    ) -> Vec<Dataset<'a>> {
48        vec![Dataset::default()
49            .name(data.name.clone())
50            .marker(symbols::Marker::Braille)
51            .style(Style::default().fg(Color::Cyan))
52            .graph_type(GraphType::Line)
53            .data(chart_data)]
54    }
55
56    fn create_axes(&self, data: &DataSeries, config: &ChartConfig) -> (Axis, Axis) {
57        // Create X-axis
58        let x_axis = Axis::default()
59            .title(config.x_axis.clone())
60            .style(Style::default().fg(Color::Gray))
61            .bounds([self.viewport.x_min, self.viewport.x_max])
62            .labels(self.create_x_labels(data));
63
64        // Create Y-axis with smart scaling
65        let y_axis = Axis::default()
66            .title(config.y_axis.clone())
67            .style(Style::default().fg(Color::Gray))
68            .bounds([self.viewport.y_min, self.viewport.y_max])
69            .labels(self.create_y_labels());
70
71        (x_axis, y_axis)
72    }
73
74    fn create_x_labels(&self, data: &DataSeries) -> Vec<String> {
75        // Check if we have timestamps
76        let has_timestamps = data.points.iter().any(|p| p.timestamp.is_some());
77
78        if has_timestamps {
79            self.create_time_labels()
80        } else {
81            self.create_numeric_labels(self.viewport.x_min, self.viewport.x_max)
82        }
83    }
84
85    fn create_time_labels(&self) -> Vec<String> {
86        let num_labels = 5;
87        let x_span = self.viewport.x_max - self.viewport.x_min;
88        let step = x_span / (f64::from(num_labels) - 1.0);
89
90        (0..num_labels)
91            .map(|i| {
92                let timestamp_secs = self.viewport.x_min + f64::from(i) * step;
93                let dt = DateTime::<Utc>::from_timestamp(timestamp_secs as i64, 0)
94                    .unwrap_or_else(Utc::now);
95                format!("{}", dt.format("%H:%M:%S"))
96            })
97            .collect()
98    }
99
100    fn create_numeric_labels(&self, min: f64, max: f64) -> Vec<String> {
101        let num_labels = 5;
102        let step = (max - min) / (f64::from(num_labels) - 1.0);
103
104        (0..num_labels)
105            .map(|i| {
106                let value = min + f64::from(i) * step;
107                if value.abs() > 1000.0 {
108                    format!("{:.0}k", value / 1000.0)
109                } else if value.abs() < 1.0 {
110                    format!("{value:.3}")
111                } else {
112                    format!("{value:.1}")
113                }
114            })
115            .collect()
116    }
117
118    fn create_y_labels(&self) -> Vec<String> {
119        self.create_numeric_labels(self.viewport.y_min, self.viewport.y_max)
120    }
121
122    pub fn viewport_mut(&mut self) -> &mut ChartViewport {
123        &mut self.viewport
124    }
125
126    #[must_use]
127    pub fn viewport(&self) -> &ChartViewport {
128        &self.viewport
129    }
130}