Spaces:
Running
Running
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
|