sql_cli/chart/renderers/
line.rs1use 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 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 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 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 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}