sql_cli/chart/
engine.rs

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