sql_cli/chart/
engine.rs

1use crate::chart::types::{ChartConfig, DataPoint, DataSeries};
2use crate::data::data_view::DataView;
3use anyhow::{anyhow, Result};
4use chrono::{DateTime, Utc};
5
6pub struct ChartEngine {
7    data_view: DataView,
8}
9
10impl ChartEngine {
11    #[must_use]
12    pub fn new(data_view: DataView) -> Self {
13        Self { data_view }
14    }
15
16    pub fn execute_chart_query(&mut self, config: &ChartConfig) -> Result<DataSeries> {
17        // The SQL query has already been executed in chart_main.rs
18        // Just extract chart data from the already-filtered data_view
19        self.extract_chart_data(&self.data_view, config)
20    }
21
22    fn extract_chart_data(&self, data: &DataView, config: &ChartConfig) -> Result<DataSeries> {
23        let headers = data.column_names();
24
25        // Find column indices
26        let x_col_idx = headers
27            .iter()
28            .position(|h| h == &config.x_axis)
29            .ok_or_else(|| anyhow!("X-axis column '{}' not found", config.x_axis))?;
30        let y_col_idx = headers
31            .iter()
32            .position(|h| h == &config.y_axis)
33            .ok_or_else(|| anyhow!("Y-axis column '{}' not found", config.y_axis))?;
34
35        let mut points = Vec::new();
36        let mut x_min = f64::MAX;
37        let mut x_max = f64::MIN;
38        let mut y_min = f64::MAX;
39        let mut y_max = f64::MIN;
40
41        // Convert data to chart points
42        for row_idx in 0..data.row_count() {
43            let x_value_str = data.get_cell_value(row_idx, x_col_idx);
44            let y_value_str = data.get_cell_value(row_idx, y_col_idx);
45
46            if let (Some(x_str), Some(y_str)) = (x_value_str, y_value_str) {
47                let (x_float, timestamp) = self.convert_str_to_float_with_time(&x_str)?;
48                let y_float = self.convert_str_to_float(&y_str)?;
49
50                // Skip invalid data points
51                if x_float.is_finite() && y_float.is_finite() {
52                    points.push(DataPoint {
53                        x: x_float,
54                        y: y_float,
55                        timestamp,
56                        label: None,
57                    });
58
59                    x_min = x_min.min(x_float);
60                    x_max = x_max.max(x_float);
61                    y_min = y_min.min(y_float);
62                    y_max = y_max.max(y_float);
63                }
64            }
65        }
66
67        if points.is_empty() {
68            return Err(anyhow!("No valid data points found"));
69        }
70
71        Ok(DataSeries {
72            name: format!("{} vs {}", config.y_axis, config.x_axis),
73            points,
74            x_range: (x_min, x_max),
75            y_range: (y_min, y_max),
76        })
77    }
78
79    fn convert_str_to_float_with_time(&self, s: &str) -> Result<(f64, Option<DateTime<Utc>>)> {
80        // Try to parse as timestamp first - handle multiple formats
81        if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
82            let utc_dt = dt.with_timezone(&Utc);
83            // Convert to seconds since epoch for plotting
84            let timestamp_secs = utc_dt.timestamp() as f64;
85            Ok((timestamp_secs, Some(utc_dt)))
86        } else if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
87            // Handle format like "2025-08-12T09:00:00" (no timezone)
88            let utc_dt = dt.and_utc();
89            let timestamp_secs = utc_dt.timestamp() as f64;
90            Ok((timestamp_secs, Some(utc_dt)))
91        } else if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S.%f") {
92            // Handle format like "2025-08-12T09:00:18.030000" (with microseconds, no timezone)
93            let utc_dt = dt.and_utc();
94            let timestamp_secs = utc_dt.timestamp() as f64;
95            Ok((timestamp_secs, Some(utc_dt)))
96        } else if let Ok(f) = s.parse::<f64>() {
97            Ok((f, None))
98        } else {
99            Err(anyhow!("Cannot convert '{}' to numeric value", s))
100        }
101    }
102
103    fn convert_str_to_float(&self, s: &str) -> Result<f64> {
104        s.parse::<f64>()
105            .map_err(|_| anyhow!("Cannot convert '{}' to numeric value", s))
106    }
107}