""" Hydro-Agro Dashboard v3.7 (Fixed Buttons & Text) FIXED: Aggregation now properly applies before analysis """ import gradio as gr import pandas as pd import numpy as np import os from datetime import datetime import plotly.graph_objects as go from plotly.subplots import make_subplots # Import components from components.data_loader import DataLoader from components.aggregation import AggregationEngine from components.window_selection import WindowSelector from components.map_view import create_map_panel # Import analysis modules from analysis.recharge import RechargeAnalyzer from analysis.qc_diagnostics import QCDiagnostics from analysis.agri_water_stress import AgriWaterStress from analysis.hydroclimate_intelligence import HydroclimateIntelligence # Import utilities from utils.preprocessing import DataPreprocessor from utils.export_handler import ExportHandler # Initialize components data_loader = DataLoader() preprocessor = DataPreprocessor() aggregator = AggregationEngine() window_selector = WindowSelector() export_handler = ExportHandler() # Initialize analysis engines recharge_analyzer = RechargeAnalyzer() qc_diagnostics = QCDiagnostics() agri_stress = AgriWaterStress() hydroclimate = HydroclimateIntelligence() # Global state class DashboardState: def __init__(self): self.df = None self.df_filtered = None self.df_aggregated = None self.current_analysis = None state = DashboardState() def load_and_initialize(file_obj, use_sample): """ Combined function: Loads data AND automatically runs the default analysis. """ try: # 1. Load Data if use_sample or file_obj is None: df = data_loader.load_sample_data() source_msg = "Sample Data (2025)" else: df = data_loader.load_uploaded_file(file_obj.name) source_msg = "Custom File Uploaded" # 2. Preprocess df = preprocessor.clean_data(df) df = preprocessor.add_derived_columns(df) # Update State state.df = df state.df_filtered = df state.df_aggregated = df # Initial aggregation is raw data # AUTO-DETECT DATE RANGE date_start = df['timestamp'].min().strftime('%Y-%m-%d') date_end = df['timestamp'].max().strftime('%Y-%m-%d') # 3. Auto-Run Hydroclimate Analysis with Daily aggregation results_text, fig, metrics_df, insights_text = run_analysis("Hydroclimate Overview", "Daily") # 4. Generate Detailed Status Info status_info = f""" ### ✅ {source_msg} | Metric | Details | | :--- | :--- | | **Records** | {len(df):,} | | **Range** | {df['timestamp'].min().date()} — {df['timestamp'].max().date()} | | **Sensors** | {len(df.columns)-1} Active | """ # Return all updates INCLUDING DATE UPDATES return ( status_info, # Update Status Panel fig, # Main plot gr.update(visible=True), # Show Filter group gr.update(visible=True), # Show Analysis group results_text, # Results text metrics_df, # Metrics table gr.update(visible=True), # Show metrics table visibility insights_text, # Policy insights text gr.update(value=date_start), # UPDATE START DATE gr.update(value=date_end) # UPDATE END DATE ) except Exception as e: error_msg = f"❌ Error: {str(e)}" return error_msg, None, gr.update(visible=False), gr.update(visible=False), "", None, gr.update(visible=False), "", gr.update(), gr.update() def create_visualization(df): """ Basic visualization for overview """ if df is None: return None fig = make_subplots( rows=3, cols=1, subplot_titles=('Soil Moisture', 'Temperature', 'Precipitation'), shared_xaxes=True, vertical_spacing=0.08, row_heights=[0.35, 0.35, 0.3] ) # Downsample if needed if len(df) > 5000: step = len(df) // 5000 df_lines = df.iloc[::step] else: df_lines = df # Soil Moisture if 'SM_5cm' in df.columns: fig.add_trace(go.Scatter(x=df_lines['timestamp'], y=df_lines['SM_5cm'], name='5cm', line=dict(color='green', width=1.5)), row=1, col=1) if 'SM_50cm' in df.columns: fig.add_trace(go.Scatter(x=df_lines['timestamp'], y=df_lines['SM_50cm'], name='50cm', line=dict(color='brown', width=1.5)), row=1, col=1) # Temperature if 'Temp_5cm' in df.columns: fig.add_trace(go.Scatter(x=df_lines['timestamp'], y=df_lines['Temp_5cm'], name='Temp', line=dict(color='red', width=1.5)), row=2, col=1) # Precipitation if 'Precipitation' in df.columns: if len(df) > 1000: precip = df.set_index('timestamp')['Precipitation'].resample('D').sum().reset_index() fig.add_trace(go.Bar(x=precip['timestamp'], y=precip['Precipitation'], name='Daily Precip', marker_color='blue', opacity=0.7), row=3, col=1) else: fig.add_trace(go.Bar(x=df['timestamp'], y=df['Precipitation'], name='Precip', marker_color='blue', opacity=0.7), row=3, col=1) fig.update_layout( margin=dict(l=20, r=20, t=40, b=20), height=500, template='plotly_white', hovermode='x unified', showlegend=True ) return fig def apply_filters(aggregation, start_date, end_date): """Update filters and refresh plot""" if state.df is None: return "⚠️ Load data first", None try: start = pd.to_datetime(start_date) end = pd.to_datetime(end_date) filtered = window_selector.apply_date_filter(state.df, (start, end)) aggregated = aggregator.aggregate(filtered, aggregation) state.df_filtered = filtered state.df_aggregated = aggregated fig = create_visualization(aggregated) info = f""" ### ✅ View Updated * **Resolution:** {aggregation} * **Records:** {len(aggregated):,} * **Range:** {start.date()} — {end.date()} """ return info, fig except Exception as e: return f"❌ Error: {str(e)}", None def run_analysis(analysis_type, aggregation_level): """ Run analysis module with proper aggregation applied FIXED: Now applies aggregation BEFORE running analysis """ if state.df_filtered is None: return "⚠️ Load data first", None, None, "" # CRITICAL FIX: Apply current aggregation setting before analysis df = aggregator.aggregate(state.df_filtered, aggregation_level) metrics_data = [] insights_text = "" try: if analysis_type == "Recharge Analysis": results = recharge_analyzer.analyze(df) fig = recharge_analyzer.create_recharge_plot(df) metrics_df = recharge_analyzer.get_metrics_dataframe(df) text = f"### Recharge Analysis ({aggregation_level})\nSee visualization and metrics." if hasattr(recharge_analyzer, 'get_policy_insights'): insights_text = recharge_analyzer.get_policy_insights(df) elif analysis_type == "Agricultural Stress": results = agri_stress.analyze(df) fig = agri_stress.create_stress_plot(df) metrics_df = agri_stress.get_metrics_dataframe(df) text = f"### Agricultural Stress Analysis ({aggregation_level})\nSee visualization and metrics." if hasattr(agri_stress, 'get_policy_insights'): insights_text = agri_stress.get_policy_insights(df) elif analysis_type == "Sensor QC": results = qc_diagnostics.analyze(df) fig = qc_diagnostics.create_diagnostic_plot(df) metrics_df = qc_diagnostics.get_metrics_dataframe(df) text = f"### Sensor QC Analysis ({aggregation_level})\nSee visualization and metrics." if hasattr(qc_diagnostics, 'get_policy_insights'): insights_text = qc_diagnostics.get_policy_insights(df) else: # Hydroclimate Overview results = hydroclimate.get_basic_stats(df) # Use enhanced plot if available if hasattr(hydroclimate, 'create_enhanced_plot'): fig = hydroclimate.create_enhanced_plot(df) else: fig = create_visualization(df) # Get enhanced metrics if hasattr(hydroclimate, 'get_metrics_dataframe'): metrics_df = hydroclimate.get_metrics_dataframe(df) else: # Fallback metrics if 'Temp_5cm' in df: metrics_data.append(["Avg Temp", f"{df['Temp_5cm'].mean():.1f}", "°C"]) metrics_data.append(["Max Temp", f"{df['Temp_5cm'].max():.1f}", "°C"]) if 'Precipitation' in df: metrics_data.append(["Total Rain", f"{df['Precipitation'].sum():.1f}", "mm"]) if 'SM_5cm' in df: metrics_data.append(["Avg Moisture", f"{df['SM_5cm'].mean():.1f}", "%"]) metrics_df = pd.DataFrame(metrics_data, columns=["Metric", "Value", "Unit"]) text = f"### Hydroclimate Overview ({aggregation_level})\nSee visualization and metrics." # Get policy insights if hasattr(hydroclimate, 'get_policy_insights'): insights_text = hydroclimate.get_policy_insights(df) state.current_analysis = {'type': analysis_type, 'results': results} return text, fig, metrics_df, insights_text except Exception as e: import traceback return f"❌ Analysis Error: {str(e)}", None, None, "" def export_data(fmt): """Export handler""" if state.df_aggregated is None: return "⚠️ No data", None try: atype = state.current_analysis['type'] if state.current_analysis else 'general' if fmt == "CSV": path = export_handler.export_csv(state.df_aggregated, atype) elif fmt == "Excel": path = export_handler.export_excel(state.df_aggregated, atype) else: path = export_handler.generate_report(state.df_aggregated, atype) return f"✅ Saved: {os.path.basename(path)}", path except Exception as e: return f"❌ Error: {e}", None # --- UI Layout --- def create_app(): # Create Map HTML initial_map_html = create_map_panel(None) # FIXED CSS - Better button visibility css = """ /* Mobile responsiveness */ @media (max-width: 768px) { .gr-button { min-height: 44px !important; font-size: 14px !important; } .map-container { height: 200px !important; } } /* Main Containers */ .map-container { border-radius: 12px; overflow: hidden; border: 1px solid #e5e7eb; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); } .load-container { border-radius: 12px; border: 1px solid #e5e7eb; padding: 20px; background: #f9fafb; height: 100%; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } /* Policy Insights Panel */ .insights-panel { background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); border-left: 4px solid #0284c7; padding: 15px; border-radius: 8px; color: #0f172a !important; /* FIX: readable text on mobile */ } /* FIX: Radio Buttons with Better Visibility */ .module-selector fieldset { gap: 10px; } .module-selector label { padding: 10px 14px !important; background: white !important; border: 2px solid #e5e7eb !important; border-radius: 8px !important; box-shadow: 0 1px 2px rgba(0,0,0,0.05) !important; transition: all 0.2s !important; color: #1f2937 !important; font-weight: 500 !important; } .module-selector label:hover { border-color: #3b82f6 !important; background: #eff6ff !important; } /* FIX: Selected state with dark background and white text */ .module-selector label:has(input:checked) { background: #2563eb !important; border-color: #1d4ed8 !important; color: white !important; font-weight: 600 !important; } /* Action Buttons */ .update-btn { background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important; color: white !important; border: none !important; font-weight: 600 !important; } .download-btn { background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%) !important; color: white !important; border: none !important; font-weight: 600 !important; } /* Dropdowns */ .filter-dropdown .wrap-inner { border: 1px solid #9ca3af !important; border-radius: 6px !important; } /* HF Mobile wraps everything in .dark, so we override that theme only */ .dark .insights-panel, .dark .insights-panel .markdown-body, .dark .insights-panel * { color: #0a0a0a !important; /* Force dark readable text */ fill: #0a0a0a !important; /* Fix icon / emoji fill if SVG */ } /* Also ensure background stays light even in dark mode */ .dark .insights-panel { background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%) !important; border-left: 4px solid #0284c7 !important; } /* Fix headings and strong text */ .dark .insights-panel h1, .dark .insights-panel h2, .dark .insights-panel h3, .dark .insights-panel strong { color: #000 !important; } """ with gr.Blocks(title="Hydro-Agro Dashboard", theme=gr.themes.Soft(primary_hue="blue"), css=css) as app: # Header with gr.Row(): gr.Markdown("## 🌊 Hydro-Agro Intelligence Dashboard") # Context Row: Map + Load with gr.Row(equal_height=True): with gr.Column(scale=1): gr.HTML( value=f'