satraox's picture
Update app.py
40f1cd4 verified
"""
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)