import pandas as pd
import dash
from dash import dcc, html, Input, Output, State
import dash_bootstrap_components as dbc
import plotly.express as px
# Load and preprocess data
df = pd.read_csv(
"https://raw.githubusercontent.com/banana0000/US-Damps/refs/heads/main/nation-dams.csv"
)
df['Hazard Potential Classification'] = df['Hazard Potential Classification'].fillna('Unknown')
df['Latitude'] = pd.to_numeric(df['Latitude'], errors='coerce')
df['Longitude'] = pd.to_numeric(df['Longitude'], errors='coerce')
df['Year Completed'] = pd.to_numeric(df['Year Completed'], errors='coerce')
def get_state_options(dff):
return [
{"label": s, "value": s}
for s in sorted(dff['State'].dropna().unique())
]
def get_hazard_options(dff):
return [
{"label": h, "value": h}
for h in sorted(dff['Hazard Potential Classification'].dropna().unique())
]
def get_year_options(dff):
return [
{"label": str(int(y)), "value": int(y)}
for y in sorted(dff['Year Completed'].dropna().unique())
]
kpi_color = "#1F2020"
my_colors = [
"#2E8DAA", "#0C2C57",
"#22E03B", "#E9E909"
]
gradient_bg = {
"background": "linear-gradient(90deg,white,lightblue)",
"minHeight": "100vh",
"width": "100vw",
"paddingBottom": "32px"
}
card_style = {
"backgroundColor": "white",
"borderRadius": "16px",
"boxShadow": "0 4px 24px rgba(0,0,0,0.08)",
"padding": "0",
}
# KPI card style for compact, side-by-side display
kpi_card_style = {
"backgroundColor": "white",
"borderRadius": "12px",
"boxShadow": "0 2px 8px rgba(0,0,0,0.06)",
"padding": "0.75rem 1rem",
"minWidth": "250px",
"maxWidth": "180px",
"margin": "0 8px",
"display": "inline-block",
"verticalAlign": "top",
}
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
app.layout = html.Div(
[
dbc.Container(
[
html.H1(
"USA Dams Dashboard",
className="my-4 text-left text-dark",
style={"fontWeight": "bold", "letterSpacing": "1px", "font-family":"Viridis"},
),
dbc.Row(
[
# Sidebar filters (left)
dbc.Col(
[
dbc.Card(
[
dbc.CardHeader(
"Filters",
className="bg-secondary text-white border-0",
style={
"padding": "1rem 1.25rem",
"borderRadius": "16px 16px 0 0",
"fontWeight": "bold",
"fontSize": "1.1rem",
},
),
dbc.CardBody(
[
html.Label("State", className="text-primary mb-1"),
dcc.Dropdown(
id="state-dropdown",
options=get_state_options(df),
multi=True,
placeholder="Select state(s)",
style={"margin-bottom": "18px"},
),
html.Label(
"Hazard Potential", className="text-primary mb-1"
),
dcc.Dropdown(
id="hazard-dropdown",
options=get_hazard_options(df),
multi=True,
placeholder="Select hazard potential(s)",
style={"margin-bottom": "18px"},
),
html.Label(
"Year Completed", className="text-primary mb-1"
),
dcc.Dropdown(
id="year-dropdown",
options=get_year_options(df),
multi=True,
placeholder="Select year(s)",
style={"margin-bottom": "6px"},
),
],
style={"padding": "1.25rem"},
),
],
className="shadow border-0 mb-4",
style={
**card_style,
"marginTop": "120px",
"marginLeft": "78px",
"minWidth": "260px",
"maxWidth": "400px",
},
),
],
xs=12, sm=12, md=4, lg=3,
style={"paddingRight": "0"},
),
# Main content (KPIs + map)
dbc.Col(
[
# KPI cards in a single row, always side by side
html.Div(
[
dbc.Card(
dbc.CardBody(
[
html.H6(
"Total Dams",
style={
"fontWeight": "bold",
"color": kpi_color,
"fontSize": "1rem",
"marginBottom": "0.3rem",
},
),
html.H4(
id="kpi-count",
style={
"fontWeight": "normal",
"color": kpi_color,
"marginBottom": "0",
},
),
]
),
className="shadow border-0",
style=kpi_card_style,
),
dbc.Card(
dbc.CardBody(
[
html.H6(
"Average Height (ft)",
style={
"fontWeight": "bold",
"color": kpi_color,
"fontSize": "1rem",
"marginBottom": "0.3rem",
},
),
html.H4(
id="kpi-height",
style={
"fontWeight": "normal",
"color": kpi_color,
"marginBottom": "0",
},
),
]
),
className="shadow border-0",
style=kpi_card_style,
),
dbc.Card(
dbc.CardBody(
[
html.H6(
"Most Common Hazard",
style={
"fontWeight": "bold",
"color": kpi_color,
"fontSize": "1rem",
"marginBottom": "0.3rem",
},
),
html.H4(
id="kpi-hazard",
style={
"fontWeight": "normal",
"color": kpi_color,
"marginBottom": "0",
},
),
]
),
className="shadow border-0",
style=kpi_card_style,
),
],
style={
"display": "flex",
"justifyContent": "center",
"alignItems": "center",
"marginBottom": "18px",
},
),
# Map card
dbc.Card(
[
dbc.CardHeader(
"Dams Map",
className="bg-secondary text-white border-0",
style={
"padding": "1rem 1.25rem",
"borderRadius": "16px 16px 0 0",
"fontWeight": "bold",
},
),
dbc.CardBody(
[
dcc.Graph(
id="map",
style={"height": "500px"},
config={
"scrollZoom": True,
"displayModeBar": True,
}
),
],
style={"padding": "1.25rem"},
),
],
className="mb-4 shadow border-0",
style={**card_style},
),
dcc.Location(id="dummy-location", refresh=True),
],
xs=12, sm=12, md=8, lg=6,
),
# Dam details (right) - always visible
dbc.Col(
dbc.Card(
[
dbc.CardHeader(
"Click on a point to see dam details",
className="bg-secondary text-white border-0",
style={
"padding": "1rem 1.25rem",
"borderRadius": "16px 16px 0 0",
"fontWeight": "bold",
},
id="details-card-title",
),
dbc.CardBody(
html.Div(id="details-card-body"),
style={"padding": "1.25rem"},
),
],
className="h-80 mt-4 shadow border-0",
style={**card_style, "minWidth": "260px", "maxWidth": "400px"},
),
xs=12, sm=12, md=12, lg=3,
style={"marginTop": "120px"},
),
],
className="g-4", # smaller gutter for closer columns
),
],
fluid=True,
style={
"background": "transparent",
"minHeight": "100vh",
"fontFamily": "Segoe UI, Arial, sans-serif",
},
)
],
style=gradient_bg,
)
# --- Dynamic filter options callback ---
@app.callback(
Output("state-dropdown", "options"),
Output("hazard-dropdown", "options"),
Output("year-dropdown", "options"),
Input("state-dropdown", "value"),
Input("hazard-dropdown", "value"),
Input("year-dropdown", "value"),
)
def update_filter_options(selected_states, selected_hazards, selected_years):
# Filter dropdown options based on current selections
dff = df.copy()
if selected_states:
dff = dff[dff['State'].isin(selected_states)]
if selected_hazards:
dff = dff[dff['Hazard Potential Classification'].isin(selected_hazards)]
if selected_years:
dff = dff[dff['Year Completed'].isin(selected_years)]
state_options = get_state_options(df if not (selected_hazards or selected_years) else dff)
hazard_options = get_hazard_options(df if not (selected_states or selected_years) else dff)
year_options = get_year_options(df if not (selected_states or selected_hazards) else dff)
return state_options, hazard_options, year_options
# --- Main dashboard callback: map and KPIs ---
@app.callback(
Output("map", "figure"),
Output("kpi-count", "children"),
Output("kpi-height", "children"),
Output("kpi-hazard", "children"),
Input("state-dropdown", "value"),
Input("hazard-dropdown", "value"),
Input("year-dropdown", "value"),
)
def update_dashboard(selected_states, selected_hazards, selected_years):
# Filter data based on dropdowns
dff = df.copy()
if selected_states:
dff = dff[dff['State'].isin(selected_states)]
if selected_hazards:
dff = dff[dff['Hazard Potential Classification'].isin(selected_hazards)]
if selected_years:
dff = dff[dff['Year Completed'].isin(selected_years)]
dff_map = dff.dropna(subset=["Latitude", "Longitude"])
# Create the map figure
if dff_map.empty:
fig_map = px.scatter_mapbox(
pd.DataFrame({"Latitude": [], "Longitude": []}),
lat="Latitude",
lon="Longitude",
zoom=2,
height=500,
mapbox_style="carto-positron"
)
else:
fig_map = px.scatter_mapbox(
dff_map,
lat="Latitude",
lon="Longitude",
color="Hazard Potential Classification",
color_discrete_sequence=my_colors,
hover_name="Dam Name", # Only show dam name in tooltip
hover_data=[], # No extra data in tooltip
custom_data=["Dam Name"],
zoom=3,
height=500,
mapbox_style="carto-positron",
)
fig_map.update_traces(
marker=dict(size=12, opacity=0.92),
hovertemplate="%{hovertext}<extra></extra>", # Only dam name in tooltip
)
fig_map.update_layout(
margin={"r": 0, "t": 0, "l": 0, "b": 0},
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#222831",
legend=dict(
bgcolor="rgba(255,255,255,0.8)",
font=dict(color="#222831"),
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="center",
x=0.5,
),
dragmode="zoom", # Default to zoom mode
)
# KPIs
kpi_count = f"{len(dff):,}"
kpi_height = (
f"{dff['Dam Height (Ft)'].mean():.1f}" if not dff.empty else "n.a."
)
kpi_hazard = (
dff['Hazard Potential Classification'].mode()[0] if not dff.empty else "n.a."
)
return (
fig_map,
kpi_count,
kpi_height,
kpi_hazard,
)
# --- Dam details card callback ---
@app.callback(
Output("details-card-title", "children"),
Output("details-card-body", "children"),
Input("map", "clickData"),
State("state-dropdown", "value"),
State("hazard-dropdown", "value"),
State("year-dropdown", "value"),
)
def show_dam_details(clickData, selected_states, selected_hazards, selected_years):
if clickData and "points" in clickData:
dam_name = clickData["points"][0]["customdata"][0]
dff = df.copy()
if selected_states:
dff = dff[dff['State'].isin(selected_states)]
if selected_hazards:
dff = dff[dff['Hazard Potential Classification'].isin(selected_hazards)]
if selected_years:
dff = dff[dff['Year Completed'].isin(selected_years)]
dam = dff[dff["Dam Name"] == dam_name]
if not dam.empty:
dam = dam.iloc[0]
title = "Dam Details"
body = [
html.H4(dam["Dam Name"], className="text-primary"),
html.Hr(),
html.P(f"State: {dam['State']}"),
html.P(f"County: {dam['County']}"),
html.P(f"City: {dam['City']}"),
html.P(
f"Year Completed: {int(dam['Year Completed']) if pd.notnull(dam['Year Completed']) else 'n.a.'}"
),
html.P(f"Dam Height (Ft): {dam['Dam Height (Ft)']}"),
html.P(
f"Hazard Potential: {dam['Hazard Potential Classification']}"
),
html.P("Website: "),
html.A(
dam['Website URL'],
href=dam['Website URL'],
target="_blank",
className="text-info",
)
if pd.notnull(dam['Website URL'])
else html.Span("n.a."),
]
return title, body
# Default message if nothing is selected
return "Click on a point to see dam details", html.P("No dam selected.")
if __name__ == "__main__":
app.run(debug=True)