File size: 6,818 Bytes
09801ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# Chart Gatekeeping - Strict validation before chart generation
"""
Ensures charts are only generated when:
1. Sufficient data exists
2. Chart type matches data
3. Data quality is acceptable
"""

from typing import Dict, Any, Optional, Tuple
import pandas as pd


# ============================================================================
# CHART REQUIREMENTS
# ============================================================================

CHART_REQUIREMENTS = {
    "bar": {
        "min_data_points": 2,
        "max_data_points": 50,  # Too many bars = unreadable
        "required_columns": ["name", "value"],
        "description": "Bar chart for comparing categories"
    },
    "line": {
        "min_data_points": 3,  # Need trend
        "max_data_points": 365,
        "required_columns": ["x", "y"],
        "description": "Line chart for trends over time"
    },
    "pie": {
        "min_data_points": 2,
        "max_data_points": 8,  # More than 8 slices = confusing
        "required_columns": ["label", "value"],
        "description": "Pie chart for proportions"
    },
    "scatter": {
        "min_data_points": 5,
        "max_data_points": 1000,
        "required_columns": ["x", "y"],
        "description": "Scatter plot for correlations"
    },
    "prediction": {
        "min_data_points": 5,  # Need history for forecast
        "max_data_points": 365,
        "required_columns": ["date", "value"],
        "description": "Forecast chart with predictions"
    },
}


# ============================================================================
# GATEKEEPING FUNCTIONS
# ============================================================================

def should_render_chart(
    df: Optional[pd.DataFrame],
    chart_type: str,
    query_type: str = None
) -> Tuple[bool, str]:
    """
    Strict gatekeeping: Should we render this chart?
    
    Returns:
        Tuple[bool, str]: (should_render, reason)
    """
    
    # No data = no chart
    if df is None:
        return False, "No data available"
    
    if df.empty:
        return False, "Dataset is empty"
    
    # Get requirements for chart type
    requirements = CHART_REQUIREMENTS.get(chart_type.lower())
    if not requirements:
        return False, f"Unknown chart type: {chart_type}"
    
    # Check minimum data points
    data_points = len(df)
    if data_points < requirements["min_data_points"]:
        return False, f"Insufficient data: {data_points} points (need {requirements['min_data_points']} minimum)"
    
    # Check maximum data points
    if data_points > requirements["max_data_points"]:
        return False, f"Too many data points: {data_points} (max {requirements['max_data_points']} for readability)"
    
    # Check for excessive null values (>30% = unreliable)
    null_ratio = df.isnull().sum().sum() / (len(df) * len(df.columns))
    if null_ratio > 0.3:
        return False, f"Data quality issue: {null_ratio:.0%} null values"
    
    return True, "Chart approved"


def validate_chart_data(
    df: pd.DataFrame,
    chart_type: str
) -> Tuple[bool, str, Optional[pd.DataFrame]]:
    """
    Validate and clean data for charting.
    
    Returns:
        Tuple[bool, str, DataFrame]: (is_valid, message, cleaned_df)
    """
    
    if df is None or df.empty:
        return False, "No data to validate", None
    
    # Make a copy to avoid modifying original
    clean_df = df.copy()
    
    # Remove rows with all nulls
    clean_df = clean_df.dropna(how='all')
    
    # Check again after cleaning
    if clean_df.empty:
        return False, "All data rows were empty", None
    
    # For numeric charts, ensure we have numeric data
    numeric_types = ["bar", "line", "pie", "scatter", "prediction"]
    if chart_type.lower() in numeric_types:
        # Find numeric columns
        numeric_cols = clean_df.select_dtypes(include=['number']).columns
        if len(numeric_cols) == 0:
            return False, "No numeric data for chart", None
    
    return True, "Data validated", clean_df


def get_chart_decision(
    df: Optional[pd.DataFrame],
    chart_type: str,
    query_type: str = None,
    force: bool = False
) -> Dict[str, Any]:
    """
    Make a chart rendering decision with full reasoning.
    
    Returns:
        Dict with keys: should_render, reason, warnings, chart_type, data_points
    """
    
    decision = {
        "should_render": False,
        "reason": "",
        "warnings": [],
        "chart_type": chart_type,
        "data_points": 0
    }
    
    if df is not None and not df.empty:
        decision["data_points"] = len(df)
    
    # Check gatekeeping
    can_render, reason = should_render_chart(df, chart_type, query_type)
    
    if not can_render and not force:
        decision["reason"] = reason
        return decision
    
    # Validate data
    if df is not None:
        is_valid, message, _ = validate_chart_data(df, chart_type)
        if not is_valid and not force:
            decision["reason"] = message
            return decision
    
    # Approved
    decision["should_render"] = True
    decision["reason"] = "Chart approved"
    
    # Add warnings if applicable
    if decision["data_points"] < 5:
        decision["warnings"].append("Limited data points - interpretation may be less reliable")
    
    return decision


# ============================================================================
# QUERY-BASED CHART SELECTION
# ============================================================================

def suggest_chart_type(query: str, df: Optional[pd.DataFrame]) -> Optional[str]:
    """
    Suggest appropriate chart type based on query and data.
    Returns None if no chart is appropriate.
    """
    query_lower = query.lower()
    
    # Trend/time queries β†’ line chart
    if any(word in query_lower for word in ["trend", "over time", "monthly", "weekly", "daily", "growth"]):
        return "line"
    
    # Comparison queries β†’ bar chart
    if any(word in query_lower for word in ["compare", "top", "bottom", "ranking", "best", "worst"]):
        return "bar"
    
    # Distribution queries β†’ pie chart
    if any(word in query_lower for word in ["breakdown", "distribution", "percentage", "share", "proportion"]):
        if df is not None and len(df) <= 8:  # Pie only for small datasets
            return "pie"
        return "bar"  # Fall back to bar for larger datasets
    
    # Prediction queries β†’ prediction chart
    if any(word in query_lower for word in ["predict", "forecast", "future", "next"]):
        return "prediction"
    
    # Correlation queries β†’ scatter
    if any(word in query_lower for word in ["correlation", "relationship", "vs", "versus"]):
        return "scatter"
    
    # Default: no chart if not clearly needed
    return None