sql_cli/chart/renderers/
line.rs

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