# -*- coding: utf-8 -*-
"""
Created on Mon Mar 17 16:49:24 2025
@author: win11
"""
from dash import Dash, html, Output, Input, dcc, callback_context
import dash_leaflet as dl
import pandas as pd
import dash_bootstrap_components as dbc
import plotly.express as px
import dash_ag_grid as dag
import re
dbc_css = "https://cdn.jsdelivr.net/gh/AnnMarieW/dash-bootstrap-templates/dbc.min.css"
# Load data
df = pd.read_csv('https://raw.githubusercontent.com/plotly/Figure-Friday/refs/heads/main/2025/week-11/us-hurricanes.csv')
df_states = pd.read_csv('https://raw.githubusercontent.com/jasonong/List-of-US-States/refs/heads/master/states.csv')
# Convert abbreviations to full state names
states_dict = pd.Series(df_states.State.values, index=df_states.Abbreviation).to_dict()
# Fix month issues
month_corrections = {"Sp-Oc": "Sep", "Jl-Au": "Jul"}
df["month"] = df["month"].replace(month_corrections)
df["year_month"] = pd.to_datetime(df["year"].astype(str) + "-" + df["month"], format="%Y-%b")
df["year"] = pd.to_numeric(df["year"], errors="coerce")
df = df.dropna().astype({"year": "int"})
df["category"] = pd.to_numeric(df["category"], errors="coerce").astype("Int64")
# Define state coordinates
state_coords = {
"AL": [32.806671, -86.791130], "AK": [61.370716, -152.404419], "AZ": [33.729759, -111.431221],
"AR": [34.969704, -92.373123], "CA": [36.116203, -119.681564], "CO": [39.059811, -105.311104],
"CT": [41.597782, -72.755371], "DE": [39.318523, -75.507141], "FL": [27.766279, -81.686783],
"GA": [33.040619, -83.643074], "HI": [20.796179, -156.331924], "ID": [44.240459, -114.478828],
"IL": [40.349457, -88.986137], "IN": [39.849426, -86.258278], "IA": [42.011539, -93.210526],
"KS": [38.526600, -96.726486], "KY": [37.668140, -84.670067], "LA": [31.169546, -91.867805],
"ME": [44.693947, -69.381927], "MD": [39.063946, -76.802101], "MA": [42.230171, -71.530106],
"MI": [43.326618, -84.536095], "MN": [45.694454, -93.900192], "MS": [32.741646, -89.678696],
"MO": [38.456085, -92.288368], "MT": [46.921925, -110.454353], "NE": [41.125370, -98.268082],
"NV": [38.313515, -117.055374], "NH": [43.452492, -71.563896], "NJ": [40.298904, -74.521011],
"NM": [34.840515, -106.248482], "NY": [42.165726, -74.948051], "NC": [35.630066, -79.806419],
"ND": [47.528912, -99.784012], "OH": [40.388783, -82.764915], "OK": [35.565342, -96.928917],
"OR": [44.572021, -122.070938], "PA": [40.590752, -77.209755], "RI": [41.680893, -71.511780],
"SC": [33.856892, -80.945007], "SD": [44.299782, -99.438828], "TN": [35.747845, -86.692345],
"TX": [31.054487, -97.563461], "UT": [40.150032, -111.862434], "VT": [44.045876, -72.710686],
"VA": [37.769337, -78.169968], "WA": [47.400902, -121.490494], "WV": [38.491226, -80.954456],
"WI": [44.268543, -89.616508], "WY": [42.755966, -107.302490]
}
#columndefs for popup in leaflet popup
columnDefs = [
{"field": "year", "sortable": True },
{"field": "month"},
{"field": "category",
'cellStyle': {
# Set of rules
"styleConditions": [
{
"condition": "params.value == 5",
"style": {"backgroundColor": "rgba(211,84,0,1)"},
},
{
"condition": "params.value == 4",
"style": {"backgroundColor": "rgba(211,84,0,.8)"},
},
{
"condition": "params.value == 3",
"style": {"backgroundColor": "rgba(211,84,0,.6)"},
},
{
"condition": "params.value == 2",
"style": {"backgroundColor": "rgba(211,84,0,.4)"},
},
{
"condition": "params.value == 1",
"style": {"backgroundColor": "rgba(211,84,0,.2)"},
},
],
# Default style if no rules apply
"defaultStyle": {"backgroundColor": "white"},
}},
{"field": "central-pressure-(mb)"},
{"field": "max-wind-(kt)"},
]
# Create a dictionary to track the highest category per state
state_max_category = {}
for entry in df["states-affected-and-category-by-states"]:
for state_abbr, category in re.findall(r"\b([A-Z]{2})\b[^\d]*(\d+)", entry):
category = int(category)
if state_abbr in state_coords:
if state_abbr not in state_max_category or category > state_max_category[state_abbr]:
state_max_category[state_abbr] = category
# Create unique markers
storm_markers = [
dl.Marker(
position=state_coords[state],
id=f"marker-{state}",
#children=dl.Tooltip(f"{states_dict[state]}: Category {category}"),
children=dl.Tooltip(f"{states_dict[state]}"),
n_clicks=0 # Initialize
) for state, category in state_max_category.items()
]
# Dash app
app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP, dbc.icons.FONT_AWESOME, dbc_css])
app.layout = dbc.Container([
dbc.Row([
dbc.Col(dl.Map(center=[41, -85], zoom=4, children=[
dl.TileLayer(),
dl.LayerGroup(id="storm-markers", children=storm_markers)
], style={'height': '50vh'}), width=5),
dbc.Col(html.Div(id='state_information', children="Click a marker for hurricane details"))
])
])
# Callback to update state information on marker click
@app.callback(
Output("state_information", "children"),
[Input(f"marker-{state}", "n_clicks") for state in state_max_category]
)
def update_state_info(*args):
ctx = callback_context
if not ctx.triggered:
return "Click a marker for hurricane details"
clicked_id = ctx.triggered[0]["prop_id"].split(".")[0]
state_abbr = clicked_id.split("-")[-1]
dff = df[df["states-affected-and-category-by-states"].str.contains(state_abbr)].sort_values("year", ascending=False)
fig = px.scatter(dff, x="year_month", y="category", title=f"{states_dict[state_abbr]} Hurricane Trends")
fig.update_yaxes(range=[0, 6])
return html.Div([
html.H4(f"{states_dict[state_abbr]} Hurricane Information"),
html.Div(dcc.Graph(figure=fig)),
html.P(""),
html.Div(dag.AgGrid(
id="column-definitions-basic",
rowData=dff.to_dict("records"),
defaultColDef={"filter": True},
columnDefs=columnDefs,
columnSize="sizeToFit",
dashGridOptions={"animateRows": False}
)),
])
if __name__ == '__main__':
app.run(debug=True)