use super::types::{DataAnalysis, IntelligenceConfig};
use crate::chart::{ChartSpec, MarkType, Encoding, EncodingDef, DataType};
use polars::prelude::*;
use std::collections::HashMap;
pub struct DataAnalyzer {
config: IntelligenceConfig,
}
impl DataAnalyzer {
pub fn new(config: IntelligenceConfig) -> Self {
Self { config }
}
pub fn analyze_data_characteristics(&self, data: &DataFrame) -> DataAnalysis {
let mut column_types = HashMap::new();
let mut suggested_charts = Vec::new();
let mut characteristics = Vec::new();
for (name, dtype) in data.schema().iter() {
let type_str = match dtype {
polars::prelude::DataType::Date => "date",
polars::prelude::DataType::Datetime(_, _) => "datetime",
polars::prelude::DataType::Int32 | polars::prelude::DataType::Int64 | polars::prelude::DataType::Float32 | polars::prelude::DataType::Float64 => "numeric",
polars::prelude::DataType::String | polars::prelude::DataType::Categorical(_, _) => "categorical",
polars::prelude::DataType::Boolean => "boolean",
_ => "other",
};
column_types.insert(name.to_string(), type_str.to_string());
}
if self.has_date_column(&column_types) {
characteristics.push("time_series".to_string());
suggested_charts.push(MarkType::Line { interpolate: None, stroke_width: None, stroke_dash: None });
}
if self.has_numeric_columns(&column_types) && self.has_categorical_columns(&column_types) {
characteristics.push("comparative".to_string());
suggested_charts.push(MarkType::Bar { width: None, corner_radius: None });
}
if self.count_numeric_columns(&column_types) >= 2 {
characteristics.push("correlational".to_string());
suggested_charts.push(MarkType::Point { size: None, shape: None, opacity: None });
}
if self.has_categorical_columns(&column_types) {
characteristics.push("categorical".to_string());
suggested_charts.push(MarkType::Bar { width: None, corner_radius: None });
}
DataAnalysis {
column_types,
suggested_charts,
characteristics,
}
}
pub fn create_time_series_suggestion(&self, schema: &Schema) -> ChartSpec {
let mut spec = ChartSpec::new();
let date_col = schema.iter()
.find(|(_, dtype)| matches!(dtype, polars::prelude::DataType::Date | polars::prelude::DataType::Datetime(_, _)))
.map(|(name, _)| name.to_string())
.unwrap_or_else(|| schema.iter().next().map(|(name, _)| name.to_string()).unwrap_or_else(|| "date".to_string()));
let numeric_col = schema.iter()
.find(|(_, dtype)| matches!(dtype, polars::prelude::DataType::Int32 | polars::prelude::DataType::Int64 | polars::prelude::DataType::Float32 | polars::prelude::DataType::Float64))
.map(|(name, _)| name.to_string())
.unwrap_or_else(|| schema.iter().nth(1).map(|(name, _)| name.to_string()).unwrap_or_else(|| "value".to_string()));
spec.mark = MarkType::Line { interpolate: None, stroke_width: None, stroke_dash: None };
spec.encoding = Encoding {
x: Some(EncodingDef {
field: date_col,
data_type: DataType::Date,
scale: None,
axis: None,
legend: None,
bin: None,
aggregate: None,
sort: None,
}),
y: Some(EncodingDef {
field: numeric_col,
data_type: DataType::Number,
scale: None,
axis: None,
legend: None,
bin: None,
aggregate: None,
sort: None,
}),
color: None,
size: None,
shape: None,
opacity: None,
text: None,
tooltip: None,
detail: None,
order: None,
row: None,
column: None,
};
spec
}
pub fn create_categorical_comparison_suggestion(&self, schema: &Schema) -> ChartSpec {
let mut spec = ChartSpec::new();
let cat_col = schema.iter()
.find(|(_, dtype)| matches!(dtype, polars::prelude::DataType::String | polars::prelude::DataType::Categorical(_, _)))
.map(|(name, _)| name.to_string())
.unwrap_or_else(|| schema.iter().next().map(|(name, _)| name.to_string()).unwrap_or_else(|| "category".to_string()));
let numeric_col = schema.iter()
.find(|(_, dtype)| matches!(dtype, polars::prelude::DataType::Int32 | polars::prelude::DataType::Int64 | polars::prelude::DataType::Float32 | polars::prelude::DataType::Float64))
.map(|(name, _)| name.to_string())
.unwrap_or_else(|| schema.iter().nth(1).map(|(name, _)| name.to_string()).unwrap_or_else(|| "value".to_string()));
spec.mark = MarkType::Bar { width: None, corner_radius: None };
spec.encoding = self.create_encoding(&cat_col, &numeric_col, DataType::String, DataType::Number);
spec
}
pub fn create_scatter_plot_suggestion(&self, schema: &Schema) -> ChartSpec {
let mut spec = ChartSpec::new();
let numeric_cols: Vec<String> = schema.iter()
.filter(|(_, dtype)| matches!(dtype, polars::prelude::DataType::Int32 | polars::prelude::DataType::Int64 | polars::prelude::DataType::Float32 | polars::prelude::DataType::Float64))
.map(|(name, _)| name.to_string())
.collect();
if numeric_cols.len() >= 2 {
spec.mark = MarkType::Point { size: None, shape: None, opacity: None };
spec.encoding = self.create_encoding(&numeric_cols[0], &numeric_cols[1], DataType::Number, DataType::Number);
}
spec
}
pub fn create_treemap_suggestion(&self, schema: &Schema) -> ChartSpec {
let mut spec = ChartSpec::new();
let cat_col = schema.iter()
.find(|(_, dtype)| matches!(dtype, polars::prelude::DataType::String | polars::prelude::DataType::Categorical(_, _)))
.map(|(name, _)| name.to_string())
.unwrap_or_else(|| schema.iter().next().map(|(name, _)| name.to_string()).unwrap_or_else(|| "category".to_string()));
let numeric_col = schema.iter()
.find(|(_, dtype)| matches!(dtype, polars::prelude::DataType::Int32 | polars::prelude::DataType::Int64 | polars::prelude::DataType::Float32 | polars::prelude::DataType::Float64))
.map(|(name, _)| name.to_string())
.unwrap_or_else(|| schema.iter().nth(1).map(|(name, _)| name.to_string()).unwrap_or_else(|| "value".to_string()));
spec.mark = MarkType::Rect { stroke: None, stroke_width: None };
spec.encoding = self.create_encoding(&cat_col, &numeric_col, DataType::String, DataType::Number);
spec
}
fn has_date_column(&self, column_types: &HashMap<String, String>) -> bool {
column_types.values().any(|t| t == "date" || t == "datetime")
}
fn has_numeric_columns(&self, column_types: &HashMap<String, String>) -> bool {
column_types.values().any(|t| t == "numeric")
}
fn has_categorical_columns(&self, column_types: &HashMap<String, String>) -> bool {
column_types.values().any(|t| t == "categorical")
}
fn count_numeric_columns(&self, column_types: &HashMap<String, String>) -> usize {
column_types.values().filter(|t| t == &"numeric").count()
}
fn create_encoding(&self, x_field: &str, y_field: &str, x_type: DataType, y_type: DataType) -> Encoding {
Encoding {
x: Some(EncodingDef {
field: x_field.to_string(),
data_type: x_type,
scale: None,
axis: None,
legend: None,
bin: None,
aggregate: None,
sort: None,
}),
y: Some(EncodingDef {
field: y_field.to_string(),
data_type: y_type,
scale: None,
axis: None,
legend: None,
bin: None,
aggregate: None,
sort: None,
}),
color: None,
size: None,
shape: None,
opacity: None,
text: None,
tooltip: None,
detail: None,
order: None,
row: None,
column: None,
}
}
}