Spaces:
Sleeping
Sleeping
| import dash | |
| from dash import dcc, html, Input, Output, State, callback_context, dash_table | |
| from dash.dependencies import ALL, MATCH | |
| import dash_bootstrap_components as dbc | |
| import pandas as pd | |
| from chatbot_backend import GroqRAGChatbot | |
| import json | |
| import folium | |
| from folium import plugins | |
| import geopandas as gpd | |
| from shapely import wkt | |
| import base64 | |
| from io import StringIO | |
| import os | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| from flask import Flask | |
| from dash.exceptions import PreventUpdate | |
| from datetime import datetime | |
| # Initialize Flask server and Dash app | |
| server = Flask(__name__) | |
| # Serve static files (images) | |
| def serve_image(filename): | |
| return server.send_static_file(f'Assests/{filename}') | |
| app = dash.Dash( | |
| __name__, | |
| external_stylesheets=[dbc.themes.MORPH], # Changed to a more modern theme | |
| server=server, | |
| meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1.0"}] | |
| ) | |
| app.title = "India Groundwater AI Chatbot" | |
| # Function to encode image to base64 | |
| def encode_image(image_path): | |
| if os.path.exists(image_path): | |
| with open(image_path, "rb") as image_file: | |
| encoded_string = base64.b64encode(image_file.read()).decode() | |
| return f"data:image/png;base64,{encoded_string}" | |
| return None | |
| # Encode the water droplet image | |
| water_droplet_image = encode_image("Assests/image.png") | |
| # Custom CSS for additional styling | |
| app.index_string = ''' | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| {%metas%} | |
| <title>{%title%}</title> | |
| {%favicon%} | |
| {%css%} | |
| <style> | |
| :root { | |
| --primary: #2E86AB; | |
| --secondary: #A23B72; | |
| --accent: #F18F01; | |
| --light: #F7F7FF; | |
| --dark: #2B2D42; | |
| --success: #4CB944; | |
| --warning: #F18F01; | |
| --danger: #E71D36; | |
| --bg: #f5f7fb; | |
| --text: #111827; | |
| } | |
| /* Dark theme overrides */ | |
| #app-root.dark-mode { | |
| --primary: #60a5fa; | |
| --secondary: #f472b6; | |
| --accent: #f59e0b; | |
| --light: #111827; | |
| --dark: #e5e7eb; | |
| --success: #34d399; | |
| --warning: #f59e0b; | |
| --danger: #ef4444; | |
| --bg: #0b1220; | |
| --text: #e5e7eb; | |
| } | |
| /* Page background and default text */ | |
| #app-root { background-color: var(--bg); color: var(--text); } | |
| #app-root > .container-fluid { min-height: 100vh; } | |
| .gradient-bg { | |
| background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); | |
| } | |
| .card-hover { | |
| transition: transform 0.3s ease, box-shadow 0.3s ease; | |
| } | |
| .card-hover:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 10px 20px rgba(0,0,0,0.1); | |
| } | |
| .feature-icon { | |
| font-size: 2.5rem; | |
| margin-bottom: 1rem; | |
| color: var(--primary); | |
| } | |
| .stat-number { | |
| font-size: 2.5rem; | |
| font-weight: 700; | |
| color: var(--primary); | |
| } | |
| .stat-label { | |
| font-size: 0.9rem; | |
| color: var(--dark); | |
| opacity: 0.8; | |
| } | |
| /* Water droplet animations */ | |
| .water-droplet { | |
| animation: float 3s ease-in-out infinite, pulse 2s ease-in-out infinite; | |
| width: 250px; | |
| height: 250px; | |
| object-fit: cover; | |
| border-radius: 50%; | |
| filter: drop-shadow(0 8px 16px rgba(0,0,0,0.3)); | |
| border: 4px solid rgba(255, 255, 255, 0.3); | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0px) rotate(0deg); } | |
| 50% { transform: translateY(-15px) rotate(3deg); } | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { transform: scale(1); } | |
| 50% { transform: scale(1.08); } | |
| } | |
| .water-droplet-container { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100%; | |
| min-height: 300px; | |
| padding: 1rem; | |
| } | |
| .chat-bubble-user { | |
| background-color: var(--primary); | |
| color: white; | |
| border-radius: 18px 18px 0 18px; | |
| padding: 12px 16px; | |
| margin: 5px 0; | |
| max-width: 80%; | |
| margin-left: auto; | |
| } | |
| .chat-bubble-bot { | |
| background-color: #eef2ff; | |
| color: #111827; | |
| border-radius: 18px 18px 18px 0; | |
| padding: 12px 16px; | |
| margin: 5px 0; | |
| max-width: 80%; | |
| margin-right: auto; | |
| } | |
| /* Modern chat layout */ | |
| .chat-container { display: flex; flex-direction: column; height: 65vh; } | |
| .chat-messages { flex: 1; overflow-y: auto; padding: 12px; background: var(--light); border-radius: 8px; border: 1px solid #e5e7eb; } | |
| .message-row { display: flex; gap: 10px; margin: 8px 0; align-items: flex-end; } | |
| .message-row.user { flex-direction: row-reverse; } | |
| .avatar { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: #dbeafe; color: #1d4ed8; font-weight: 700; flex: 0 0 36px; } | |
| .avatar.bot { background: #fef3c7; color: #92400e; } | |
| .bubble { padding: 10px 14px; border-radius: 14px; max-width: 75%; box-shadow: 0 1px 2px rgba(0,0,0,0.05); } | |
| .bubble.user { background: var(--primary); color: white; border-top-right-radius: 4px; } | |
| .bubble.bot { background: #ffffff; color: #111827; border-top-left-radius: 4px; } | |
| .timestamp { font-size: 10px; opacity: 0.7; margin-top: 4px; } | |
| .input-area { margin-top: 12px; } | |
| #app-root.dark-mode .chat-messages { background: #0f172a; border-color: #1f2937; } | |
| #app-root.dark-mode .bubble.bot { background: #0b1220; color: var(--text); } | |
| .navbar-brand { | |
| font-weight: 700; | |
| font-size: 1.5rem; | |
| } | |
| .section-title { | |
| position: relative; | |
| padding-bottom: 15px; | |
| margin-bottom: 25px; | |
| font-weight: 700; | |
| } | |
| .section-title:after { | |
| content: ''; | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| width: 50px; | |
| height: 3px; | |
| background: var(--primary); | |
| } | |
| .btn-primary { | |
| background-color: var(--primary); | |
| border-color: var(--primary); | |
| } | |
| .btn-primary:hover { | |
| background-color: #1D6A8F; | |
| border-color: #1D6A8F; | |
| } | |
| .btn-accent { | |
| background-color: var(--accent); | |
| border-color: var(--accent); | |
| color: white; | |
| } | |
| .btn-accent:hover { | |
| background-color: #D97F00; | |
| border-color: #D97F00; | |
| color: white; | |
| } | |
| /* General readability and overflow safety */ | |
| body, #app-root, .card, .navbar, .nav-link, .tab-content, .tab-pane, .card-text, .card-title { | |
| word-wrap: break-word; | |
| overflow-wrap: anywhere; | |
| } | |
| /* Light theme component backgrounds */ | |
| .card { background-color: #ffffff; } | |
| .bg-light { background-color: #f1f5f9 !important; } | |
| .text-muted { color: #6b7280 !important; } | |
| /* Dark mode component overrides */ | |
| #app-root.dark-mode .navbar, #app-root.dark-mode .navbar-light { background-color: #111827 !important; } | |
| #app-root.dark-mode .navbar-brand, #app-root.dark-mode .nav-link { color: var(--text) !important; } | |
| #app-root.dark-mode .card { background-color: #111827; color: var(--text); border-color: #1f2937; } | |
| #app-root.dark-mode .bg-light { background-color: #0f172a !important; color: var(--text) !important; } | |
| #app-root.dark-mode .text-muted { color: #9ca3af !important; } | |
| #app-root.dark-mode .dropdown-menu { background-color: #0f172a; color: var(--text); } | |
| #app-root.dark-mode .form-control, | |
| #app-root.dark-mode .dbc-input, | |
| #app-root.dark-mode .Select-control, | |
| #app-root.dark-mode .Select-menu-outer { background-color: #0f172a; color: var(--text); } | |
| #app-root.dark-mode .chat-bubble-bot { background-color: #0f172a; color: var(--text); } | |
| #app-root.dark-mode .chat-bubble-user { background-color: var(--primary); color: #0b1220; } | |
| #app-root.dark-mode .tab-content, #app-root.dark-mode .tab-pane { background-color: transparent; } | |
| #app-root.dark-mode .table { color: var(--text); } | |
| /* Chat history container */ | |
| #chat-history { background-color: #f8f9fa; } | |
| #app-root.dark-mode #chat-history { background-color: #0b1220; border-color: #1f2937; } | |
| </style> | |
| </head> | |
| <body> | |
| {%app_entry%} | |
| <footer> | |
| {%config%} | |
| {%scripts%} | |
| {%renderer%} | |
| </footer> | |
| </body> | |
| </html> | |
| ''' | |
| try: | |
| chatbot = GroqRAGChatbot() | |
| CHATBOT_READY = True | |
| except Exception as e: | |
| print(f"Chatbot initialization error: {e}") | |
| CHATBOT_READY = False | |
| # Default placeholder figure for Forecasts tab so it never renders blank | |
| def build_placeholder_forecast_figure(title_suffix="No time-series data"): | |
| try: | |
| x_vals = list(range(1, 11)) | |
| y_vals = list(range(1, 11)) | |
| fig = go.Figure() | |
| fig.add_trace(go.Scatter( | |
| x=x_vals, | |
| y=y_vals, | |
| mode='lines+markers', | |
| name='Placeholder', | |
| marker=dict(color="#2563eb"), | |
| line=dict(color="#2563eb") | |
| )) | |
| fig.update_layout( | |
| title=f"Forecast Preview ({title_suffix})", | |
| height=360, | |
| margin=dict(l=10, r=10, t=50, b=10), | |
| plot_bgcolor="#ffffff", | |
| paper_bgcolor="#ffffff" | |
| ) | |
| return fig | |
| except Exception: | |
| return go.Figure() | |
| # App Layout | |
| app.layout = html.Div([ | |
| # Top Navbar - Modern Design | |
| dbc.Navbar( | |
| dbc.Container([ | |
| html.A( | |
| dbc.Row([ | |
| dbc.Col(html.Span("💧", style={"fontSize": "28px"})), | |
| dbc.Col(dbc.NavbarBrand("India Groundwater AI", className="ms-2")), | |
| ], align="center", className="g-0"), | |
| href="#", | |
| style={"textDecoration": "none"} | |
| ), | |
| dbc.NavbarToggler(id="navbar-toggler"), | |
| dbc.Collapse( | |
| dbc.Nav([], className="ms-auto", navbar=True), | |
| id="navbar-collapse", | |
| navbar=True, | |
| ), | |
| dbc.Switch(id="theme-switch", value=False, label="Dark", className="ms-3"), | |
| ], fluid=True), | |
| color="light", | |
| dark=False, | |
| sticky="top", | |
| className="mb-4 shadow-sm" | |
| ), | |
| # Main Content | |
| html.Div([ | |
| # Top-level Tabs | |
| dcc.Tabs(id="main-tabs", value="tab-home", children=[ | |
| # Landing Page - Completely Redesigned | |
| dcc.Tab(label="Home", value="tab-home", children=[ | |
| dbc.Container(fluid=True, children=[ | |
| # Hero Section | |
| dbc.Row([ | |
| dbc.Col([ | |
| html.Div([ | |
| html.Img(src=water_droplet_image if water_droplet_image else "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZHJvcGxldCIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojZmZmZmZmO3N0b3Atb3BhY2l0eToxIiAvPgogICAgICA8c3RvcCBvZmZzZXQ9IjcwJSIgc3R5bGU9InN0b3AtY29sb3I6IzAwYjNjYztzdG9wLW9wYWNpdHk6MSIgLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgPC9kZWZzPgogIDxwYXRoIGQ9Ik0xNTAgMTVMMTgwIDYwTDE1MCA5MEwxMjAgNjBaIiBmaWxsPSJ1cmwoI2Ryb3BsZXQpIi8+CiAgPHRleHQgeD0iMTUwIiB5PSIxMzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IndoaXRlIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMjAiPklOR1JFUzwvdGV4dD4KPC9zdmc+", | |
| className="water-droplet", | |
| alt="Water Droplet Icon") | |
| ], className="water-droplet-container") | |
| ], md=5, lg=5, className="d-flex align-items-center justify-content-center"), | |
| dbc.Col([ | |
| html.Div([ | |
| html.H1("India Groundwater Intelligence Platform", className="display-4 fw-bold mb-4"), | |
| html.P("Advanced AI-powered insights for sustainable groundwater management across India", | |
| className="lead mb-4 text-black"), | |
| dbc.Button("Get Started", id="hero-cta", color="primary", size="lg", | |
| className="me-2 text-white", href="#tab-explore"), | |
| dbc.Button("Live Demo", id="hero-demo", color="black", size="lg", | |
| outline=True, href="#tab-chat"), | |
| ], className="p-5 rounded-3", | |
| style={"backgroundColor": "rgba(255,255,255,0.9)"}) | |
| ], md=7, lg=7) | |
| ], className="gradient-bg py-5 mb-5 text-white align-items-center", | |
| style={"borderRadius": "0 0 30px 30px", "minHeight": "500px"}), | |
| # Stats Section | |
| dbc.Row([ | |
| dbc.Col([ | |
| html.Div([ | |
| html.Div("🌍", className="feature-icon"), | |
| html.Div(id="total-districts", children="--", className="stat-number"), | |
| html.Div("Districts Covered", className="stat-label") | |
| ], className="text-center p-4 bg-white shadow-sm rounded-3 card-hover") | |
| ], md=3, className="mb-3"), | |
| dbc.Col([ | |
| html.Div([ | |
| html.Div("💧", className="feature-icon"), | |
| html.Div(id="avg-development", children="--", className="stat-number"), | |
| html.Div("Avg Development %", className="stat-label") | |
| ], className="text-center p-4 bg-white shadow-sm rounded-3 card-hover") | |
| ], md=3, className="mb-3"), | |
| dbc.Col([ | |
| html.Div([ | |
| html.Div("⚠️", className="feature-icon"), | |
| html.Div(id="over-exploited", children="--", className="stat-number"), | |
| html.Div("Over-exploited Areas", className="stat-label") | |
| ], className="text-center p-4 bg-white shadow-sm rounded-3 card-hover") | |
| ], md=3, className="mb-3"), | |
| dbc.Col([ | |
| html.Div([ | |
| html.Div("🔍", className="feature-icon"), | |
| html.Div(id="critical-districts", children="--", className="stat-number"), | |
| html.Div("Critical Status", className="stat-label") | |
| ], className="text-center p-4 bg-white shadow-sm rounded-3 card-hover") | |
| ], md=3, className="mb-3"), | |
| ], className="mb-5"), | |
| # Features Section | |
| dbc.Row([ | |
| dbc.Col([ | |
| html.H2("Key Features", className="section-title") | |
| ], width=12) | |
| ], className="mb-4"), | |
| dbc.Row([ | |
| dbc.Col([ | |
| dbc.Card([ | |
| dbc.CardBody([ | |
| html.Div("🗺️", style={"fontSize": "3rem", "textAlign": "center", "marginBottom": "1rem"}), | |
| html.H4("Interactive Maps", className="card-title"), | |
| html.P("Explore groundwater data through interactive visualizations with detailed district-level information.", | |
| className="card-text text-black") | |
| ]) | |
| ], className="h-100 card-hover border-0 shadow-sm") | |
| ], md=4, className="mb-4"), | |
| dbc.Col([ | |
| dbc.Card([ | |
| dbc.CardBody([ | |
| html.Div("🤖", style={"fontSize": "3rem", "textAlign": "center", "marginBottom": "1rem"}), | |
| html.H4("AI-Powered Insights", className="card-title"), | |
| html.P("Get answers to complex groundwater questions using our advanced natural language processing capabilities.", | |
| className="card-text") | |
| ]) | |
| ], className="h-100 card-hover border-0 shadow-sm") | |
| ], md=4, className="mb-4"), | |
| dbc.Col([ | |
| dbc.Card([ | |
| dbc.CardBody([ | |
| html.Div("📊", style={"fontSize": "3rem", "textAlign": "center", "marginBottom": "1rem"}), | |
| html.H4("Advanced Analytics", className="card-title"), | |
| html.P("Dive deep into trends, forecasts, and comprehensive reports on groundwater availability and usage patterns.", | |
| className="card-text") | |
| ]) | |
| ], className="h-100 card-hover border-0 shadow-sm") | |
| ], md=4, className="mb-4"), | |
| ], className="mb-5"), | |
| # How It Works Section | |
| dbc.Row([ | |
| dbc.Col([ | |
| html.H2("How It Works", className="section-title") | |
| ], width=12) | |
| ], className="mb-4"), | |
| dbc.Row([ | |
| dbc.Col([ | |
| dbc.Card([ | |
| dbc.CardBody([ | |
| html.Div("1", style={ | |
| "width": "40px", | |
| "height": "40px", | |
| "backgroundColor": "var(--primary)", | |
| "color": "white", | |
| "borderRadius": "50%", | |
| "display": "flex", | |
| "alignItems": "center", | |
| "justifyContent": "center", | |
| "marginBottom": "1rem", | |
| "fontWeight": "bold" | |
| }), | |
| html.H4("Ask a Question", className="card-title"), | |
| html.P("Type your question about groundwater data in natural language - no technical knowledge required.", | |
| className="card-text") | |
| ]) | |
| ], className="h-100 card-hover border-0 shadow-sm") | |
| ], md=4, className="mb-4"), | |
| dbc.Col([ | |
| dbc.Card([ | |
| dbc.CardBody([ | |
| html.Div("2", style={ | |
| "width": "40px", | |
| "height": "40px", | |
| "backgroundColor": "var(--primary)", | |
| "color": "white", | |
| "borderRadius": "50%", | |
| "display": "flex", | |
| "alignItems": "center", | |
| "justifyContent": "center", | |
| "marginBottom": "1rem", | |
| "fontWeight": "bold" | |
| }), | |
| html.H4("Get AI Analysis", className="card-title"), | |
| html.P("Our AI processes your query, analyzes the groundwater database, and extracts relevant insights.", | |
| className="card-text") | |
| ]) | |
| ], className="h-100 card-hover border-0 shadow-sm") | |
| ], md=4, className="mb-4"), | |
| dbc.Col([ | |
| dbc.Card([ | |
| dbc.CardBody([ | |
| html.Div("3", style={ | |
| "width": "40px", | |
| "height": "40px", | |
| "backgroundColor": "var(--primary)", | |
| "color": "white", | |
| "borderRadius": "50%", | |
| "display": "flex", | |
| "alignItems": "center", | |
| "justifyContent": "center", | |
| "marginBottom": "1rem", | |
| "fontWeight": "bold" | |
| }), | |
| html.H4("Explore Results", className="card-title"), | |
| html.P("View interactive maps, visualizations, and detailed reports based on the AI's findings.", | |
| className="card-text") | |
| ]) | |
| ], className="h-100 card-hover border-0 shadow-sm") | |
| ], md=4, className="mb-4"), | |
| ], className="mb-5"), | |
| # Call to Action | |
| dbc.Row([ | |
| dbc.Col([ | |
| dbc.Card([ | |
| dbc.CardBody([ | |
| html.H3("Ready to explore India's groundwater data?", className="text-center mb-4"), | |
| html.Div([ | |
| dbc.Button("Start Exploring", color="primary", size="lg", className="me-3 text-white bg-blue", href="#tab-explore"), | |
| dbc.Button("Chat with AI", color="", size="lg", outline=True, className="text-gray-600", href="#tab-chat"), | |
| ], className="d-flex justify-content-center") | |
| ]) | |
| ], className="border-0 shadow-sm bg-blue") | |
| ], width=12) | |
| ]), | |
| # Footer | |
| dbc.Row([ | |
| dbc.Col([ | |
| html.Hr(), | |
| html.P("India Groundwater AI Platform - Powered by Advanced Analytics and AI", | |
| className="text-center text-muted mt-4") | |
| ], width=12) | |
| ], className="mt-5") | |
| ]) | |
| ]), | |
| # Explore Page (Map, Viz, Results, Details) | |
| dcc.Tab(label="Explore", value="tab-explore", children=[ | |
| dbc.Container(fluid=True, children=[ | |
| dbc.Row([ | |
| # Left Sidebar (Quick Stats) | |
| dbc.Col([ | |
| dbc.Card([ | |
| dbc.CardHeader(html.H5("📊 Quick Stats", className="mb-0")), | |
| dbc.CardBody([ | |
| dbc.Row([ | |
| dbc.Col([ | |
| html.Div([ | |
| html.Div("Total Districts", className="small text-muted"), | |
| html.H3(id="total-districts-explore", children="--", className="mb-0 text-primary") | |
| ], className="p-3 bg-light rounded-3") | |
| ], width=6, className="mb-3"), | |
| dbc.Col([ | |
| html.Div([ | |
| html.Div("Avg Development %", className="small text-muted"), | |
| html.H3(id="avg-development-explore", children="--", className="mb-0 text-warning") | |
| ], className="p-3 bg-light rounded-3") | |
| ], width=6, className="mb-3") | |
| ]), | |
| html.Hr(className="my-3"), | |
| dbc.Row([ | |
| dbc.Col([ | |
| html.Div([ | |
| html.Div("Over-exploited", className="small text-muted"), | |
| html.H4(id="over-exploited-explore", children="--", className="mb-0 text-danger") | |
| ], className="p-3 bg-light rounded-3") | |
| ], width=6, className="mb-3"), | |
| dbc.Col([ | |
| html.Div([ | |
| html.Div("Critical Status", className="small text-muted"), | |
| html.H4(id="critical-districts-explore", children="--", className="mb-0 text-warning") | |
| ], className="p-3 bg-light rounded-3") | |
| ], width=6, className="mb-3") | |
| ]) | |
| ]) | |
| ], className="mb-4 shadow-sm"), | |
| ], width=3, style={"position": "sticky", "top": "20px", "height": "calc(100vh - 120px)", "overflowY": "auto"}), | |
| # Right Content Tabs | |
| dbc.Col([ | |
| dbc.Tabs([ | |
| dbc.Tab(label="Map", tab_id="tab-map", children=[ | |
| dbc.Card([ | |
| dbc.CardHeader([ | |
| html.H5("🗺️ Underground Water Coverage Map", className="mb-0"), | |
| dbc.Badge(id="map-status", children="No Data", color="secondary", className="float-end") | |
| ]), | |
| dbc.CardBody([ | |
| dcc.Loading( | |
| id="map-loading", | |
| children=[ | |
| html.Div( | |
| id="groundwater-map", | |
| style={"height": "500px", "width": "100%", "borderRadius": "8px", "overflow": "hidden"} | |
| ) | |
| ], | |
| type="circle" | |
| ), | |
| html.Div([ | |
| dbc.Button("Download Map (HTML)", id="download-map-html-btn", color="primary", size="sm", className="mt-3"), | |
| dcc.Download(id="download-map-html") | |
| ], className="mt-2") | |
| ]) | |
| ], className="mb-4 shadow-sm") | |
| ]), | |
| dbc.Tab(label="Visualization", tab_id="tab-viz", children=[ | |
| dbc.Card([ | |
| dbc.CardHeader(html.H5("📈 Data Visualization", className="mb-0")), | |
| dbc.CardBody([ | |
| dcc.Loading( | |
| id="viz-loading", | |
| type="circle", | |
| children=[ | |
| dcc.Graph(id="viz-graph", figure=go.Figure(), | |
| config={"displaylogo": False, "toImageButtonOptions": {"format": "png", "filename": "groundwater_visualization"}}), | |
| html.Hr(), | |
| dcc.Graph(id="viz-graph-2", figure=go.Figure(), | |
| config={"displaylogo": False, "toImageButtonOptions": {"format": "png", "filename": "groundwater_visualization_2"}}) | |
| ] | |
| ), | |
| html.Div([ | |
| dbc.Button("Download CSV", id="download-csv-btn", color="primary", size="sm", className="me-2"), | |
| dcc.Download(id="download-csv"), | |
| dbc.Button("Download PNG", id="download-png-btn", color="primary", size="sm") | |
| ], className="mt-3") | |
| ]) | |
| ], className="mb-4 shadow-sm") | |
| ]), | |
| dbc.Tab(label="Results", tab_id="tab-results", children=[ | |
| dbc.Card([ | |
| dbc.CardHeader(html.H5("📋 Query Results", className="mb-0")), | |
| dbc.CardBody([ | |
| html.Div(id="results-table") | |
| ]) | |
| ], className="mb-4 shadow-sm") | |
| ]), | |
| dbc.Tab(label="Details", tab_id="tab-details", children=[ | |
| dbc.Card([ | |
| dbc.CardHeader(html.H5("🔍 Query Details", className="mb-0")), | |
| dbc.CardBody([ | |
| dbc.Collapse([ | |
| dbc.Card([ | |
| dbc.CardBody([ | |
| html.Pre(id="intent-display", style={"fontSize": "12px", "backgroundColor": "#f8f9fa", "padding": "15px", "borderRadius": "5px"}) | |
| ]) | |
| ]) | |
| ], id="details-collapse", is_open=False), | |
| dbc.Button("Show/Hide Details", id="toggle-details", color="primary", className="mt-3", size="sm") | |
| ]) | |
| ], className="mb-4 shadow-sm") | |
| ]) | |
| ]) | |
| ], width=9) | |
| ]) | |
| ]) | |
| ]), | |
| # Visualizations Page (dedicated) | |
| dcc.Tab(label="Visualizations", value="tab-visualizations", children=[ | |
| dbc.Container(fluid=True, children=[ | |
| dbc.Row([ | |
| dbc.Col([ | |
| dbc.Card([ | |
| dbc.CardHeader(html.H5("📈 Visual Insights", className="mb-0")), | |
| dbc.CardBody([ | |
| dcc.Loading( | |
| id="viz-loading-standalone", | |
| type="circle", | |
| children=[ | |
| dcc.Graph(id="viz-graph-standalone", figure=go.Figure(), | |
| config={"displaylogo": False, "toImageButtonOptions": {"format": "png", "filename": "visualization_standalone"}}), | |
| html.Hr(), | |
| dcc.Graph(id="viz-graph-2-standalone", figure=go.Figure(), | |
| config={"displaylogo": False, "toImageButtonOptions": {"format": "png", "filename": "visualization_standalone_2"}}) | |
| ] | |
| ), | |
| html.Div([ | |
| dbc.Button("Download CSV", id="download-csv-btn-standalone", color="primary", size="sm", className="me-2"), | |
| dcc.Download(id="download-csv-standalone"), | |
| dbc.Button("Download PNG", id="download-png-btn-standalone", color="primary", size="sm") | |
| ], className="mt-3") | |
| ]) | |
| ], className="mb-4 shadow-sm") | |
| ], width=12) | |
| ]) | |
| ]) | |
| ]), | |
| # Reports Page | |
| dcc.Tab(label="Reports", value="tab-reports", children=[ | |
| dbc.Container(fluid=True, children=[ | |
| dbc.Row([ | |
| dbc.Col([ | |
| dbc.Card([ | |
| dbc.CardHeader(html.H5("📄 Generate Report", className="mb-0")), | |
| dbc.CardBody([ | |
| html.P("Download current results as CSV report."), | |
| dbc.Button("Download CSV Report", id="download-csv-btn-report",className="btn-primary text-white", color="blue"), | |
| dcc.Download(id="download-csv-report") | |
| ]) | |
| ], className="shadow-sm") | |
| ], width=6) | |
| ], className="p-3") | |
| ]) | |
| ]), | |
| # Forecasts Page | |
| dcc.Tab(label="Forecasts", value="tab-forecasts", children=[ | |
| dbc.Container(fluid=True, children=[ | |
| dbc.Row([ | |
| dbc.Col([ | |
| dbc.Card([ | |
| dbc.CardHeader(html.H5("🔮 Forecast (Preview)", className="mb-0")), | |
| dbc.CardBody([ | |
| html.P("Forecasting requires time-series data. Showing a placeholder visualization of the current metric."), | |
| dcc.Graph(id="viz-graph-forecast", figure=build_placeholder_forecast_figure()) | |
| ]) | |
| ], className="shadow-sm") | |
| ], width=12) | |
| ], className="p-3") | |
| ]) | |
| ]), | |
| # Knowledge Cards Page | |
| dcc.Tab(label="Knowledge", value="tab-knowledge", children=[ | |
| dbc.Container(fluid=True, children=[ | |
| dbc.Row([ | |
| dbc.Col([ | |
| dbc.Card([ | |
| dbc.CardHeader(html.H5("🧠 Insights", className="mb-0 text-black")), | |
| dbc.CardBody([ | |
| html.Div(id="knowledge-cards", className="d-flex flex-column gap-2 text-black") | |
| ]) | |
| ], className="shadow-sm") | |
| ], width=12) | |
| ], className="p-3") | |
| ]) | |
| ]), | |
| # Chat Page | |
| dcc.Tab(label="Chat", value="tab-chat", children=[ | |
| dbc.Container(fluid=True, children=[ | |
| dbc.Row([ | |
| dbc.Col([ | |
| dbc.Card([ | |
| dbc.CardHeader([ | |
| html.H5("💬 Ask Your Question", className="mb-0"), | |
| dbc.Badge(id="status-indicator", children="Ready", | |
| color="success", className="float-end") | |
| ]), | |
| dbc.CardBody([ | |
| dbc.Row([ | |
| dbc.Col([ | |
| dbc.Label("Language"), | |
| dcc.Dropdown(id="language-select", options=[ | |
| {"label": "English", "value": "en"}, | |
| {"label": "Hindi", "value": "hi"}, | |
| {"label": "Telugu", "value": "te"}, | |
| {"label": "Tamil", "value": "ta"}, | |
| ], value="en", clearable=False, className="mb-3") | |
| ]) | |
| ]), | |
| dbc.InputGroup([ | |
| dbc.Input( | |
| id="chat-input", | |
| placeholder="Ask about groundwater data...", | |
| type="text", | |
| className="form-control" | |
| ), | |
| dbc.Button("Send", id="send-btn", color="primary", n_clicks=0), | |
| dbc.Button("New Chat", id="new-chat-btn", color="secondary", n_clicks=0, className="ms-2") | |
| ], className="mb-3"), | |
| html.P("Quick examples:", className="small text-muted mb-2"), | |
| dbc.ButtonGroup([ | |
| dbc.Button("Highest Draft", id="ex1", color="outline-primary", size="sm"), | |
| dbc.Button("Over-exploited", id="ex2", color="outline-primary", size="sm"), | |
| dbc.Button("Compare Districts", id="ex3", color="outline-primary", size="sm"), | |
| dbc.Button("Underground Coverage", id="ex4", color="outline-primary", size="sm") | |
| ], className="mb-3 flex-wrap"), | |
| html.Hr(), | |
| html.Div(id="chat-history", | |
| style={"height": "60vh", "overflowY": "auto", "border": "1px solid #dee2e6", | |
| "borderRadius": "0.375rem", "padding": "10px", "backgroundColor": "#f8f9fa"}) | |
| ]) | |
| ], className="shadow-sm") | |
| ], width=8, className="mx-auto") | |
| ]) | |
| ]) | |
| ]) | |
| ]), | |
| ]), | |
| # Toast container | |
| html.Div(id="toast-container"), | |
| # Data Stores | |
| dcc.Store(id="chat-data", data=[]), | |
| dcc.Store(id="current-result", data={}), | |
| dcc.Store(id="followup-store", data=None), | |
| # Auto-refresh interval for stats | |
| dcc.Interval(id="stats-interval", interval=30000, n_intervals=0) | |
| ], id="app-root", style={"minHeight": "100vh"}) | |
| # Theme toggle callback: apply dark-mode class to root | |
| def toggle_theme(is_dark): | |
| if is_dark is None: | |
| raise PreventUpdate | |
| return "dark-mode" if is_dark else "" | |
| # Follow-up buttons handler: write clicked text into followup-store | |
| def handle_followup_click(n_clicks_list, labels): | |
| try: | |
| if not n_clicks_list: | |
| raise dash.exceptions.PreventUpdate | |
| # Find which button was clicked last | |
| for idx, n in enumerate(n_clicks_list): | |
| # Any positive click | |
| if n and n > 0: | |
| return labels[idx] | |
| raise dash.exceptions.PreventUpdate | |
| except Exception: | |
| raise dash.exceptions.PreventUpdate | |
| # Callback for example buttons | |
| def set_example_query(btn1, btn2, btn3, btn4): | |
| ctx = callback_context | |
| if not ctx.triggered: | |
| return "" | |
| button_id = ctx.triggered[0]["prop_id"].split(".")[0] | |
| examples = { | |
| "ex1": "Which districts have the highest groundwater draft?", | |
| "ex2": "Show me over-exploited districts with development over 100%", | |
| "ex3": "Compare groundwater availability between Chennai and Coimbatore", | |
| "ex4": "Show districts with largest underground water coverage areas" | |
| } | |
| return examples.get(button_id, "") | |
| # Main chat processing callback | |
| def process_chat(n_clicks, n_submit, n_new_chat, followup_data, user_input, chat_data, language_value): | |
| ctx = callback_context | |
| # Handle New Chat reset | |
| if ctx.triggered and ctx.triggered[0]["prop_id"].startswith("new-chat-btn"): | |
| return ([], create_default_map(), html.P("No data to display"), "", | |
| "Ready", "success", "No Data", "secondary", [], {}, go.Figure(), go.Figure(), None) | |
| # If a follow-up was clicked, treat it as the new user input | |
| if ctx.triggered and ctx.triggered[0]["prop_id"].startswith("followup-store") and followup_data: | |
| user_input = followup_data | |
| if not user_input or not user_input.strip() or not CHATBOT_READY: | |
| return ([], create_default_map(), html.P("No data to display"), "", | |
| "Ready", "success", "No Data", "secondary", [], {}, go.Figure(), go.Figure(), None) | |
| try: | |
| # Process query | |
| result = chatbot.chat(user_input.strip()) | |
| # Update chat history | |
| new_chat = chat_data + [ | |
| {"type": "user", "message": user_input}, | |
| {"type": "bot", "message": result["response"]} | |
| ] | |
| # Create chat display including summaries and follow-ups | |
| chat_display = [] | |
| for i, msg in enumerate(new_chat[-10:]): # Show last 10 messages | |
| if msg["type"] == "user": | |
| chat_display.append( | |
| html.Div([ | |
| html.Strong("You: ", className="text-primary"), | |
| html.Span(msg["message"]) | |
| ], className="chat-bubble-user") | |
| ) | |
| else: | |
| chat_display.append( | |
| html.Div([ | |
| html.Strong("🤖 Assistant: ", className="text-success"), | |
| dcc.Markdown(msg["message"], dangerously_allow_html=False) | |
| ], className="chat-bubble-bot") | |
| ) | |
| # Add optional summary card and follow-ups from backend | |
| summary_text = result.get("summary") | |
| follow_ups = result.get("follow_ups", []) | |
| if summary_text or follow_ups: | |
| extras = [] | |
| if summary_text: | |
| extras.append( | |
| dbc.Alert([html.Strong("Summary: ", className="me-1"), html.Span(summary_text)], color="info", className="mb-2") | |
| ) | |
| if follow_ups: | |
| extras.append( | |
| html.Div([ | |
| html.Div("Try these:", className="small text-muted mb-1"), | |
| dbc.ButtonGroup([ | |
| *[dbc.Button(q, id={"type": "followup", "index": i}, color="outline-primary", size="sm", className="me-1 mb-1") for i, q in enumerate(follow_ups)] | |
| ], className="flex-wrap") | |
| ], className="mb-3") | |
| ) | |
| chat_display.extend(extras) | |
| # Create enhanced groundwater map | |
| groundwater_map = create_underground_water_map(result["results"]) | |
| # Create results table | |
| results_table = create_results_table(result["results"]) | |
| # Format intent details | |
| intent_display = json.dumps(result["intent_analysis"], indent=2) | |
| # Status updates | |
| status = f"Found {result['results_count']} results" | |
| status_color = "success" if result["success"] else "danger" | |
| # Enhanced map status with underground water info | |
| underground_districts = len([r for r in result['results'] if r.get('geometry') and r.get('st_area_shape')]) | |
| map_status = f"Mapped {underground_districts} districts with underground coverage" | |
| map_color = "info" if underground_districts > 0 else "warning" | |
| # Build visualization figures if spec provided | |
| viz_fig = build_visualization_figure(result) | |
| viz_fig_2 = build_secondary_visualization_figure(result) | |
| # Toast notification (success) | |
| toast = dbc.Toast( | |
| [html.Div(f"{status}")], | |
| header="Query Processed", | |
| icon="success", | |
| duration=4000, | |
| is_open=True, | |
| style={"position": "fixed", "top": 10, "right": 10, "zIndex": 2000} | |
| ) | |
| return (chat_display, groundwater_map, results_table, intent_display, | |
| status, status_color, map_status, map_color, new_chat, result, viz_fig, viz_fig_2, toast) | |
| except Exception as e: | |
| error_msg = f"Error processing query: {str(e)}" | |
| error_chat = chat_data + [ | |
| {"type": "user", "message": user_input}, | |
| {"type": "bot", "message": error_msg} | |
| ] | |
| chat_display = [ | |
| html.Div([ | |
| html.Strong("Error: ", className="text-danger"), | |
| html.Span(error_msg) | |
| ], className="mb-2 p-2 bg-danger text-white rounded") | |
| ] | |
| toast_err = dbc.Toast( | |
| [html.Div(error_msg)], | |
| header="Error", | |
| icon="danger", | |
| duration=6000, | |
| is_open=True, | |
| style={"position": "fixed", "top": 10, "right": 10, "zIndex": 2000} | |
| ) | |
| return (chat_display, create_default_map(), html.P("Error occurred"), error_msg, | |
| "Error", "danger", "Error", "danger", error_chat, {}, go.Figure(), go.Figure(), toast_err) | |
| # Quick stats callback | |
| def update_stats(n_intervals): | |
| if not CHATBOT_READY: | |
| return "--", "--", "--", "--", "--", "--", "--", "--" | |
| try: | |
| stats = chatbot.get_quick_stats() | |
| return ( | |
| str(stats.get("total_districts", "--")), | |
| f"{stats.get('avg_development', '--')}%", | |
| str(stats.get("over_exploited", "--")), | |
| str(stats.get("critical", "--")), | |
| str(stats.get("total_districts", "--")), | |
| f"{stats.get('avg_development', '--')}%", | |
| str(stats.get("over_exploited", "--")), | |
| str(stats.get("critical", "--")) | |
| ) | |
| except Exception: | |
| return "--", "--", "--", "--", "--", "--", "--", "--" | |
| # Toggle details callback | |
| def toggle_details(n_clicks, is_open): | |
| if n_clicks: | |
| return not is_open | |
| return is_open | |
| def create_default_map(): | |
| """Create default India map""" | |
| try: | |
| # India center coordinates | |
| india_center = [20.5937, 78.9629] | |
| m = folium.Map( | |
| location=india_center, | |
| zoom_start=5, # Zoom out for India view | |
| tiles='OpenStreetMap' | |
| ) | |
| # Add a marker for India center | |
| folium.Marker( | |
| india_center, | |
| popup="India - Underground Water Data Center", | |
| tooltip="Click for more info", | |
| icon=folium.Icon(color='blue', icon='tint') | |
| ).add_to(m) | |
| # Add title | |
| title_html = ''' | |
| <h3 align="center" style="font-size:16px"><b>India Underground Water Coverage</b></h3> | |
| ''' | |
| m.get_root().html.add_child(folium.Element(title_html)) | |
| return html.Iframe( | |
| srcDoc=m._repr_html_(), | |
| style={"width": "100%", "height": "450px", "border": "none"} | |
| ) | |
| except Exception as e: | |
| return html.Div([ | |
| html.H5("Map Loading Error", className="text-center text-muted"), | |
| html.P(f"Unable to load map: {str(e)}", className="text-center") | |
| ], style={"height": "450px", "display": "flex", "flexDirection": "column", | |
| "justifyContent": "center", "alignItems": "center"}) | |
| def extract_centroid(geometry): | |
| """Extract centroid from WKT geometry with proper coordinate handling""" | |
| try: | |
| # Parse the WKT geometry | |
| geom = wkt.loads(geometry) | |
| # Get the centroid | |
| centroid = geom.centroid | |
| # Extract coordinates and swap them (lat, lon instead of lon, lat) | |
| # Database stores coordinates as (longitude, latitude) but we need (latitude, longitude) | |
| coords = list(centroid.coords)[0] | |
| # Return as (latitude, longitude) | |
| return (coords[1], coords[0]) | |
| except Exception as e: | |
| print(f"Error parsing geometry: {e}") | |
| return None | |
| def create_underground_water_map(results): | |
| """Create interactive underground water coverage map with proper coordinate handling""" | |
| if not results: | |
| return create_default_map() | |
| try: | |
| # Convert results to dataframe | |
| df = pd.DataFrame(results) | |
| # Check if we have geometry data | |
| if 'geometry' not in df.columns: | |
| return create_simple_data_map(df) | |
| # Filter out rows without geometry or underground water data | |
| df_with_geo = df[(df['geometry'].notna()) & | |
| (df['st_area_shape'].notna()) & | |
| (df['st_length_shape'].notna())].copy() | |
| if len(df_with_geo) == 0: | |
| return create_simple_data_map(df) | |
| # India center coordinates | |
| india_center = [20.5937, 78.9629] | |
| # Create map | |
| m = folium.Map( | |
| location=india_center, | |
| zoom_start=5, | |
| tiles='CartoDB positron' | |
| ) | |
| # Calculate underground water coverage intensity | |
| if len(df_with_geo) > 0: | |
| max_area = df_with_geo['st_area_shape'].max() | |
| min_area = df_with_geo['st_area_shape'].min() | |
| # Add districts with enhanced underground water visualization | |
| for idx, row in df_with_geo.iterrows(): | |
| try: | |
| # Parse WKT geometry | |
| geom = wkt.loads(row['geometry']) | |
| # Calculate underground water coverage intensity | |
| area_intensity = (row['st_area_shape'] - min_area) / (max_area - min_area) if max_area > min_area else 0.5 | |
| # Determine color based on development stage and underground coverage | |
| dev_stage = row.get('stage_of_development', 0) | |
| if pd.isna(dev_stage): | |
| color = 'gray' | |
| elif dev_stage > 100: | |
| color = 'red' # Over-exploited | |
| elif dev_stage > 80: | |
| color = 'orange' # Critical | |
| elif dev_stage > 60: | |
| color = 'yellow' # Semi-critical | |
| else: | |
| color = 'green' # Safe | |
| # Adjust opacity based on underground coverage area | |
| fill_opacity = max(0.3, min(0.9, 0.3 + (area_intensity * 0.6))) | |
| # Create enhanced popup content with underground water info | |
| popup_content = create_underground_popup_content(row) | |
| # Add geometry to map with enhanced styling | |
| if geom.geom_type == 'MultiPolygon': | |
| for polygon in geom.geoms: | |
| # Extract coordinates and swap them (lat, lon instead of lon, lat) | |
| coords = [[point[1], point[0]] for point in polygon.exterior.coords] | |
| folium.Polygon( | |
| locations=coords, | |
| color='black', | |
| weight=2, | |
| fillColor=color, | |
| fillOpacity=fill_opacity, | |
| popup=folium.Popup(popup_content, max_width=500), | |
| tooltip=f"{row.get('district', 'Unknown District')} - Coverage: {row.get('st_area_shape', 0):,.0f} sq.m" | |
| ).add_to(m) | |
| elif geom.geom_type == 'Polygon': | |
| # Extract coordinates and swap them (lat, lon instead of lon, lat) | |
| coords = [[point[1], point[0]] for point in geom.exterior.coords] | |
| folium.Polygon( | |
| locations=coords, | |
| color='black', | |
| weight=2, | |
| fillColor=color, | |
| fillOpacity=fill_opacity, | |
| popup=folium.Popup(popup_content, max_width=500), | |
| tooltip=f"{row.get('district', 'Unknown District')} - Coverage: {row.get('st_area_shape', 0):,.0f} sq.m" | |
| ).add_to(m) | |
| except Exception as e: | |
| print(f"Error processing geometry for {row.get('district', 'unknown')}: {e}") | |
| continue | |
| # Add enhanced legend | |
| add_underground_legend_to_map(m) | |
| # Add title | |
| title_html = f''' | |
| <h3 align="center" style="font-size:16px"><b>India Underground Water Coverage - {len(df_with_geo)} Districts</b></h3> | |
| <p align="center" style="font-size:12px; color: #666;">Opacity indicates underground water coverage area intensity</p> | |
| ''' | |
| m.get_root().html.add_child(folium.Element(title_html)) | |
| return html.Iframe( | |
| srcDoc=m._repr_html_(), | |
| style={"width": "100%", "height": "450px", "border": "none"} | |
| ) | |
| except Exception as e: | |
| print(f"Error creating underground water map: {e}") | |
| return create_default_map() | |
| def create_underground_popup_content(row): | |
| """Create enhanced HTML popup content with underground water details""" | |
| district = row.get('district', 'Unknown') | |
| content = f"<b>{district} District</b><br><hr>" | |
| # Underground Water Coverage Section | |
| content += "<b>🏔️ Underground Water Coverage:</b><br>" | |
| if 'st_area_shape' in row and not pd.isna(row['st_area_shape']): | |
| content += f"Coverage Area: <b>{row['st_area_shape']:,.0f} sq.m</b><br>" | |
| if 'st_length_shape' in row and not pd.isna(row['st_length_shape']): | |
| content += f"Perimeter: <b>{row['st_length_shape']:,.0f} m</b><br>" | |
| content += "<br><b>💧 Groundwater Metrics:</b><br>" | |
| # Add key groundwater metrics | |
| metrics = [ | |
| ('Development Stage', 'stage_of_development', '%'), | |
| ('Total Draft', 'annual_gw_draft_total', ' HM'), | |
| ('Net Availability', 'net_gw_availability', ' HM'), | |
| ('Replenishable Resource', 'annual_replenishable_gw_resource', ' HM'), | |
| ('Irrigation Draft', 'annual_draft_irrigation', ' HM') | |
| ] | |
| for label, key, unit in metrics: | |
| if key in row and not pd.isna(row[key]): | |
| value = row[key] | |
| if isinstance(value, (int, float)): | |
| if unit == '%': | |
| content += f"{label}: <b>{value:.1f}{unit}</b><br>" | |
| elif unit == ' HM': | |
| content += f"{label}: <b>{value:,.0f}{unit}</b><br>" | |
| else: | |
| content += f"{label}: <b>{value}{unit}</b><br>" | |
| # Add underground water assessment | |
| if 'st_area_shape' in row and not pd.isna(row['st_area_shape']): | |
| area = row['st_area_shape'] | |
| if area > 3000000000: # > 3 billion sq.m | |
| assessment = "🟢 Extensive underground coverage" | |
| elif area > 1500000000: # > 1.5 billion sq.m | |
| assessment = "🟡 Moderate underground coverage" | |
| else: | |
| assessment = "🔴 Limited underground coverage" | |
| content += f"<br><b>Assessment:</b> {assessment}" | |
| return content | |
| def add_underground_legend_to_map(m): | |
| """Add enhanced color legend for underground water coverage""" | |
| legend_html = ''' | |
| <div style="position: fixed; | |
| bottom: 50px; left: 50px; width: 200px; height: 160px; | |
| background-color: white; border:2px solid grey; z-index:9999; | |
| font-size:12px; padding: 10px; border-radius: 5px;"> | |
| <p><b>Development Stage:</b></p> | |
| <p><i class="fa fa-square" style="color:green"></i> Safe (<60%)</p> | |
| <p><i class="fa fa-square" style="color:yellow"></i> Semi-critical (60-80%)</p> | |
| <p><i class="fa fa-square" style="color:orange"></i> Critical (80-100%)</p> | |
| <p><i class="fa fa-square" style="color:red"></i> Over-exploited (>100%)</p> | |
| <hr> | |
| <p><b>Underground Coverage:</b></p> | |
| <p>Opacity = Coverage area intensity</p> | |
| </div> | |
| ''' | |
| m.get_root().html.add_child(folium.Element(legend_html)) | |
| def create_simple_data_map(df): | |
| """Create simple marker-based map when no geometry available""" | |
| try: | |
| # India center coordinates | |
| india_center = [20.5937, 78.9629] | |
| m = folium.Map(location=india_center, zoom_start=5, tiles='OpenStreetMap') | |
| # Add markers for districts in the data | |
| for idx, row in df.iterrows(): | |
| district = row.get('district', '').strip() | |
| # Use a simple approach - just place markers at approximate locations | |
| # In a real implementation, you'd want to geocode district names | |
| # For now, we'll just use random coordinates around India | |
| import random | |
| lat = 20.5937 + random.uniform(-5, 5) | |
| lon = 78.9629 + random.uniform(-5, 5) | |
| # Determine marker color based on development stage | |
| dev_stage = row.get('stage_of_development', 0) | |
| if pd.isna(dev_stage): | |
| color = 'gray' | |
| elif dev_stage > 100: | |
| color = 'red' | |
| elif dev_stage > 80: | |
| color = 'orange' | |
| elif dev_stage > 60: | |
| color = 'blue' | |
| else: | |
| color = 'green' | |
| # Create popup with underground water info | |
| popup_content = create_underground_popup_content(row) | |
| folium.Marker( | |
| [lat, lon], | |
| popup=folium.Popup(popup_content, max_width=400), | |
| tooltip=f"{district} - Underground coverage available", | |
| icon=folium.Icon(color=color, icon='tint') | |
| ).add_to(m) | |
| # Add title | |
| title_html = f''' | |
| <h3 align="center" style="font-size:16px"><b>India Districts - {len(df)} Locations</b></h3> | |
| ''' | |
| m.get_root().html.add_child(folium.Element(title_html)) | |
| return html.Iframe( | |
| srcDoc=m._repr_html_(), | |
| style={"width": "100%", "height": "450px", "border": "none"} | |
| ) | |
| except Exception as e: | |
| return create_default_map() | |
| def create_results_table(results): | |
| """Create enhanced results table with underground water columns""" | |
| if not results: | |
| return html.P("No results to display", className="text-muted") | |
| df = pd.DataFrame(results) | |
| # Prioritize columns including underground water metrics | |
| display_cols = [] | |
| priority_cols = ['district', 'st_area_shape', 'st_length_shape', 'annual_gw_draft_total', | |
| 'stage_of_development', 'net_gw_availability'] | |
| for col in priority_cols: | |
| if col in df.columns: | |
| display_cols.append(col) | |
| # Add other columns except geometry | |
| other_cols = [col for col in df.columns if col not in priority_cols and col != 'geometry'] | |
| display_cols.extend(other_cols[:3]) | |
| if display_cols: | |
| display_df = df[display_cols].head(10) | |
| # Format column names for better readability | |
| column_names = [] | |
| for col in display_cols: | |
| if col == 'st_area_shape': | |
| column_names.append({"name": "Underground Coverage (sq.m)", "id": col, "type": "numeric", "format": {"specifier": ",.0f"}}) | |
| elif col == 'st_length_shape': | |
| column_names.append({"name": "Underground Perimeter (m)", "id": col, "type": "numeric", "format": {"specifier": ",.0f"}}) | |
| else: | |
| column_names.append({"name": col.replace('_', ' ').title(), "id": col}) | |
| return dash_table.DataTable( | |
| data=display_df.to_dict('records'), | |
| columns=column_names, | |
| style_cell={'textAlign': 'left', 'fontSize': '12px', 'padding': '8px', 'whiteSpace': 'normal', 'height': 'auto'}, | |
| style_header={'backgroundColor': 'rgb(230, 230, 230)', 'fontWeight': 'bold'}, | |
| style_data_conditional=[ | |
| { | |
| 'if': { | |
| 'filter_query': '{stage_of_development} > 100', | |
| 'column_id': 'stage_of_development' | |
| }, | |
| 'backgroundColor': '#ffebee', | |
| 'color': 'black', | |
| }, | |
| { | |
| 'if': { | |
| 'filter_query': '{stage_of_development} > 80 && {stage_of_development} <= 100', | |
| 'column_id': 'stage_of_development' | |
| }, | |
| 'backgroundColor': '#fff3e0', | |
| 'color': 'black', | |
| }, | |
| # Highlight large underground coverage areas | |
| { | |
| 'if': { | |
| 'filter_query': '{st_area_shape} > 3000000000', | |
| 'column_id': 'st_area_shape' | |
| }, | |
| 'backgroundColor': '#e8f5e8', | |
| 'color': 'black', | |
| } | |
| ], | |
| page_size=10, | |
| sort_action="native", | |
| filter_action="native", | |
| tooltip_data=[ | |
| { | |
| column: {'value': 'Underground water coverage area in square meters', 'type': 'markdown'} | |
| if column == 'st_area_shape' | |
| else {'value': 'Underground water perimeter in meters', 'type': 'markdown'} | |
| if column == 'st_length_shape' | |
| else {'value': str(value), 'type': 'text'} | |
| for column, value in row.items() | |
| } for row in display_df.to_dict('records') | |
| ], | |
| tooltip_duration=None | |
| ) | |
| return html.P("Unable to display results", className="text-muted") | |
| def build_visualization_figure(result_dict): | |
| """Create a Plotly figure based on backend-provided visualization spec and results.""" | |
| try: | |
| viz = result_dict.get("visualization", {}) if isinstance(result_dict, dict) else {} | |
| results = result_dict.get("results", []) if isinstance(result_dict, dict) else [] | |
| if not viz or not viz.get("enabled") or not results: | |
| return go.Figure() | |
| df = pd.DataFrame(results) | |
| # Coerce numeric columns safely | |
| for col in [viz.get("y"), viz.get("x")]: | |
| if col and col in df.columns: | |
| try: | |
| df[col] = pd.to_numeric(df[col], errors='coerce') if col != 'district' else df[col] | |
| except Exception: | |
| pass | |
| chart_type = viz.get("chart_type", "bar") | |
| x_col = viz.get("x") | |
| y_col = viz.get("y") | |
| top_n = viz.get("top_n", 10) | |
| title = viz.get("title", "Data Visualization") | |
| # Reduce to top_n by y if possible | |
| plot_df = df.copy() | |
| if y_col in plot_df.columns and pd.api.types.is_numeric_dtype(plot_df[y_col]): | |
| plot_df = plot_df.sort_values(by=y_col, ascending=False).head(top_n) | |
| else: | |
| plot_df = plot_df.head(top_n) | |
| if chart_type == "histogram" and x_col and x_col in plot_df.columns: | |
| fig = px.histogram(plot_df, x=x_col, nbins=20, title=title) | |
| elif chart_type == "scatter" and x_col and y_col and x_col in plot_df.columns and y_col in plot_df.columns: | |
| fig = px.scatter(plot_df, x=x_col, y=y_col, hover_data=[c for c in plot_df.columns if c not in ['geometry']], title=title) | |
| else: | |
| # Default to bar; pick axis intelligently | |
| if (not x_col or x_col not in plot_df.columns) and 'district' in plot_df.columns: | |
| x_col = 'district' | |
| if not y_col or y_col not in plot_df.columns: | |
| # choose a numeric column fallback | |
| candidates = [c for c in [ | |
| 'annual_gw_draft_total', 'stage_of_development', 'net_gw_availability', | |
| 'annual_replenishable_gw_resource', 'annual_draft_irrigation', | |
| 'st_area_shape', 'st_length_shape' | |
| ] if c in plot_df.columns] | |
| y_col = candidates[0] if candidates else None | |
| if x_col and y_col and y_col in plot_df.columns: | |
| fig = px.bar(plot_df, x=x_col, y=y_col, title=title, hover_data=[c for c in plot_df.columns if c not in ['geometry']]) | |
| else: | |
| fig = go.Figure() | |
| fig.update_layout(margin=dict(l=10, r=10, t=50, b=10), height=400) | |
| return fig | |
| except Exception: | |
| return go.Figure() | |
| def build_secondary_visualization_figure(result_dict): | |
| """Second complementary chart (e.g., perimeter vs area scatter).""" | |
| try: | |
| results = result_dict.get("results", []) if isinstance(result_dict, dict) else [] | |
| if not results: | |
| return go.Figure() | |
| df = pd.DataFrame(results) | |
| if not {'st_area_shape', 'st_length_shape'}.issubset(df.columns): | |
| return go.Figure() | |
| # Coerce numeric | |
| for col in ['st_area_shape', 'st_length_shape']: | |
| if col in df.columns: | |
| df[col] = pd.to_numeric(df[col], errors='coerce') | |
| # Keep top 50 by area | |
| df_plot = df.sort_values(by='st_area_shape', ascending=False).head(50) | |
| if 'district' not in df_plot.columns: | |
| df_plot['district'] = [f"District {i+1}" for i in range(len(df_plot))] | |
| fig = px.scatter( | |
| df_plot, | |
| x='st_area_shape', | |
| y='st_length_shape', | |
| hover_name='district', | |
| title='Perimeter vs Area (Underground Coverage)', | |
| labels={'st_area_shape': 'Coverage Area (sq.m)', 'st_length_shape': 'Perimeter (m)'} | |
| ) | |
| fig.update_layout(margin=dict(l=10, r=10, t=50, b=10), height=350) | |
| return fig | |
| except Exception: | |
| return go.Figure() | |
| # Download current map as HTML | |
| def download_map_html(n_clicks, result_dict): | |
| try: | |
| if not n_clicks: | |
| return dash.no_update | |
| # Rebuild the map HTML from current results for export | |
| results = (result_dict or {}).get("results", []) | |
| iframe = create_underground_water_map(results) | |
| if isinstance(iframe, html.Iframe): | |
| html_str = iframe.props.get("srcDoc") or "" | |
| else: | |
| html_str = "" | |
| if not html_str: | |
| return dash.no_update | |
| return dict(content=html_str, filename="groundwater_map.html") | |
| except Exception: | |
| return dash.no_update | |
| # Download CSV of current results | |
| def download_results_csv(n_clicks, result_dict): | |
| try: | |
| if not n_clicks: | |
| return dash.no_update | |
| results = (result_dict or {}).get("results", []) | |
| df = pd.DataFrame(results) | |
| if df.empty: | |
| return dash.no_update | |
| return dcc.send_data_frame(df.to_csv, "groundwater_results.csv", index=False) | |
| except Exception: | |
| return dash.no_update | |
| # Standalone Visualizations page CSV download | |
| def download_results_csv_standalone(n_clicks, result_dict): | |
| try: | |
| if not n_clicks: | |
| return dash.no_update | |
| results = (result_dict or {}).get("results", []) | |
| df = pd.DataFrame(results) | |
| if df.empty: | |
| return dash.no_update | |
| return dcc.send_data_frame(df.to_csv, "groundwater_results.csv", index=False) | |
| except Exception: | |
| return dash.no_update | |
| # Sync standalone visualizations with current result | |
| def populate_standalone_viz(result_dict): | |
| try: | |
| if not result_dict: | |
| return go.Figure(), go.Figure() | |
| return build_visualization_figure(result_dict), build_secondary_visualization_figure(result_dict) | |
| except Exception: | |
| return go.Figure(), go.Figure() | |
| # Reports page CSV download | |
| def download_results_csv_report(n_clicks, result_dict): | |
| try: | |
| if not n_clicks: | |
| return dash.no_update | |
| results = (result_dict or {}).get("results", []) | |
| df = pd.DataFrame(results) | |
| if df.empty: | |
| return dash.no_update | |
| return dcc.send_data_frame(df.to_csv, "groundwater_report.csv", index=False) | |
| except Exception: | |
| return dash.no_update | |
| # Forecast and Knowledge Cards from current results | |
| def build_forecast_and_cards(result_dict, active_tab): | |
| try: | |
| def _placeholder_figure(title_suffix="No time-series data"): | |
| x_vals = list(range(1, 11)) | |
| y_vals = list(range(1, 11)) | |
| fig = go.Figure() | |
| fig.add_trace(go.Scatter( | |
| x=x_vals, | |
| y=y_vals, | |
| mode='lines+markers', | |
| name='Placeholder', | |
| marker=dict(color="#2563eb"), | |
| line=dict(color="#2563eb") | |
| )) | |
| fig.update_layout( | |
| title=f"Forecast Preview ({title_suffix})", | |
| height=360, | |
| margin=dict(l=10, r=10, t=50, b=10), | |
| plot_bgcolor="#ffffff", | |
| paper_bgcolor="#ffffff" | |
| ) | |
| return fig | |
| if not result_dict: | |
| # Minimal readable placeholder with hint | |
| placeholder = _placeholder_figure() | |
| hint = dbc.Alert("Run a query in Chat or Explore to populate insights.", color="secondary", className="mb-2") | |
| return placeholder, [hint] | |
| results = result_dict.get("results", []) | |
| viz = result_dict.get("visualization", {}) | |
| insights = result_dict.get("insights", []) or [] | |
| df = pd.DataFrame(results) | |
| if df.empty: | |
| # Even if results empty, still show insights if any | |
| insights_children = [] | |
| if insights: | |
| insights_children.append( | |
| dbc.Alert( | |
| [html.Strong("Insights", className="me-2")] + [html.Div(f"• {i.get('title', '')}: {i.get('detail', '')}") for i in insights], | |
| color="light", | |
| className="mb-3" | |
| ) | |
| ) | |
| placeholder = _placeholder_figure() | |
| if not insights_children: | |
| insights_children = [dbc.Alert("No insights available for current selection.", color="secondary", className="mb-2")] | |
| return placeholder, insights_children | |
| # Forecast placeholder: line over sorted top N by selected metric | |
| metric = viz.get("y") if viz else None | |
| if not metric or metric not in df.columns: | |
| for c in [ | |
| 'annual_gw_draft_total', 'stage_of_development', 'net_gw_availability', | |
| 'annual_replenishable_gw_resource', 'annual_draft_irrigation', 'st_area_shape' | |
| ]: | |
| if c in df.columns: | |
| metric = c | |
| break | |
| plot_df = df.copy() | |
| if metric in plot_df.columns: | |
| with pd.option_context('mode.use_inf_as_na', True): | |
| plot_df[metric] = pd.to_numeric(plot_df[metric], errors='coerce') | |
| plot_df = plot_df.dropna(subset=[metric]).sort_values(by=metric, ascending=False).head(20) | |
| x_vals = list(range(1, len(plot_df) + 1)) | |
| fig_forecast = go.Figure() | |
| fig_forecast.add_trace(go.Scatter( | |
| x=x_vals, | |
| y=plot_df[metric], | |
| mode='lines+markers', | |
| name='Metric', | |
| marker=dict(color="#2563eb"), | |
| line=dict(color="#2563eb") | |
| )) | |
| fig_forecast.update_layout( | |
| title=f"Forecast Preview for {metric.replace('_',' ').title()}", | |
| height=360, | |
| margin=dict(l=10, r=10, t=50, b=10), | |
| plot_bgcolor="#ffffff", | |
| paper_bgcolor="#ffffff" | |
| ) | |
| else: | |
| fig_forecast = _placeholder_figure("No suitable metric found") | |
| # Knowledge: insights + cards | |
| knowledge_children = [] | |
| if insights: | |
| knowledge_children.append( | |
| dbc.Alert( | |
| [html.Strong("Insights", className="me-2 text-black")] + [html.Div(f"• {i.get('title', '')}: {i.get('detail', '')}") for i in insights], | |
| color="light", | |
| className="mb-3 text-black" | |
| ) | |
| ) | |
| cards = [] | |
| top_df = plot_df.head(6) if metric in df.columns else df.head(6) | |
| for _, row in top_df.iterrows(): | |
| title = str(row.get('district', 'District')).title() | |
| area = row.get('st_area_shape') | |
| perim = row.get('st_length_shape') | |
| dev = row.get('stage_of_development') | |
| body = [ | |
| html.Div(f"Area: {area:,.0f} sq.m" if isinstance(area, (int, float)) else f"Area: {area}"), | |
| html.Div(f"Perimeter: {perim:,.0f} m" if isinstance(perim, (int, float)) else f"Perimeter: {perim}"), | |
| html.Div(f"Development: {dev:.1f}%" if isinstance(dev, (int, float)) else f"Development: {dev}") | |
| ] | |
| cards.append( | |
| dbc.Card([ | |
| dbc.CardHeader(html.Strong(title)), | |
| dbc.CardBody(body) | |
| ], className="me-2 mb-2 shadow-sm", style={"minWidth": "220px"}) | |
| ) | |
| knowledge_children.extend(cards) | |
| return fig_forecast, knowledge_children | |
| except Exception: | |
| return go.Figure(), [] | |
| if __name__ == "__main__": | |
| if CHATBOT_READY: | |
| print("🌊 India Underground Water AI Chatbot") | |
| print("🚀 Starting server at http://localhost:8050") | |
| app.run(debug=True, host="0.0.0.0", port=8050) | |
| else: | |
| print("❌ Cannot start - Chatbot initialization failed") | |
| print("Please check your environment variables and database connection") |