Spaces:
Sleeping
Sleeping
| """ | |
| 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'<div class="map-container" style="height: 280px;">{initial_map_html}</div>', | |
| label="Watershed Location" | |
| ) | |
| with gr.Column(scale=1): | |
| with gr.Group(elem_classes="load-container"): | |
| gr.Markdown("### π Data Source") | |
| with gr.Row(): | |
| use_sample = gr.Checkbox(label="Use Sample Data (2025)", value=True) | |
| file_input = gr.File(label="Upload CSV", file_types=[".csv"], height=80) | |
| load_btn = gr.Button("βΆ Load & Analyze Data", variant="primary", size="lg") | |
| load_status = gr.Markdown("System Ready. Please load data.") | |
| gr.Markdown("---") | |
| # Main Workspace | |
| with gr.Row(): | |
| # Sidebar: Controls | |
| with gr.Column(scale=1, min_width=280): | |
| # Filters Panel | |
| with gr.Group(visible=False) as filter_group: | |
| gr.Markdown("### π Time Filters") | |
| aggregation = gr.Dropdown( | |
| label="Time Step", | |
| choices=["15min", "Hourly", "Daily", "Weekly", "Monthly"], | |
| value="Daily", | |
| elem_classes="filter-dropdown" | |
| ) | |
| with gr.Row(): | |
| start_date = gr.Textbox(label="Start", value="2025-01-01") | |
| end_date = gr.Textbox(label="End", value="2025-06-30") | |
| apply_btn = gr.Button("β» Update View", elem_classes="update-btn") | |
| # Analysis Panel | |
| with gr.Group(visible=False) as analysis_group: | |
| gr.Markdown("### π§ Analysis Modules") | |
| analysis_type = gr.Radio( | |
| choices=["Hydroclimate Overview", "Recharge Analysis", "Agricultural Stress", "Sensor QC"], | |
| value="Hydroclimate Overview", | |
| label="Select Module", | |
| elem_classes="module-selector" | |
| ) | |
| analyze_btn = gr.Button("π Run Analysis", variant="primary") | |
| gr.Markdown("### π₯ Export Reports") | |
| export_fmt = gr.Radio(["CSV", "Excel", "Report"], value="CSV", label="Format") | |
| export_btn = gr.Button("πΎ Download Results", elem_classes="download-btn") | |
| export_file = gr.File(visible=False) | |
| # Main Visualization Area | |
| with gr.Column(scale=3): | |
| main_plot = gr.Plot(label="Data Visualization") | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| results_text = gr.Markdown("") | |
| metrics_table = gr.Dataframe(label="Key Indicators", visible=False) | |
| with gr.Column(scale=1): | |
| # Policy Insights Panel | |
| insights_panel = gr.Markdown( | |
| "", | |
| elem_classes="insights-panel" | |
| ) | |
| # Event Wiring - FIXED: Pass aggregation to run_analysis | |
| load_btn.click( | |
| fn=load_and_initialize, | |
| inputs=[file_input, use_sample], | |
| outputs=[load_status, main_plot, filter_group, analysis_group, | |
| results_text, metrics_table, metrics_table, insights_panel, | |
| start_date, end_date] | |
| ) | |
| apply_btn.click( | |
| fn=apply_filters, | |
| inputs=[aggregation, start_date, end_date], | |
| outputs=[load_status, main_plot] | |
| ) | |
| # AUTO-UPDATE: When aggregation dropdown changes, automatically update the view | |
| aggregation.change( | |
| fn=apply_filters, | |
| inputs=[aggregation, start_date, end_date], | |
| outputs=[load_status, main_plot] | |
| ) | |
| # CRITICAL FIX: Pass aggregation as input to run_analysis | |
| analyze_btn.click( | |
| fn=run_analysis, | |
| inputs=[analysis_type, aggregation], # β ADDED aggregation input | |
| outputs=[results_text, main_plot, metrics_table, insights_panel] | |
| ).then(lambda: gr.update(visible=True), outputs=[metrics_table]) | |
| export_btn.click( | |
| fn=export_data, | |
| inputs=[export_fmt], | |
| outputs=[load_status, export_file] | |
| ).then(lambda x: gr.update(visible=True, value=x) if x else gr.update(visible=False), | |
| inputs=[export_file], outputs=[export_file]) | |
| return app | |
| if __name__ == "__main__": | |
| app = create_app() | |
| app.launch(server_name="0.0.0.0", server_port=7860, share=False, show_error=True) |