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 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 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 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 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 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
82 let utc_dt = dt.with_timezone(&Utc);
83 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 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 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}