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