use super::column_inference::InferredColumnType;
use super::data::DataType;
use super::mark::Mark;
#[derive(Debug, Clone)]
pub struct ChartRecommendation {
pub mark: Mark,
pub confidence: f64,
pub reason: String,
}
impl ChartRecommendation {
pub fn new(mark: Mark, confidence: f64, reason: impl Into<String>) -> Self {
Self {
mark,
confidence,
reason: reason.into(),
}
}
}
pub fn recommend_charts(
x_field: &InferredColumnType,
y_field: Option<&InferredColumnType>,
) -> Vec<ChartRecommendation> {
let mut recommendations = Vec::new();
match (&x_field.semantic_type, y_field.map(|f| &f.semantic_type)) {
(DataType::Temporal, Some(DataType::Quantitative)) => {
recommendations.push(ChartRecommendation::new(
Mark::Line,
0.95,
"Time series data pairs well with line charts to show trends over time",
));
recommendations.push(ChartRecommendation::new(
Mark::Area,
0.85,
"Area charts emphasize cumulative trends and volume over time",
));
}
(DataType::Nominal, Some(DataType::Quantitative)) if x_field.cardinality <= 50 => {
recommendations.push(ChartRecommendation::new(
Mark::Bar,
0.90,
"Bar charts are ideal for comparing categorical values",
));
}
(DataType::Nominal, Some(DataType::Quantitative)) if x_field.cardinality > 50 => {
recommendations.push(ChartRecommendation::new(
Mark::Bar,
0.50,
format!(
"Bar chart with {} categories may be hard to read. Consider filtering data.",
x_field.cardinality
),
));
}
(DataType::Ordinal, Some(DataType::Quantitative)) if x_field.cardinality <= 50 => {
recommendations.push(ChartRecommendation::new(
Mark::Bar,
0.85,
"Bar charts work well for ordered categorical data (rankings, sizes, etc.)",
));
}
(DataType::Quantitative, Some(DataType::Quantitative)) => {
recommendations.push(ChartRecommendation::new(
Mark::Point,
0.90,
"Scatter plots reveal correlations and patterns between two numeric variables",
));
recommendations.push(ChartRecommendation::new(
Mark::Circle,
0.85,
"Bubble charts can encode a third dimension via circle size",
));
}
(DataType::Nominal, None) if x_field.cardinality <= 10 => {
recommendations.push(ChartRecommendation::new(
Mark::Arc,
0.80,
"Pie charts show part-to-whole relationships for few categories",
));
}
(DataType::Nominal, None) if x_field.cardinality <= 50 => {
recommendations.push(ChartRecommendation::new(
Mark::Bar,
0.75,
"Bar charts are better than pie charts for comparing many categories",
));
}
(DataType::Quantitative, None) => {
recommendations.push(ChartRecommendation::new(
Mark::Bar,
0.60,
"Use bar chart to show distribution of values (group into bins first)",
));
}
_ => {
recommendations.push(ChartRecommendation::new(
Mark::Bar,
0.50,
"Bar chart is a versatile default for many data types",
));
}
}
recommendations.sort_by(|a, b| {
b.confidence
.partial_cmp(&a.confidence)
.unwrap_or(std::cmp::Ordering::Equal)
});
recommendations
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::column_inference::{FieldValueType, TypeMetadata};
fn make_inferred(semantic_type: DataType, cardinality: usize) -> InferredColumnType {
InferredColumnType {
storage_type: FieldValueType::Numeric,
semantic_type,
confidence: 1.0,
cardinality,
null_count: 0,
sample_size: 100,
metadata: TypeMetadata::default(),
}
}
#[test]
fn test_recommends_line_for_time_series() {
let x = make_inferred(DataType::Temporal, 30);
let y = make_inferred(DataType::Quantitative, 30);
let recs = recommend_charts(&x, Some(&y));
assert!(!recs.is_empty());
assert_eq!(recs[0].mark, Mark::Line);
assert!(recs[0].confidence >= 0.9);
}
#[test]
fn test_recommends_bar_for_categorical_comparison() {
let x = make_inferred(DataType::Nominal, 10);
let y = make_inferred(DataType::Quantitative, 10);
let recs = recommend_charts(&x, Some(&y));
assert!(!recs.is_empty());
assert_eq!(recs[0].mark, Mark::Bar);
assert!(recs[0].confidence >= 0.85);
}
#[test]
fn test_warns_for_high_cardinality_bar() {
let x = make_inferred(DataType::Nominal, 150);
let y = make_inferred(DataType::Quantitative, 150);
let recs = recommend_charts(&x, Some(&y));
assert!(!recs.is_empty());
assert_eq!(recs[0].mark, Mark::Bar);
assert!(recs[0].confidence < 0.7);
assert!(recs[0].reason.contains("hard to read"));
}
#[test]
fn test_recommends_scatter_for_numeric_correlation() {
let x = make_inferred(DataType::Quantitative, 100);
let y = make_inferred(DataType::Quantitative, 100);
let recs = recommend_charts(&x, Some(&y));
assert!(!recs.is_empty());
assert_eq!(recs[0].mark, Mark::Point);
assert!(recs[0].confidence >= 0.85);
}
#[test]
fn test_recommends_pie_for_few_categories() {
let x = make_inferred(DataType::Nominal, 5);
let recs = recommend_charts(&x, None);
assert!(!recs.is_empty());
assert_eq!(recs[0].mark, Mark::Arc);
assert!(recs[0].reason.contains("part-to-whole"));
}
#[test]
fn test_no_pie_for_many_categories() {
let x = make_inferred(DataType::Nominal, 20);
let recs = recommend_charts(&x, None);
assert!(!recs.is_empty());
assert_eq!(recs[0].mark, Mark::Bar);
}
#[test]
fn test_recommendations_sorted_by_confidence() {
let x = make_inferred(DataType::Temporal, 30);
let y = make_inferred(DataType::Quantitative, 30);
let recs = recommend_charts(&x, Some(&y));
assert!(recs.len() >= 2);
for i in 1..recs.len() {
assert!(recs[i - 1].confidence >= recs[i].confidence);
}
}
#[test]
fn test_ordinal_data_gets_bar_recommendation() {
let x = make_inferred(DataType::Ordinal, 12);
let y = make_inferred(DataType::Quantitative, 12);
let recs = recommend_charts(&x, Some(&y));
assert!(!recs.is_empty());
assert_eq!(recs[0].mark, Mark::Bar);
assert!(recs[0].reason.contains("ordered categorical"));
}
#[test]
fn test_always_returns_at_least_one_recommendation() {
let x = make_inferred(DataType::Nominal, 0);
let y = make_inferred(DataType::Nominal, 0);
let recs = recommend_charts(&x, Some(&y));
assert!(!recs.is_empty());
assert_eq!(recs[0].mark, Mark::Bar);
}
}