hail_dashboard / app.py
andrewammann's picture
Update app.py
b3fbad6 verified
import os
import sys
import dash
from dash import dcc, html, dash_table, callback, Input, Output, State
import dash_bootstrap_components as dbc
import pandas as pd
from datetime import datetime
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from geopy.extra.rate_limiter import RateLimiter
from geopy.geocoders import Nominatim
from dash.exceptions import PreventUpdate
from vincenty import vincenty
import duckdb
import requests
import urllib
from dotenv import load_dotenv
import time
from functools import wraps
import glob
# Load environment variables
load_dotenv()
# Initialize the Dash app
app = dash.Dash(
__name__,
external_stylesheets=[dbc.themes.BOOTSTRAP],
suppress_callback_exceptions=True
)
app.title = "Hail Damage Analyzer"
server = app.server
# Cache functions
def simple_cache(expire_seconds=300):
def decorator(func):
cache = {}
@wraps(func)
def wrapper(*args, **kwargs):
key = (func.__name__, args, frozenset(kwargs.items()))
current_time = time.time()
if key in cache:
result, timestamp = cache[key]
if current_time - timestamp < expire_seconds:
return result
result = func(*args, **kwargs)
cache[key] = (result, current_time)
return result
return wrapper
return decorator
@simple_cache(expire_seconds=300)
def duck_sql(sql_code):
con = duckdb.connect()
con.execute("PRAGMA threads=2")
con.execute("PRAGMA enable_object_cache")
return con.execute(sql_code).df()
@simple_cache(expire_seconds=300)
def get_data(lat, lon, date_str):
data_dir = r"C:/Users/aammann/OneDrive - Great American Insurance Group/Documents/Python Scripts/hail_data"
parquet_files = glob.glob(f"{data_dir}/hail_*.parquet")
print("Parquet files found:", parquet_files)
if not parquet_files:
raise ValueError("No parquet files found in the specified directory")
file_paths = ", ".join([f"'{file}'" for file in parquet_files])
lat_min, lat_max = lat-1, lat+1
lon_min, lon_max = lon-1, lon+1
code = f"""
SELECT
"#ZTIME" as "Date_utc",
LON,
LAT,
MAXSIZE
FROM read_parquet([{file_paths}], hive_partitioning=1)
WHERE
LAT BETWEEN {lat_min} AND {lat_max}
AND LON BETWEEN {lon_min} AND {lon_max}
AND "#ZTIME" <= '{date_str}'
"""
return duck_sql(code)
def distance(x):
left_coords = (x[0], x[1]) # LAT, LON
right_coords = (x[2], x[3]) # Lat_address, Lon_address
return vincenty(left_coords, right_coords, miles=True)
def geocode(address):
try:
try:
address2 = address.replace(' ', '+').replace(',', '%2C')
df = pd.read_json(
f'https://geocoding.geo.census.gov/geocoder/locations/onelineaddress?address={address2}&benchmark=2020&format=json')
results = df.iloc[0, 0]['results'].iloc[0]['coordinates']
return results['y'], results['x']
except:
geolocator = Nominatim(user_agent="HailDamageAnalyzer")
geocode = RateLimiter(geolocator.geocode, min_delay_seconds=1)
location = geolocator.geocode(address)
if location:
return location.latitude, location.longitude
raise Exception("Geocoding failed")
except:
try:
geocode_key = os.getenv("GEOCODE_KEY")
if not geocode_key:
raise Exception("Geocode API key not found")
address_encoded = urllib.parse.quote(address)
url = f'https://api.geocod.io/v1.7/geocode?q={address_encoded}&api_key={geocode_key}'
response = requests.get(url, verify=False)
response.raise_for_status()
json_response = response.json()
return json_response['results'][0]['location']['lat'], json_response['results'][0]['location']['lng']
except Exception as e:
print(f"Geocoding error: {str(e)}")
raise Exception("Could not geocode address. Please try again with a different address.")
# Layout
app.layout = html.Div([
dcc.Store(id="filtered-data-store"),
dcc.Download(id="download-dataframe-csv"),
dbc.Button("Download Data as CSV", id="btn-download-csv", color="secondary", className="mb-3"),
dbc.Container([
dbc.Row([
dbc.Col([
html.H1("Hail Damage Analyzer", className="text-center my-4"),
html.P("Analyze historical hail data", className="text-center text-muted"),
html.Hr()
], width=12)
]),
dbc.Row([
dbc.Col([
html.Div([
html.H5("Search Parameters", className="mb-3"),
dbc.Form([
dbc.Label("Address"),
dbc.Input(id="address-input", type="text", placeholder="Enter address", value="Dallas, TX", className="mb-3"),
dbc.Label("Maximum Date"),
dcc.DatePickerSingle(
id='date-picker',
min_date_allowed=datetime(2010, 1, 1),
max_date_allowed=datetime(2025, 7, 5),
date=datetime(2025, 7, 5),
className="mb-3 w-100"
),
dbc.Label("Show Data Within"),
dcc.Dropdown(
id='distance-dropdown',
options=[
{'label': 'All Distances', 'value': 'all'},
{'label': 'Within 1 Mile', 'value': '1'},
{'label': 'Within 3 Miles', 'value': '3'},
{'label': 'Within 5 Miles', 'value': '5'},
{'label': 'Within 10 Miles', 'value': '10'}
],
value='all',
className="mb-4"
),
dbc.Button("Search", id="search-button", color="primary", className="w-100 mb-3")
]),
html.Div(id="summary-cards", className="mt-4")
], className="p-3 bg-light rounded-3")
], md=4),
dbc.Col([
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader("Hail Data Overview"),
dbc.CardBody([
dcc.Loading(
id="loading-hail-data",
type="circle",
children=[
html.Div(id="hail-data-table"),
html.Div(id="map-container", className="mt-4")
]
)
])
])
])
]),
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader("Hail Size Over Time"),
dbc.CardBody([
dcc.Loading(
id="loading-hail-chart",
type="circle",
children=[
dcc.Graph(id="hail-size-chart")
]
)
])
], className="mt-4")
])
])
], md=8)
]),
html.Div(id="intermediate-data", style={"display": "none"}),
dbc.Row([
dbc.Col([
html.Hr(),
html.P("Β© 2025 Hail Damage Analyzer", className="text-center text-muted small")
])
], className="mt-4")
], fluid=True)
])
# Main callback
@app.callback(
[Output("intermediate-data", "children"),
Output("summary-cards", "children"),
Output("hail-data-table", "children"),
Output("map-container", "children"),
Output("hail-size-chart", "figure"),
Output("filtered-data-store", "data")],
[Input("search-button", "n_clicks"),
Input("address-input", "n_submit")],
[State("address-input", "value"),
State("date-picker", "date"),
State("distance-dropdown", "value")]
)
def update_all(n_clicks, n_submit, address, date_str, distance_filter):
print("Update all callback triggered") # Debug
ctx = dash.callback_context
if not ctx.triggered:
raise PreventUpdate
try:
lat, lon = geocode(address)
date_obj = datetime.strptime(date_str.split('T')[0], '%Y-%m-%d')
date_formatted = date_obj.strftime('%Y%m%d')
df = get_data(lat, lon, date_formatted)
if df.empty:
error_alert = dbc.Alert("No hail data found for this location and date range.", color="warning")
return dash.no_update, error_alert, "", "", {}, []
df["Lat_address"] = lat
df["Lon_address"] = lon
df['Miles to Hail'] = [
distance(i) for i in df[['LAT', 'LON', 'Lat_address', 'Lon_address']].values
]
df['MAXSIZE'] = df['MAXSIZE'].round(2)
if distance_filter != 'all':
max_distance = float(distance_filter)
df = df[df['Miles to Hail'] <= max_distance]
max_size = df['MAXSIZE'].max()
last_date = df['Date_utc'].max()
total_events = len(df)
summary_cards = dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardBody([
html.H6("Max Hail Size (in)", className="card-title"),
html.H3(f"{max_size:.1f}", className="text-center")
])
], className="text-center")
], md=4, className="mb-3"),
dbc.Col([
dbc.Card([
dbc.CardBody([
html.H6("Last Hail Event", className="card-title"),
html.H3(last_date, className="text-center")
])
], className="text-center")
], md=4, className="mb-3"),
dbc.Col([
dbc.Card([
dbc.CardBody([
html.H6("Total Events", className="card-title"),
html.H3(f"{total_events}", className="text-center")
])
], className="text-center")
], md=4, className="mb-3")
])
df_display = df[['Date_utc', 'MAXSIZE', 'Miles to Hail']].copy()
df_display['Miles to Hail'] = df_display['Miles to Hail'].round(2)
df_display = df_display.rename(columns={
'Date_utc': 'Date',
'MAXSIZE': 'Hail Size (in)',
'Miles to Hail': 'Distance (miles)'
})
data_table = dash_table.DataTable(
id='hail-data-table',
columns=[{"name": i, "id": i} for i in df_display.columns],
data=df_display.to_dict('records'),
page_size=10,
style_table={'overflowX': 'auto'},
style_cell={
'textAlign': 'left',
'padding': '8px',
'minWidth': '50px', 'width': '100px', 'maxWidth': '180px',
'whiteSpace': 'normal'
},
style_header={
'backgroundColor': 'rgb(230, 230, 230)',
'fontWeight': 'bold'
},
style_data_conditional=[
{
'if': {
'filter_query': '{Hail Size (in)} >= 2',
'column_id': 'Hail Size (in)'
},
'backgroundColor': '#ffcccc',
'fontWeight': 'bold'
}
]
)
map_fig = go.Figure()
for _, row in df.iterrows():
size = row['MAXSIZE']
map_fig.add_trace(
go.Scattermapbox(
lon=[row['LON']],
lat=[row['LAT']],
mode='markers',
marker=go.scattermapbox.Marker(
size=size * 3,
color='red',
opacity=0.7
),
text=f"Size: {size} in Date: {row['Date_utc']}",
hoverinfo='text',
showlegend=False
)
)
if not df.empty:
center_lat = df['Lat_address'].iloc[0]
center_lon = df['Lon_address'].iloc[0]
map_fig.add_trace(
go.Scattermapbox(
lon=[center_lon],
lat=[center_lat],
mode='markers',
marker=go.scattermapbox.Marker(
size=14,
color='blue',
symbol='star'
),
text=f"Your Location: {address}",
hoverinfo='text',
showlegend=False
)
)
map_fig.update_layout(
mapbox_style="open-street-map",
mapbox=dict(
center=dict(lat=center_lat, lon=center_lon),
zoom=10
),
margin={"r":0, "t":0, "l":0, "b":0},
height=400
)
df_chart = df.copy()
df_chart['Date'] = pd.to_datetime(df_chart['Date_utc'])
df_chart = df_chart.sort_values('Date')
chart_fig = px.scatter(
df_chart,
x='Date',
y='MAXSIZE',
color='Miles to Hail',
size='MAXSIZE',
hover_data=['Miles to Hail'],
title='Hail Size Over Time',
labels={
'MAXSIZE': 'Hail Size (in)',
'Miles to Hail': 'Distance (miles)'
}
)
chart_fig.update_traces(
marker=dict(
line=dict(width=1, color='DarkSlateGrey'),
opacity=0.7
),
selector=dict(mode='markers')
)
chart_fig.update_layout(
xaxis_title='Date',
yaxis_title='Hail Size (in)',
plot_bgcolor='rgba(0,0,0,0.02)',
paper_bgcolor='white',
hovermode='closest'
)
intermediate_data = df.to_json(date_format='iso', orient='split')
map_figure = dcc.Graph(figure=map_fig)
chart_figure = chart_fig
store_data = df.to_dict('records')
print("Store data populated:", store_data[:2])
return (
intermediate_data,
summary_cards,
data_table,
map_figure,
chart_figure,
store_data
)
except Exception as e:
error_alert = dbc.Alert(f"Error: {str(e)}", color="danger")
return dash.no_update, error_alert, "", "", {}, []
from dash import callback_context
@callback(
Output("download-dataframe-csv", "data"),
[Input("btn-download-csv", "n_clicks")],
[State("filtered-data-store", "data")],
prevent_initial_call=True
)
def download_csv(n_clicks, data):
if not n_clicks or not data:
return dash.no_update
df = pd.DataFrame(data)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"hail_data_export_{timestamp}.csv"
csv_string = df.to_csv(index=False, encoding='utf-8')
return dict(content=csv_string, filename=filename)
if __name__ == '__main__':
print("πŸš€ Dash app is running! Open this link in your browser:")
print("πŸ‘‰ http://localhost:7860/")
app.run(debug=True, host='0.0.0.0', port=7860)