Datavision / backend /agents /chart_gatekeeper.py
DataVision CI/CD Bot
release: clean production build for HuggingFace Space
09801ca
Raw
History Blame Contribute Delete
6.82 kB
# 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